mybatis详细流程原理


---------------------- 第一阶段XML的解析 ----------------------------------------------------

第一部分:项目结构

user_info表:没什么好说的就3个字段

User实体类:

mapper:UserMapper 为根据id查询用户信息

UserMapper.xml

mybaitis的主配置文件:

数据库连接的属性文件:

测试类:

结构图:


第二部分:mybatis重要组件和运行流程图

  • Configuration: MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中

  • SqlSession: 作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能

  • Executor: MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护

  • StatementHandler: 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等

  • ParameterHandler: 负责对用户传递的参数转换成JDBC Statement 所对应的数据类型

  • ResultSetHandler: 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合

  • TypeHandler: 负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换

  • MappedStatement: MappedStatement维护一条<select|update|delete|insert>节点的封装

  • SqlSource: 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回

  • BoundSql: 表示动态生成的SQL语句以及相应的参数信息


第三部分:初始化源码分析

  @Component
  public class TestMybatis implements CommandLineRunner{

        @Override
        public void run(String... args) throws Exception {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);

            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

            SqlSession session = sqlSessionFactory.openSession();

            try {
                Object o = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById", 1);
            } finally {
                session.close();
            }
        }
 }

这是mybatis操作数据库最基本的步骤,前两行代码没什么好说的就是资源加载mybatis的主配置文件获取输入流对象,我们看第三行代码

    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

这行代码的意思就是根据mybatis-config.xml(主配置文件)的流对象构建一个会话工厂对象。而且还用到了建造者模式--->大致意思就是要创建某个对象不直接new这个对象而是利用其它的类来创建这个对象。mybatis的所有初始化工作都是这行代码完成,那么我们进去一探究竟


第一步:进入build方法

     public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
          // SqlSessionFactory 会创建 XMLConfigBuilder对象 来解析 mybatis-config.xml 配置文件
          // XMLConfigBuilder 继承自 BaseBuilder抽象类,顾名思义这一系的类使用了 建造者设计模式
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
          
          // 解析配置文件的内容 到 Configuration对象,根据 Configuration对象
          // 创建 DefaultSqlSessionFactory对象,然后返回
          return build(parser.parse());
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
          ErrorContext.instance().reset();
          try {
            inputStream.close();
          } catch (IOException e) {
    
          }
        }
    }

可以看到会创建一个XMLConfigBuilder解析器对象,这个对象的作用就是解析mybatis-config.xml(主配置文件)用的。先说明一下,我们可以看出主配置文件的最外层节点是标签,mybatis的初始化就是把这个标签以及他的所有子标签进行解析,把解析好的数据封装在Configuration这个类中


第二步:进入 XMLConfigBuilder.parse方法

  public Configuration parse() {
      if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
      }
      parsed = true;

      // 在 mybatis-config.xml配置文件 中查找 <configuration>节点,并开始解析
      parseConfiguration(parser.evalNode("/configuration"));
      return configuration;
  }

XMLConfigBuilder维护一个parsed属性默认为false,这个方法一开始就判断这个主配置文件是否已经被解析,如果解析过了就抛异常


第三步:进入parseConfiguration(...)方法

   private void parseConfiguration(XNode root) {
      try {
        // 根据 root.evalNode("properties") 中的值就可以知道具体是解析哪个标签的方法咯
        propertiesElement(root.evalNode("properties"));
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        loadCustomLogImpl(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);

        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        
        mapperElement(root.evalNode("mappers"));
      } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
      }
  }

我们可以看出这个方法是对configuration的所有子标签挨个解析。比如常在配置文件中出现的settings属性配置,在settings会配置缓存,日志之类的。


还有typeAliases是配置别名。environments是配置数据库链接和事务。这些子节点会被一个个解析并且把解析后的数据封装在


Configuration 这个类中,可以看第二步方法的返回值就是Configuration对象。在这里我们重点分析的解析mappers这个子标签


这个标签里面还会有一个个的mapper标签去映射mapper所对应的xxMapper.xml,可以回头看看主配置文件


3.1 解析typeHandlers标签

   private void typeHandlerElement(XNode parent) throws Exception {
        if (parent != null) {
          // 处理 <typeHandlers> 下的所有子标签
          for (XNode child : parent.getChildren()) {
            // 处理 <package> 标签
            if ("package".equals(child.getName())) {

              // 获取指定的包名
              String typeHandlerPackage = child.getStringAttribute("name");

              // 通过 typeHandlerRegistry 的 register(packageName)方法
              // 扫描指定包中的所有 TypeHandler类,并进行注册
              typeHandlerRegistry.register(typeHandlerPackage);
            } else {
              // Java数据类型
              String javaTypeName = child.getStringAttribute("javaType");

              // JDBC数据类型
              String jdbcTypeName = child.getStringAttribute("jdbcType");
              String handlerTypeName = child.getStringAttribute("handler");
              Class<?> javaTypeClass = resolveClass(javaTypeName);
              JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
              Class<?> typeHandlerClass = resolveClass(handlerTypeName);

              // 注册
              if (javaTypeClass != null) {
                if (jdbcType == null) {
                  typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                } else {
                  typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                }
              } else {
                typeHandlerRegistry.register(typeHandlerClass);
              }
            }
          }
        }
   }


3.2 解析environments标签

   private void environmentsElement(XNode context) throws Exception {
      if (context != null) {
        // 如果未指定 XMLConfigBuilder 的 environment字段,则使用 default属性 指定的 <environment>环境
        if (environment == null) {
          environment = context.getStringAttribute("default");
        }
        // 遍历 <environment>节点
        for (XNode child : context.getChildren()) {
          String id = child.getStringAttribute("id");
          if (isSpecifiedEnvironment(id)) {
            // 实例化 TransactionFactory
            TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

            // 创建 DataSourceFactory 和 DataSource
            DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
            DataSource dataSource = dsFactory.getDataSource();

            // 创建的 Environment对象 中封装了上面的 TransactionFactory对象 和 DataSource对象
            Environment.Builder environmentBuilder = new Environment.Builder(id)
                .transactionFactory(txFactory)
                .dataSource(dataSource);

            // 将 environment属性值添加到 configuration 中
            configuration.setEnvironment(environmentBuilder.build());
          }
        }
      }
   }


第四步:进入 XMLConfigBuilder.mapperElement方法解析mappers标签


Mybatis 初始化时,除了解析 mybatis-config.xml 主配置文件,还会解析全部的xxMapper.xml映射配置文件,


mybatis-config.xml 文件的 节点 会告诉 Mybatis 去哪里查找映射配置文件,及使用了配置注解标识的接口

    // 解析 <mappers>节点,本方法会创建 XMLMapperBuilder对象 加载映射文件,如果映射配置文件存在
    // 相应的 Mapper接口,也会加载相应的 Mapper接口,解析其中的注解 并完成向 MapperRegistry 的注册
    private void mapperElement(XNode parent) throws Exception {
      if (parent != null) {
        // 处理 <mappers> 的子节点
        for (XNode child : parent.getChildren()) {
          if ("package".equals(child.getName())) {

            // 获取 <package>子节点 中的包名
            String mapperPackage = child.getStringAttribute("name");

            // 扫描指定的包目录,然后向 MapperRegistry 注册 Mapper接口
            configuration.addMappers(mapperPackage);
          } else {

            // 获取 <mapper>节点 的 resource、url、mapperClass属性,这三个属性互斥,只能有一个不为空
            // Mybatis 提供了通过包名、映射文件路径、类全名、URL 四种方式引入映射器。
            // 映射器由一个接口和一个 XML配置文件 组成,XML文件 中定义了一个 命名空间namespace,
            // 它的值就是接口对应的全路径。
            String resource = child.getStringAttribute("resource");
            String url = child.getStringAttribute("url");
            String mapperClass = child.getStringAttribute("class");

            // 如果 <mapper>节点 指定了 resource 或是 url属性,则创建 XMLMapperBuilder对象 解析
            // resource 或是 url属性 指定的 Mapper配置文件
            if (resource != null && url == null && mapperClass == null) {
              ErrorContext.instance().resource(resource);
              InputStream inputStream = Resources.getResourceAsStream(resource);
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              mapperParser.parse();
            } else if (resource == null && url != null && mapperClass == null) {
              ErrorContext.instance().resource(url);
              InputStream inputStream = Resources.getUrlAsStream(url);
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
              mapperParser.parse();
            } else if (resource == null && url == null && mapperClass != null) {

              // 如果 <mapper>节点 指定了 class属性,则向 MapperRegistry 注册 该Mapper接口
              Class<?> mapperInterface = Resources.classForName(mapperClass);
              configuration.addMapper(mapperInterface);
            } else {
              throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
            }
          }
        }
      }
    }

(1)这个方法一开始是一个循环,为什么要循环呢?因为一个mappers节点下面可能会有很多mapper节点。在应用中肯定不止一个mapper.xml。所以他会去遍历每一个mapper节点去解析该节点所映射的xml文件。

(2)循环下面是一个if..else判断。它先判断mappers下面的子节点是不是package节点。因为在实际开发中有很多的xml文件,不可能每一个xml文件都用一个mapper节点去映射,我们干脆会用一个package节点去映射一个包下面的所有的xml,这是多文件映射。

(3)如果不是package节点那肯定就是mapper节点做单文件映射。我们看下面的三行代码,发现单文件映射有3种方式-->1 第一种使用mapper节点的resource属性直接映射xml文件。2 第二种是使用mapper节点url属性映射磁盘内的某个xml文件。

3 第三种是使用mapper节点的class属性直接映射某个mapper接口。可以回头看看我的主配置文件的mappers节点。

(4)实际上映射xml的方式看源码可以得出有4种方式,我们先看单文件映射的resource方式,因为这种方式理解了其他三种方式就比较好理解了。


第五步:看resource方式解析xml

  if (resource != null && url == null && mapperClass == null) {
       ErrorContext.instance().resource(resource);
       InputStream inputStream = Resources.getResourceAsStream(resource);
       XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
       mapperParser.parse();
  }

(1)第一行代码的意思是实例化一个错误上下文对象。这个对象是干什么用的呢?我们回忆一下我们使用mybatis的过程中如果出现错误会不会提示这个错误在哪个xml中,还提示这个错误在xml中的哪个sql中。这个对象的作用就是把错误信息封装起来,

如果出现错误就会调用这个对象的toString方法,感兴趣的可以去看看这个对象的源码。这个resource参数就是String类型的xml的名字,在我们的项目中是UserMapper.xml

(2) 第二行没什么好说的就是读取这个xml获取输入流对象

(3)然后创建一个mapper的xml文件解析器,你看他们这名字起的,一看就知道是什么意思。就好比那个XMLConfigBuilder一看就是解析主配置文件用的


第六步:进入XMLMapperBuilder.parse()方法:

   public void parse() {

      // 是否已经加载过该配置文件
      if (!configuration.isResourceLoaded(resource)) {

         // 解析 <mapper>节点
         configurationElement(parser.evalNode("/mapper"));

        // 将 resource 添加到 configuration 的 loadedResources属性 中,
        // 该属性是一个 HashSet<String>类型的集合,其中记录了已经加载过的映射文件
        configuration.addLoadedResource(resource);

        // 注册 Mapper接口
        bindMapperForNamespace();
      }
      // 处理 configurationElement()方法 中解析失败的 <resultMap>节点
      parsePendingResultMaps();

      // 处理 configurationElement()方法 中解析失败的 <cacheRef>节点
      parsePendingCacheRefs();

      // 处理 configurationElement()方法 中解析失败的 <statement>节点
      parsePendingStatements();
  }

(1)一开始就一个判断这个xml是否被解析过了。因为onfiguration对象会维护一个String类型的set集合loadedResources,这个集合中存放了所有已经被解析过的xml的名字,我们在这里是没有被解析的,所以进入if中


第七步:进入 XMLMapperBuilder.configurationElement方法

  private void configurationElement(XNode context) {
      try {
        // 获取 <mapper>节点 的 namespace属性
        String namespace = context.getStringAttribute("namespace");

        if (namespace == null || namespace.equals("")) {
          throw new BuilderException("Mapper's namespace cannot be empty");
        }

        // 使用 MapperBuilderAssistant对象 的 currentNamespace属性 记录 namespace命名空间
        builderAssistant.setCurrentNamespace(namespace);

        // 解析 <cache-ref>节点,后面的解析方法 也都见名知意
        cacheRefElement(context.evalNode("cache-ref"));

        // 开启二级缓存的标签
        cacheElement(context.evalNode("cache"));          
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
      } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
      }
  }

二级缓存的属性设置

 private void cacheElement(XNode context) {
    if (context != null) {
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

二级缓存的对象创建

   public Cache useNewCache(Class<? extends Cache> typeClass,
        Class<? extends Cache> evictionClass,
        Long flushInterval,
        Integer size,
        boolean readWrite,
        boolean blocking,
        Properties props) {
      Cache cache = new CacheBuilder(currentNamespace)
          .implementation(valueOrDefault(typeClass, PerpetualCache.class))
          .addDecorator(valueOrDefault(evictionClass, LruCache.class))
          .clearInterval(flushInterval)
          .size(size)
          .readWrite(readWrite)
          .blocking(blocking)
          .properties(props)
          .build();
      configuration.addCache(cache);
      currentCache = cache;           // 将二级缓存对象设置到 currentCache 属性中
      return cache;
  }

这个方法就是解析一个mapper.xml所有节点数据。比如解析namespace,resultMap等等。重点是最后一句

    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

我们进入这个 XMLMapperBuilder.buildStatementFromContext 方法中

  private void buildStatementFromContext(List<XNode> list) {
      if (configuration.getDatabaseId() != null) {
          buildStatementFromContext(list, configuration.getDatabaseId());
      }
      buildStatementFromContext(list, null);
  }

没什么好说的,继续进入buildStatementFromContext()遍历XML节点一个一个获取

   private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
          final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
          try {
            statementParser.parseStatementNode();
          } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
          }
        }
   }

(1)这个方法一开始是一个循环,遍历一个list,这个list里装的是xml中的所有sql节点,比如select、insert、update、delete ,每一个sql是一个节点。循环解析每一个sql节点

(2)创建一个xml的会话解析器去解析每个节点


第八步:进入XMLStatementBuilder.parseStatementNode方法

  public void parseStatementNode() {

      String id = context.getStringAttribute("id");
      String databaseId = context.getStringAttribute("databaseId");

      if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
      }

      String nodeName = context.getNode().getNodeName();
      SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
      boolean useCache = context.getBooleanAttribute("useCache", isSelect);          // 开启二级缓存
      boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

      // Include Fragments before parsing
      XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
      includeParser.applyIncludes(context.getNode());

      String parameterType = context.getStringAttribute("parameterType");
      Class<?> parameterTypeClass = resolveClass(parameterType);

      String lang = context.getStringAttribute("lang");
      LanguageDriver langDriver = getLanguageDriver(lang);

      // Parse selectKey after includes and remove them.
      processSelectKeyNodes(id, parameterTypeClass, langDriver);

      // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
      KeyGenerator keyGenerator;
      String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);

      if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
      } else {
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
            configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      }

      // 这里做SQL处理,将 select * from user where id = #{id} 转为 select * from user where id = ?
      SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); 
      StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
      Integer fetchSize = context.getIntAttribute("fetchSize");
      Integer timeout = context.getIntAttribute("timeout");
      String parameterMap = context.getStringAttribute("parameterMap");
      String resultType = context.getStringAttribute("resultType");
      Class<?> resultTypeClass = resolveClass(resultType);
      String resultMap = context.getStringAttribute("resultMap");
      String resultSetType = context.getStringAttribute("resultSetType");
      ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
      String keyProperty = context.getStringAttribute("keyProperty");
      String keyColumn = context.getStringAttribute("keyColumn");
      String resultSets = context.getStringAttribute("resultSets");

      builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
          fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
          resultSetTypeEnum, flushCache, useCache, resultOrdered,
          keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
 }

看到这个方法很长,其实大致意思就是解析这个sql标签里的所有数据,并把所有数据通过addMappedStatement这个方法封装在MappedStatement这个对象中,


这个对象中封装了一条sql所在标签的所有内容,比如这个sql标签的id ,sql语句,入参,出参,等等。

  <select id="findById" resultType="com.atguigu.mybatis.entity.User" parameterType="java.lang.Integer">
        select * from user where id = #{id}
  </select>


我们要牢记一个sql的标签对应一个MappedStatement对象


XMLScriptBuilder#parseScriptNode用于解析动态sql:

  public SqlSource parseScriptNode() {
    // 解析select\insert\ update\delete标签中的SQL语句,最终将解析到的SqlNode封装到MixedSqlNode中的List集合中
    // ****将带有${}号的SQL信息封装到TextSqlNode
    // ****将带有#{}号的SQL信息封装到StaticTextSqlNode
    // ****将动态SQL标签中的SQL信息分别封装到不同的SqlNode中
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;

    if (isDynamic) {
      // 如果SQL中包含${}和动态SQL语句,则将SqlNode封装到DynamicSqlSource
      // 最终结果是:select id from blog where id = ${id}
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);

    } else {
      // 如果SQL中包含#{},则将SqlNode封装到RawSqlSource中,并指定parameterType
      // 最终的结果是:select id from blog where id = ?
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }

    return sqlSource;
}


parseDynamicTags解析sql语句,

判断具体走new DynamicSqlSource(configuration, rootSqlNode) 还是走new RawSqlSource(configuration, rootSqlNode, parameterType) ?

解析select\insert\ update\delete标签中的SQL语句,最终将解析到的SqlNode封装到MixedSqlNode中的List集合中

  // org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
  protected MixedSqlNode parseDynamicTags(XNode node) {
      List<SqlNode> contents = new ArrayList<>();
      //获取<select>\<insert>\<update>\<delete>4个标签的子节点,子节点包括元素节点和文本节点
      NodeList children = node.getNode().getChildNodes();

      for (int i = 0; i < children.getLength(); i++) {
        // 获取标签内的原始自定义sql:select * from blog where id = #{id}
        XNode child = node.newXNode(children.item(i));

        // 处理文本节点
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {

          String data = child.getStringBody("");

          // 将文本内容封装到SqlNode中,还是原始sql
          TextSqlNode textSqlNode = new TextSqlNode(data);

          // SQL语句中带有${}的话,就表示是dynamic的
          if (textSqlNode.isDynamic()) {
            contents.add(textSqlNode);
            isDynamic = true;
          } else {
            // SQL语句中(除了${}和下面的动态SQL标签),就表示是static的
            // StaticTextSqlNode的apply只是进行字符串的追加操作
            contents.add(new StaticTextSqlNode(data));
          }

          //处理元素节点
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { 
          // issue #628
          String nodeName = child.getNode().getNodeName();

          // 动态SQL标签处理器
          // 策略模式
          NodeHandler handler = nodeHandlerMap.get(nodeName);

          if (handler == null) {
            throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
          }

          handler.handleNode(child, contents);

          // 动态SQL标签是dynamic的
          isDynamic = true;
        }
      }
      return new MixedSqlNode(contents);
  }


如果SQL中包含${}和动态SQL语句,则走以下代码:sqlSource = new DynamicSqlSource(configuration, rootSqlNode)

将SqlNode封装到DynamicSqlSource中,最终返回结果是:select id from blog where id = ${id},但要将 select id from blog where id = ${id}转为

select id from blog where id = 1,


再第三阶段openSession.selectOne("findById", 1)的第四步:获取BoundSql对象,BoundSql boundSql = sqlSource.getBoundSql(parameterObject),此时的sqlSource为DynamicSqlSource


如果SQL中包含#{},则走以下代码:sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType)

 public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    // 解析SQL语句
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);

    // 获取入参类型
    Class<?> clazz = parameterType == null ? Object.class : parameterType;

    // 开始解析
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
 }

SqlSourceBuilder#parse方法解析sql

  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {

      ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);

      // 创建分词解析器
      GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);

      // 解析#{},最终sql解析为select * from blog where id = ?
      String sql = parser.parse(originalSql);

      // 将解析之后的SQL信息,封装到StaticSqlSource对象中
      // SQL字符串是带有?号的字符串,?相关的参数信息,封装到ParameterMapping集合中
      return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }


第九步:进入MapperBuilderAssistant.addMappedStatement方法

   public MappedStatement addMappedStatement(
            String id,
            SqlSource sqlSource,
            StatementType statementType,
            SqlCommandType sqlCommandType,
            Integer fetchSize,
            Integer timeout,
            String parameterMap,
            Class<?> parameterType,
            String resultMap,
            Class<?> resultType,
            ResultSetType resultSetType,
            boolean flushCache,
            boolean useCache,               // 使用二级缓存,在<select>标签中设置useCache属性,默认true
            boolean resultOrdered,
            KeyGenerator keyGenerator,
            String keyProperty,
            String keyColumn,
            String databaseId,
            LanguageDriver lang,
            String resultSets) {


          if (unresolvedCacheRef) {
            throw new IncompleteElementException("Cache-ref not yet resolved");
          }


          id = applyCurrentNamespace(id, false);
          boolean isSelect = sqlCommandType == SqlCommandType.SELECT;


          MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
              .resource(resource)
              .fetchSize(fetchSize)
              .timeout(timeout)
              .statementType(statementType)
              .keyGenerator(keyGenerator)
              .keyProperty(keyProperty)
              .keyColumn(keyColumn)
              .databaseId(databaseId)
              .lang(lang)
              .resultOrdered(resultOrdered)
              .resultSets(resultSets)
              .resultMaps(getStatementResultMaps(resultMap, resultType, id))
              .resultSetType(resultSetType)
              .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
              .useCache(valueOrDefault(useCache, isSelect))              // 使用二级缓存,在<select>标签中设置useCache属性,默认true
              .cache(currentCache);                                      // 将二级缓存保存到MappedStatement对象中       

          ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
          if (statementParameterMap != null) {
            statementBuilder.parameterMap(statementParameterMap);
          }

          MappedStatement statement = statementBuilder.build();
          configuration.addMappedStatement(statement);
          return statement;
   }

乍一看这个方法很长,我们只看最后三行代码:

(1) MappedStatement statement = statementBuilder.build();通过解析出的参数构建了一个MapperStatement对象。


(2)configuration.addMappedStatement(statement); 这行是把解析出来的MapperStatement装到Configuration维护的Map集合中。

key值是这个sql标签的id值,我们这里应该就是selectUserById,value值就是我们解析出来的MapperStatement对象。


其实我们解析xml的目的就是把每个xml中的每个增删改查的SQL语句标签解析成一个个MapperStatement并把解析出来的这些对象装到Configuration的Map中备用


第十步: 返回第六步的代码:

   public void parse() {

      if (!configuration.isResourceLoaded(resource)) {
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
      }

      parsePendingResultMaps();
      parsePendingCacheRefs();
      parsePendingStatements();
  }

刚才到第九步都是在执行configurationElement(parser.evalNode("/mapper"));这行代码,接下来看下一行代码configuration.addLoadedResource(resource);


到第九步的时候我们已经把一个xml完全解析完了,所以在此就会把这个解析完的xml的名字装到configuration中


第十一步:进入bindMapperForNamespace()方法


bindMapperForNamespace()方法作用:将每个xxMapper.xml【映射配置文件】的命名空间与对应 Mapper 接口的绑定,并注册到 MapperRegistry

  private void bindMapperForNamespace() {

      // 获取映射配置文件的命名空间
      String namespace = builderAssistant.getCurrentNamespace();

      if (namespace != null) {
        Class<?> boundType = null;
        try {
          // 解析命名空间对应的类型
          boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {}

        if (boundType != null) {

          // 是否已加载 boundType接口
          if (!configuration.hasMapper(boundType)) {

            // 追加个 "namespace:" 的前缀,并添加到 Configuration 的 loadedResources集合中
            configuration.addLoadedResource("namespace:" + namespace);

            // 添加到 Configuration的mapperRegistry集合中
            configuration.addMapper(boundType);
          }
        }
      }
  }

(1)一开始获取名称空间,名称空间一般都是我们mapper的全限定名,通过它反射获取这个Mapper接口的代理对象

(2)if判断,Configuration中也维护了一个knownMappers对象,

key值是由namespace通过条件反射生成的mapper的class对象,value是由namespace产生的对应的mapper的MapperProxyFactory代理对象工厂


第十二步:进入addMapper()方法

    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }
  public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
          if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
          }

          boolean loadCompleted = false;
          try {
            knownMappers.put(type, new MapperProxyFactory<>(type));

            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }
  }

我们可以看出mapperRegistry这个类维护的Map的名字是knownMappers---->(已知的mapper--->就是注册过的mapper).


我们看他的put,key是由namespace命名空间生成的class对象,value是MapperProxy的代理工厂new MapperProxyFactory<>(type)


到此mybatis根据主配置文件初始化就完成了,那说了这么久到底做了什么呢?我们总结一下:

1、总的来说就是解析主配置文件(mybatis-configuration.xml)把主配置文件里的所有信息包括映射文件信息(xxMapper.xml)封装到Configuration这个对象中


2、稍微细一点就是 通过XmlConfigBuilder解析主配置文件,然后通过XmlMapperBuild解析mappers下映射的所有xml文件(循环解析)。

把每个xml中的各个sql解析成一个个MapperStatement对象装在Configuration维护的一个名为mappedStatements的Map集合中(key值是sql标签的id值,value是mapperstatement对象)

----->每个xxMapper.xml【映射配置文件】的命名空间与对应 Mapper 接口的绑定是维护在Configuration对象 -->

mapperRegistry-->knownMappers的Map集合中(key是:namespace命名空间的class对象,value是:namespace命名空间对应的代理工厂)


---------------------- 第二阶段SqlSession的创建 ----------------------------------------------------


第一步:获取session会话对象源码分析

    SqlSession session = sqlSessionFactory.openSession();

    public SqlSession openSession() {
      return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
    }

   private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          // 获取配置的Environment对象
          final Environment environment = configuration.getEnvironment();

          // 从environment中获取TransactionFactory对象,如果没有,就创建一个ManagedTransactionFactory实例并返回
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

          // 从事务工厂中获取一个事务对象
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

          // 根据事务对象tx和配置的Executor类型execType创建Executor实例
          // ExecutorType是个枚举类型,有三个值 SIMPLE, REUSE, BATCH,分别对应了SimpleExecutor、ReuseExecutor、BatchExecutor
          final Executor executor = configuration.newExecutor(tx, execType);

          // 创建DefaultSqlSession对象
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
  }

我们看第二段代码:因为我们解析主配置文件把所有的节点信息都保存在了configuration对象中,它开始直接或得Environment节点的信息,这个节点配置了数据库连接和事务。


之后通过Environment创建了一个事务工厂,然后通过事务工厂实例化了一个事务对象。 ------> 最后他创建了一个执行器Executor


我们知道session是与数据库交互的顶层api,session中会维护一个Executor 来负责sql生产和执行和查询缓存等。我们再来看看new这个执行器的时候的过程

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;

        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          executor = new SimpleExecutor(this, transaction);
        }

        if (cacheEnabled) {
          executor = new CachingExecutor(executor);
        }

        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
 }
  • SimpleExecutor: 简单执行器,是 MyBatis 中默认使用的执行器,每次执行 SQL 都会创建一个新的 Statement 对象,执行完毕后立即关闭

  • ReuseExecutor: 可重用执行器,缓存同一个 SQL 对应的 Statement 对象,避免重复创建

  • BatchExecutor: 批处理执行器,将多个 UPDATE/INSERT/DELETE 操作添加addBatch() 缓存中,调用 flushStatements() 时一次性提交

  public SimpleExecutor(Configuration configuration, Transaction transaction) {
      super(configuration, transaction);
  }
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.transaction = transaction;                      // 事务
        this.deferredLoads = new ConcurrentLinkedQueue<>();
        this.localCache = new PerpetualCache("LocalCache");  // 一级缓存
        this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
        this.closed = false;
        this.configuration = configuration;
        this.wrapper = this;
    }

    executor = new CachingExecutor(executor);

    public CachingExecutor(Executor delegate) {
          this.delegate = delegate;             // SimpleExecutor执行器
          delegate.setExecutorWrapper(this);
   }

  // 创建DefaultSqlSession对象
  new DefaultSqlSession(configuration, executor, autoCommit);

  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
      this.configuration = configuration;
      this.executor = executor;               // CachingExecutor执行器
      this.dirty = false;
      this.autoCommit = autoCommit;
  }

创建一个DefaultSqlSession,这里面维护了Configuration和Executor


---------------------- 第三阶段openSession.selectOne("findById", 1)解析 ----------------------------------------------------


第一步:进入selectOne()方法

    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.selectList(statement, parameter);
        if (list.size() == 1) {
          return list.get(0);
        } else if (list.size() > 1) {
          throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
          return null;
        }
    }


第二步:进入selectList()方法

   public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
          try {
            // 从configuration中取出封装了sql标签信息的MapperStatement对象	
            MappedStatement ms = configuration.getMappedStatement(statement);

            return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

          } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
          } finally {
            ErrorContext.instance().reset();
          }
  }


第三步:进入CachingExcutor缓存执行器的query方法

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
      // 获取BoundSql对象
      BoundSql boundSql = ms.getBoundSql(parameterObject);

      // 创建CacheKey对象,该对象由多个参数组装而成
      CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);

      // query方法的重载,进行后续处理
      return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
 }


第四步:获取BoundSql对象

   public BoundSql getBoundSql(Object parameterObject) {

          // 获取组装完成的sql
          BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
          List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

          if (parameterMappings == null || parameterMappings.isEmpty()) {
            boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
          }

          for (ParameterMapping pm : boundSql.getParameterMappings()) {
            String rmId = pm.getResultMapId();
            if (rmId != null) {
              ResultMap rm = configuration.getResultMap(rmId);
              if (rm != null) {
                hasNestedResultMaps |= rm.hasNestedResultMaps();
              }
            }
          }
          return boundSql;
  }


第五步:执行DynamicSqlSource的getBoundSql方法

   public BoundSql getBoundSql(Object parameterObject) {
          // 将参数封装成动态上下文,DynamicContext中sqlBuilder就是最后组装的sql
          DynamicContext context = new DynamicContext(this.configuration, parameterObject);

          // 使用了责任链模式,根据条件,动态组装sql
          this.rootSqlNode.apply(context);
          SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration);
          Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();

          // 将#{参数}替换为?进行占位
          SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
          BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
          Map var10000 = context.getBindings();
          Objects.requireNonNull(boundSql);
          var10000.forEach(boundSql::setAdditionalParameter);
          return boundSql;
  }


第六步:返回第三步,创建CacheKey对象

   public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
          if (closed) {
            throw new ExecutorException("Executor was closed.");
          }

          // 可以看到CacheKey对象由MappedStatement的id、RowBounds的offset和limit
          // sql语句(包含占位符"?")、用户传递的实参组成
          CacheKey cacheKey = new CacheKey();
          cacheKey.update(ms.getId());
          cacheKey.update(rowBounds.getOffset());
          cacheKey.update(rowBounds.getLimit());
          cacheKey.update(boundSql.getSql());
          List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
          TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

          // 获取用户传入的实参,并添加到CacheKey对象中
          for (ParameterMapping parameterMapping : parameterMappings) {

            // 过滤掉输出类型的参数
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              Object value;
              String propertyName = parameterMapping.getProperty();

              if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (parameterObject == null) {
                value = null;
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
              } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
              }

              // 将实参添加到CacheKey对象中
              cacheKey.update(value);
            }
          }

          // 如果configuration的environment不为空,则将该environment的id
          // 添加到CacheKey对象中
          if (configuration.getEnvironment() != null) {
            cacheKey.update(configuration.getEnvironment().getId());
          }

          return cacheKey;
   }


第七步:返回第三步,调用CachingExecutor#query()方法

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        // 真正开启二级缓存的开关
        Cache cache = ms.getCache(); 
                      
        if (cache != null) {
          flushCacheIfRequired(ms);
          if (ms.isUseCache() && resultHandler == null) {   // 使用二级缓存,在<select>标签中设置useCache属性,默认true
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);  // 从二级缓存中获取数据
            if (list == null) {
              list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
              tcm.putObject(cache, key, list);   // 将数据加入二级缓存中
            }
            return list;
          }
        }
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
   }

delegate在以下地方赋值

   public Executor newExecutor(Transaction transaction, ExecutorType executorType) {

        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;

        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction); 
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          executor = new SimpleExecutor(this, transaction);
        }

        if (cacheEnabled) {
          executor = new CachingExecutor(executor); // 赋值delegate
        }

        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
  }
   public CachingExecutor(Executor delegate) {
        this.delegate = delegate;                 // 赋值delegate
        delegate.setExecutorWrapper(this);
  }

我们点进去发现是进入的Executor接口,不慌,找他的实现类,它先走的是CachingExcutor缓存执行器,我们研究一下代码,


我们看第二段代码他一开始从MapperStatement中获取BoundSql 这个对象,因为真正的sql语句封装在这个对象中,


而且这个对象也负责把sql中的占位符替换成我们传的参数,只是MapperStatement维护了BoundSql 的引用而已


第八步:delegate.query:则是调用了SimpleExcutor简单执行器#query()方法,因为SimpleExcutor的引用维护到CachingExcutor中

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
          // 检查当前Executor是否已关闭
          ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
          if (closed) {
            throw new ExecutorException("Executor was closed.");
          }

          if (queryStack == 0 && ms.isFlushCacheRequired()) {
            // 非嵌套查询,且<select>节点配置的flushCache属性为true时,才会清空一级缓存
            clearLocalCache();
          }

          List<E> list;
          try {
            // 增加查询层数
            queryStack++;

            // 根据传入的CacheKey对象 查询一级缓存
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

            if (list != null) {
              // 在一级缓存命中时,获取缓存中保存的输出类型参数
              handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
              // 缓存未命中,则从数据库查询结果集,其中会调用doQuery()方法完成数据库查询操作
              list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
          } finally {
            // 当前查询完成,查询层数减少
            queryStack--;
          }

          if (queryStack == 0) {
              // 延迟加载的相关内容
              for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
              }

              deferredLoads.clear();
              if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                clearLocalCache();
              }
          }
          return list;
  }

一开始声明了一个集合list,然后通过我们之前创建的缓存key去一级缓存localCache中查询是否有缓存,下面判断,如果集合不是null就处理一下缓存数据直接返回list,


如果没有缓存,他回从数据库中查,你看他们这名字起的一看就知道是什么意思queryFromDatabase,


我们现在执行的是第一条selectOne,没有缓存我们进入queryFromDatabase方法


第九步:进入queryFromDatabase方法,将查询的数据放入localCache【一级缓存中】

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);

        try {
          list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
          localCache.removeObject(key);
        }

        localCache.putObject(key, list);

        if (ms.getStatementType() == StatementType.CALLABLE) {
          localOutputParameterCache.putObject(key, parameter);
        }

        return list;
  }

你看这段代码,先在本地缓存中占个位,然后执行doQuery从数据库中查数据,然后移除刚才的缓存中的占位,最后把查出来的数据put进本地缓存中,


我不知道他这个占位又移除到底想搞什么幺蛾子,反正我们明白,那不重要,重要的是他执行了doQuery从数据库中查到数据并放入缓存中,


我们接着看一下doQuery这个方法的代码


第十步:调用SimpleExecutor执行器的doQuery方法

SimpleExecutor 继承了 BaseExecutor 抽象类,它是最简单的 Executor 接口实现。Executor 组件使用了模板方法模式,


一级缓存等固定不变的操作都封装到了 BaseExecutor 中,在 SimpleExecutor 中就不必再关心一级缓存等操作,只需要专注实现 4 个基本方法的实现即可

  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
          // 获取配置对象
          Configuration configuration = ms.getConfiguration();

          // 创建StatementHandler对象
          StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
          
          // 完成Statement的创建和初始化,该方法首先会调用StatementHandler的prepare()方法
          // 创建Statement对象,然后调用StatementHandler的parameterize()方法处理占位符
          stmt = prepareStatement(handler, ms.getStatementLog());

          // 调用StatementHandler的query()方法,执行sql语句,并通过ResultSetHandler
          // 完成结果集的映射
          return handler.query(stmt, resultHandler);
        } finally {
          // 关闭Statement对象
          closeStatement(stmt);
        }
  }

  public interface StatementHandler {
      //预编译处理器,声明Statement,通过JDBC中的Connection声明
      Statement prepare(Connection connection) throws SQLException;
      
      //绑定参数  
      void parameterize(Statement statement) throws SQLException;
      
      //批量操作
      void batch(Statement statement)  throws SQLException;
      
      //更新操作
      int update(Statement statement) throws SQLException;
      
      //查询操作
      <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
      
      //获取当前操作的动态SQL
      BoundSql getBoundSql();

      //获取当前操作的参数处理器
      ParameterHandler getParameterHandler();
  }

StatementHandler接口的实现类有三个分别是:

  • SimpleStatementHandler:创建普通的Statement简单处理器,执行静态SQL

  • PreparedStatementHandler:创建PrepareStatement预编译处理器,可以进行预处理,设置预编译参数,防止SQL注入

  • CallableStatementHandler:创建CallabelStatement存储过程处理器,是用来执行存储过程的,设置出参和读取出参

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
 }
   public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
          switch (ms.getStatementType()) {
            case STATEMENT:
              delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
              break;
            case PREPARED:
              delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
              break;
            case CALLABLE:
              delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
              break;
            default:
              throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
          }
  }

  // 构造参数
  public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
  }
  protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

          // 核心配置对象
          this.configuration = mappedStatement.getConfiguration();

          // 记录执行SQL语句的Executor对象
          this.executor = executor;

          // 记录SQL语句信息的MappedStatement对象
          this.mappedStatement = mappedStatement;
          this.rowBounds = rowBounds;

          
          this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
          this.objectFactory = configuration.getObjectFactory();

          if (boundSql == null) { 
            generateKeys(parameterObject);
            boundSql = mappedStatement.getBoundSql(parameterObject);
          }
          // 记录SQL语句对应的BoundSql对象
          this.boundSql = boundSql;

          // 创建参数处理器
          this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);

          // 创建结果集处理器
          this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
 }

StatementHandler其操作流程如下:

1、使用Configuration.newStatementHandler()创建 ,主要逻辑是基于MappedStatement的sql语句配置的StatementType(statement、prepared、callable)创建对应类型的StatementHandler

2、根据不同类型的StatementHandler,创建不同类型的Statement(PreparedStatement、CallableStatement)

3、将请求入参填充到SQL语句中,由parameterHandler参数处理器进行处理,最终底层是由不同的TypeHander类型处理器实现参数填充 ps.setInt(i, parameter)-- java-->JDBC;

4、执行SQL语句查询操作,ps.execute();

5、获得结果集,由resultSetHandler结果集处理器进行处理,


第十一步:返回第十步的prepareStatement(handler, ms.getStatementLog())方法

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {

        Statement stmt;
        Connection connection = getConnection(statementLog);

        // 创建Statement对象
        stmt = handler.prepare(connection, transaction.getTimeout());

        // 处理占位符
        handler.parameterize(stmt);
        return stmt;
}
  // 创建Statement对象
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {

        ErrorContext.instance().sql(boundSql.getSql());
        Statement statement = null;

        try {
          statement = instantiateStatement(connection);
          setStatementTimeout(statement, transactionTimeout);
          setFetchSize(statement);
          return statement;
        } catch (SQLException e) {
          closeStatement(statement);
          throw e;
        } catch (Exception e) {
          closeStatement(statement);
          throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
        }
 }
 // 直接调用 JDBC Connection 的 prepareStatement()方法,创建 PreparedStatement 对象。当 mappedStatement.getKeyGenerator() 返回的类型为 Jdbc3KeyGenerator时,
 // 根据keyColumnNames包含的字段来初始化PreparedStatement对象
 protected Statement instantiateStatement(Connection connection) throws SQLException {

          String sql = boundSql.getSql();

          if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
              String[] keyColumnNames = mappedStatement.getKeyColumns();
              if (keyColumnNames == null) {
                //返回数据库生成的主键
                return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); // 相当于JDBC获取操作对象
              } else {

                //在insert语句执行完成之后,会将keyColumnNames指定的列返回
                return connection.prepareStatement(sql, keyColumnNames);
              }
          } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
            return connection.prepareStatement(sql);
          } else {
            return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
          }
 }


第十二步:返回第十一步,执行handler.parameterize(stmt)方法

   // PreparedStatementHandler#parameterize方法
   public void parameterize(Statement statement) throws SQLException {
        parameterHandler.setParameters((PreparedStatement) statement);
   }

   /**
    * parameterHandler.setParameters((PreparedStatement) statement); 
    * 则实质调用的是BaseStatementHandler类里的:this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql)的setParameters 
    * 
    */
  public void setParameters(PreparedStatement ps) {

        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());

        // 从boundSql中获取参数映射列表
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
          for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              Object value;
              String propertyName = parameterMapping.getProperty();

              if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (parameterObject == null) {
                value = null;
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
              } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
              }

              // 获取类型处理器,主要是根据方法入参类型确定的
              TypeHandler typeHandler = parameterMapping.getTypeHandler();
              JdbcType jdbcType = parameterMapping.getJdbcType();
              if (value == null && jdbcType == null) {
                jdbcType = configuration.getJdbcTypeForNull();
              }

              try {
                // 使用类型处理器,为预处理语句设置参数值
                typeHandler.setParameter(ps, i + 1, value, jdbcType);
              } catch (TypeException | SQLException e) {
                throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
              }
            }
          }
        }
 }

typeHandler.setParameter(ps, i + 1, value, jdbcType)该方法的作用:该方法 setParameter 负责将查询参数设置到 PreparedStatement对象中。

它从 boundSql 对象中获取ParameterMapping参数映射器,使用 TypeHandler 将ParameterMapping参数映射器中的参数设置到 PreparedStatement中,

这样可以确保在执行 SQL 查询时,参数会被正确地传递到数据库中


调用BaseTypeHandler.setParameter方法

 public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        if (parameter == null) {

            if (jdbcType == null) {
              throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
            }

            try {
              ps.setNull(i, jdbcType.TYPE_CODE);
            } catch (SQLException e) {
              throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
                    + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
                    + "Cause: " + e, e);
            }

        } else {
            try {
              // 根据参数的类型,调用不同XXTypeHandler进行传参处理
              setNonNullParameter(ps, i, parameter, jdbcType);
            } catch (Exception e) {
              throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
                    + "Try setting a different JdbcType for this parameter or a different configuration property. "
                    + "Cause: " + e, e);
            }
        }
 }

例如参数类型为Integer,则调用IntegerTypeHandler.setNonNullParameter方法

  public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) throws SQLException {
        // 传参设值处理
        ps.setInt(i, parameter);
 }


第十三步:返回第十步执行handler.query(stmt, resultHandler)方法,实则是调用PreparedStatementHandler#query方法

  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {

        PreparedStatement ps = (PreparedStatement) statement;

        //1.执行语句,调用 PreparedStatement 的execute方法
        ps.execute();

        // 通过 ResultSetHandler 对象封装结果集,映射成 JavaBean
        return resultSetHandler.<E> handleResultSets(ps);
 }

到这里就差不多了,代码最后回调用PreparedStatement#execute方法去执行,执行的结果会保存到PreparedStatement中,然后通过ResultSetHandler去处理结果集,


则实质调用的是BaseStatementHandler类里的:this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);


默认实现为DefaultResultSetHandler代码来到org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets


第十四步:执行handleResultSets方法,处理 ResultSet的结果,将其转换成一个 List<java对象>

  public List<Object> handleResultSets(Statement stmt) throws SQLException {

        // 记录活动日志
        ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
        
        // 初始化结果列表,创建一个列表 multipleResults 来存储处理后JavaBean的结果集
        final List<Object> multipleResults = new ArrayList<Object>();

        int resultSetCount = 0;

        //1、Statement 对象中获取第一个结果集的封装对象 ResultSetWrapper
        ResultSetWrapper rsw = getFirstResultSet(stmt);
    	
        //2、从mappedStatement中拿到ResultMap,也就是我们在mapper.xml总定义的<resultMap/>结果集映射器
        List<ResultMap> resultMaps = mappedStatement.getResultMaps();

        int resultMapCount = resultMaps.size();
        validateResultMapsCount(rsw, resultMapCount);

        while (rsw != null && resultMapCount > resultSetCount) {
          ResultMap resultMap = resultMaps.get(resultSetCount);
          //3、真正处理结果集映射
          handleResultSet(rsw, resultMap, multipleResults, null);
          rsw = getNextResultSet(stmt);
          cleanUpAfterHandlingResultSet();
          resultSetCount++;
        }

        //4、从mappedStatement中拿到ResultSet,也就是我们在mapper.xml总定义的<resultSets/>结果集映射器
        String[] resultSets = mappedStatement.getResultSets();
        if (resultSets != null) {
          while (rsw != null && resultSetCount < resultSets.length) {
            ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
            if (parentMapping != null) {
              String nestedResultMapId = parentMapping.getNestedResultMapId();
              ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
              handleResultSet(rsw, resultMap, null, parentMapping);
            }
            rsw = getNextResultSet(stmt);
            cleanUpAfterHandlingResultSet();
            resultSetCount++;
          }
        }

        return collapseSingleResultList(multipleResults);
 }

核心流程:

  • 1、mappedStatement.getResultMaps(); //获取ResultMap

  • 2、handleResultSet(rsw, resultMap, multipleResults, null); //根据规则(resultMap)处理 ResultSet,将结果集转换为Object列表

  • 3、return collapseSingleResultList(multipleResults); //返回结果集


第十五步:DefaultResultSetHandler#handleResultSet方法

  // 根据resultMap或ResultSet结果集,将结果集转换为Object列表,并保存到multipleResults
 private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
        try {
            if (parentMapping != null) {
              //1、非存储过程的情况,parentMapping为null。handleResultSets方法的第一个while循环传参就是null
              handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
            } else {
              
              //1.2、如果没有自定义的resultHandler,则使用默认的DefaultResultHandler对象
              if (resultHandler == null) {
                  //1.3、创建DefaultResultHandler对象
                  DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);

                  //1.4、(核心方法)处理resultMap或ResultSet返回的每一行Row,里面会循环处理全部的结果集
                  handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);

                  //1.5、将defaultResultHandler的处理结果,添加到multipleResults中
                  multipleResults.add(defaultResultHandler.getResultList());
              } else {
                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
              }
      }
 }

核心流程:

  • 1、handleRowValues //主要处理结果集

  • 2、multipleResults.add //添加结果集到对应集合


第十六步:DefaultResultSetHandler#handleRowValues

    public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
        
        //1、处理嵌套映射的情况
        if (resultMap.hasNestedResultMaps()) {

          //1.1、校验RowBounds
          ensureNoRowBounds();

          //1.2、校验自定义resultHandler
          checkResultHandler();

          //1.3、处理嵌套映射的结果集
          handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);

        } else {
          //2、简单映射的情况
          handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
        }
    }

核心流程:

  • 1.handleRowValuesForNestedResultMap //处理嵌套映射

  • 2.handleRowValuesForSimpleResultMap //处理简单映射


第十七步:DefaultResultSetHandler#handleRowValuesForSimpleResultMap

  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)throws SQLException {

        //1.创建DefaultResultContext
        DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
        
        //2.获得ResultSet对象,并跳到 rowBounds 指定的开始位置
        skipRows(rsw.getResultSet(), rowBounds);
        
        //3.循环处理结果集(shouldProcessMoreRows校验context是否已经关闭和是否达到limit,rsw获取下一条记录)
        while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {

            //4.根据该行记录和ResultMap.discriminator ,决定映射使用的 ResultMap 对象
            ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);

            //5.根据确定的ResultMap将ResultSet中的该行记录映射为Java对象(处理一行)
            Object rowValue = getRowValue(rsw, discriminatedResultMap);

            //6.将映射得到的Java对象添加到ResultHandler.resultList中
            storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
        }
  }

核心流程:

  • 1、skipRows:定位到正确位置

  • 2、shouldProcessMoreRows:校验,并判断是否超过limit限制

  • 3、resolveDiscriminatedResultMap:处理鉴别器信息

  • 4、getRowValue:将一行记录转换为一个Java对象

  • 5、storeObject:将转化得到的Java对象保存


第十八步:DefaultResultSetHandler#getRowValue()

  // 根据确定的ResultMap将ResultSet中的记录映射为Java对象(处理一行)
  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {

        //创建ResultLoaderMap,和懒加载相关
        final ResultLoaderMap lazyLoader = new ResultLoaderMap();

        //1.创建结果对象,但还没赋值
        Object resultObject = createResultObject(rsw, resultMap, lazyLoader, null);

        //2.如果hasTypeHandlerForResultObject(rsw, resultMap.getType())返回 true ,意味着rowValue是基本类型,无需执行下列逻辑。
        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {

            //3.根据结果对象类型的不同,创建不同的MetaObject对象,用于访问rowValue对象
            final MetaObject metaObject = configuration.newMetaObject(resultObject);

            //4.foundValues代表,是否成功映射任一属性。若成功,则为true ,若失败,则为false
            boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty();

            //5.判断是否开启自动映射功能
            if (shouldApplyAutomaticMappings(resultMap, false)) {

              //6.自动映射未明确的列,将结果映射到java对象中
              foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
            }
            //7.映射ResultMap中明确映射的列,至此,ResultSet的该行记录的数据已经完全映射到结果对象resultObject的对应属性中
            foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;

            foundValues = lazyLoader.size() > 0 || foundValues;

            //8.如果映射属性失败,则置空 resultObject 对象。
            resultObject = foundValues ? resultObject : null;
            return resultObject;
        }
        return resultObject;
 }

核心流程:

  • 1、createResultObject:根据映射规则(resultMap的type信息)创建Java对象

  • 2、shouldApplyAutomaticMappings->applyAutomaticMappings: 如果使用了自动映射,那么就会处理未明确映射关系的属性

  • 3、applyPropertyMappings: 处理映射关系明确的属性

  • 4、映射失败则会把结果置为null


第十九步: DefaultResultSetHandler#applyAutomaticMappings

  // 自动映射未明确的列
  private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {

          //1.获得UnMappedColumnAutoMapping数组
        List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);

        boolean foundValues = false;
        if (autoMapping.size() > 0) {

          //2.遍历UnMappedColumnAutoMapping数组
          for (UnMappedColumnAutoMapping mapping : autoMapping) {

              //3.获得指定字段的值
            final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);

            //4.若非空,标记foundValues有值
            if (value != null) {
              foundValues = true;
            }
            //5.将映射列字段,保存metaObject对象中
            if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
              metaObject.setValue(mapping.property, value);
            }
          }
        }
        return foundValues;
  }
final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);


调用BaseTypeHandler.getResult方法

  public T getResult(ResultSet rs, String columnName) throws SQLException {
        try {
          return getNullableResult(rs, columnName);
        } catch (Exception e) {
          throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e, e);
        }
 }

根据结果集的参数类型,调用不同XXTypeHandler.getNullableResult方法,例如:IntegerTypeHandler.getNullableResult方法

  public Integer getNullableResult(ResultSet rs, String columnName) throws SQLException {
      int result = rs.getInt(columnName);
      return result == 0 && rs.wasNull() ? null : result;
  }

核心流程:

  • 1、首先会在一个集合里面讲未明确映射的列保存起来,先将他获取出来。

  • 2、然后从数据库记录中,根据列的名称获取到列对应的Value

  • 3、通过反射模块提供的metaObject设值到parameterObject中。注意metaObject是Mybatis反射模块以及封装好的相关类,给属性设值提供简单的使用方式


总结:第十四步--->第十九步代码解析

会先从mappedStatement中拿到ResultMap,也就是我们在mapper.xml总定义的结果集映射器,然后调用DefaultResultHandler#handleResultSet方法结果集处理


DefaultResultHandler方法中会拿到ResultMap说指定的type也就是 使用 ObjectFactory 对象工厂利用反射通过无参构造器创建对应的实体类实例但还没进行赋值。


然后会拿到根据ResultMap结果映射的每一个column , 再根据column数据类型,使用对应的TypeHandler从结果集中取出对应的值,并赋值给对象的实例。到此结果集映射成对象完毕并保持到DefaultResultSetHandler的multipleResults对象中。最后返回结果。最后关闭Statement


---------------------- 第四阶段总结一下Mybatis的执行流程 ----------------------------------------------------

  • 当我们执行SqlSession的select方法时,会从Configuration中拿到MappedStatement(包括SQL,参数映射,结果集映射等)。然后调用executor去执行query方法

  • 接着代码来到executor执行器,它会先创建CacheKey缓存的key,缓存的key是 statementId ; 分页 ;SQL,参数值 一起组成的,也就是说只要是同一SQL,分页条件也相同,参数也相同的话,就可以命中缓存。

  • 如果有开启二级缓存的话,会尝试执行二级缓存,有就返回,没有就从数据库查询,然后再把结果添加到二级缓存。二级缓存在TransactionalCacheManager中管理起来的。

  • 假设没有二级缓存,尝试从数据库查询此刻会尝试从一级缓存查询数据,有就返回,没有就从数据库查询。一级缓存在SqlSession中的BaseExecutor.PerpetualCache中,所以是先执行二级缓存再执行一级缓存。

  • 如果还是没有命中缓存,就会通过Connection创建一个PrepareStatement,然后调用StatementHandler#query去执行PrepareStatement。

  • 紧接着StatementHandler方法底层会调用PrepareStatement#execute查询结果,然后调用ResultSetHandler处理结果。

  • ResultSetHandler会先拿到结果集,然后找到配置的ResultMap。根据ResultMap中配置的type也就是实体类的权限定名,使用ObjectFactory对象工厂使用反射创建对象实例。

  • 再接着就会拿到ResultMap总映射的column列,根据column类型找到对应的TypeHanlder拿到值,使用反射赋值给对象实例。最后返回对象列表

posted @ 2024-08-06 23:44  jock_javaEE  阅读(112)  评论(0)    收藏  举报