mybatis如何解析xml和加载Configuration

一、加载xml的入口

mybatis配置xml路径的类,一般都在SqlSessionFactoryBean。怎么加载它呢?有两种方式:

  1. xml形式:

        
            
                
            
        
    
  1. @Configuration注解形式
    @Bean(name = "xxxSqlSessionFactory")
    public SqlSessionFactory xxxSqlSessionFactory(@Qualifier("xxxDataSource")  DataSource dataSource){
        SqlSessionFactory sqlSessionFactory = null;
        try {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean() ;
            sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath*:mapper/crt/*.xml"));
            sqlSessionFactoryBean.setDataSource(dataSource);
            sqlSessionFactory = sqlSessionFactoryBean.getObject();
            sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(),e);
            throw new CouponBizException(CouponBizCodeEnum.SYSTEM_ERROR) ;
        }

        return sqlSessionFactory ;
    }

不管哪种方式,最终都会执行到sqlSessionFactoryBean.getObject()方法,得到一个SqlSessionFactory接口的实现类,具体是哪个实现类,下面会讲到。好了,入口知道了,我们就开始继续深入吧~

二、加载Configuration

应用在启动的时候,Spring容器会触发sqlSessionFactoryBean.getObject()方法得到单例对象,以便放到容器中。看下getObject方法:

 @Override
  public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      afterPropertiesSet();
    }
    return this.sqlSessionFactory;
  }

第一次必然会调用afterPropertiesSet()。继续深入看下源码:

@Override
  public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
              "Property 'configuration' and 'configLocation' can not specified with together");

    this.sqlSessionFactory = buildSqlSessionFactory();
  }

sqlSessionFactory接口的实现类,就在buildSqlSessionFactory()里面。继续深入:

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    Configuration configuration;

    //说明了mapper接口的配置,默认用xml来实现
    XMLConfigBuilder xmlConfigBuilder = null;
   //上面的xml配置,我们已经传入configuration了,这里必然会进来
    if (this.configuration != null) {
      configuration = this.configuration;
    //property字段,我们并没有配置多余其他字段,所以这里都不会进来
      if (configuration.getVariables() == null) {
        configuration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        configuration.getVariables().putAll(this.configurationProperties);
      }
    } else if (this.configLocation != null) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }

    //xml的配置未配置该标签,无需进来
    if (this.objectFactory != null) {
      configuration.setObjectFactory(this.objectFactory);
    }

 //xml的配置未配置该标签,无需进来
    if (this.objectWrapperFactory != null) {
      configuration.setObjectWrapperFactory(this.objectWrapperFactory);
    }

 //xml的配置未配置该标签,无需进来
    if (this.vfs != null) {
      configuration.setVfsImpl(this.vfs);
    }

 //xml的配置未配置该标签,无需进来
    if (hasLength(this.typeAliasesPackage)) {
      String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeAliasPackageArray) {
        configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
        }
      }
    }

 //xml的配置未配置该标签,无需进来
    if (!isEmpty(this.typeAliases)) {
      for (Class typeAlias : this.typeAliases) {
        configuration.getTypeAliasRegistry().registerAlias(typeAlias);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered type alias: '" + typeAlias + "'");
        }
      }
    }

 //xml的配置未配置该标签,无需进来
    if (!isEmpty(this.plugins)) {
      for (Interceptor plugin : this.plugins) {
        configuration.addInterceptor(plugin);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered plugin: '" + plugin + "'");
        }
      }
    }

 //xml的配置未配置该标签,无需进来
    if (hasLength(this.typeHandlersPackage)) {
      String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeHandlersPackageArray) {
        configuration.getTypeHandlerRegistry().register(packageToScan);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
        }
      }
    }

 //xml的配置未配置该标签,无需进来
    if (!isEmpty(this.typeHandlers)) {
      for (TypeHandler typeHandler : this.typeHandlers) {
        configuration.getTypeHandlerRegistry().register(typeHandler);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered type handler: '" + typeHandler + "'");
        }
      }
    }

 //xml的配置未配置该标签,无需进来
    if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
      try {
        configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
      } catch (SQLException e) {
        throw new NestedIOException("Failed getting a databaseId", e);
      }
    }

 //xml的配置未配置该标签,无需进来
    if (this.cache != null) {
      configuration.addCache(this.cache);
    }

//xmlConfigBuilder尚未初始化,无需进来
    if (xmlConfigBuilder != null) {
      try {
        xmlConfigBuilder.parse();

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
        }
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }

     //xml的配置未配置该标签,所以必为null,需要进来
    if (this.transactionFactory == null) {
      //默认的transactionFactory是SpringManagedTransactionFactory
      this.transactionFactory = new SpringManagedTransactionFactory();
    }
    //创建Environment,其实就是为了封装dataSource和transactionFactory
    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

    //xml的配置了mapperLocations,所以不为null,需要进来
    if (!isEmpty(this.mapperLocations)) {
      //遍历每个mapper文件
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
        //为每个xml文件,初始化一个xmlMapperBuilder。它含有configuration,目的就是容纳下面parse xml得到的结果
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
        //开始解析xml文件,并把解析出来的所有标签,放到configuration对应的字段上
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }

    return this.sqlSessionFactoryBuilder.build(configuration);
  }

有点长。。。
看完里面的注释,也就都明白了。加载configuration的核心就在xmlMapperBuilder.parse()方法里面。

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

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

然后进入configurationElement方法:

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      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);
    }
  }

可以看到先解析 .... 标签里面的东东,然后递归继续解析mapper里面的....,以及 ....。我们来看一下,mapper的xml标签格式好了。





    
        
        
        
             
      ....
    

    
        id, uid....
    

    
        insert into xxx values()
    

    

    
          update xxxx
          set status= #{targetStatus, jdbcType=NUMERIC}
          where id = #{id} and status = #{sourceStatus}
    

可以看到解析的入口就在标签,而它恰好就是mapper xml文件的格式。然后依次按照规范解析其余标签,以及标签里面的属性信息,放到Configuration对应的字段。
下面我们以解析标签上面的属性,很多属性是不是很陌生。但是最常见的几个属性,我们一定知道。比如:id, parameterMap, resultMap。其实这些属性在mybatis的dtd描述文件里面就有的,不信我们点击xml的标签所有属性,包括:真正的sql语句。但是我们的sql语句是动态的,也就是说有条件的,只有在真正执行的才能确定sql语句。那么xml的静态的sql语句是怎么保存的呢?答案就在sqlSource字段。debug查看,如下:


image.png

内容被切割为不同类型的对象了,比如:StaticTextSqlNode对象存放固定sql语句,如where前面的语句,它肯定是固定不变的,无需动态生成。而IfSqlNode它有test字段String类型,又有contents字段,而contents又是一个StaticTextSqlNode属于静态不变的语句。
可以看出mybatis的动态sql,会在初始化的时候生成sqlSource这种模板,后面再运行的时候,会根据sql请求参数,匹配这个sqlSource,最终生成要执行的sql。

MappedStatement生成完成后,执行configuration.addMappedStatement(statement),加入到configuration。

总结:到这里我们就知道了xml的配置根据标签,一个一个去解析。解析完成后,最终会生成MappedStatement对象,然后把它添加到configuration。整个解析完成后,configuration是不是就拥有了所有xml的配置信息了,包括:sql语句,以及sql的返回值字段到对象的映射关系。这些sql执行和sql结果映射需要的东西,全部都在configuration里面了。

下一节准备分析:

  1. Mybatis中Sql解析执行的原理是什么?
  2. Mybatis中Executor接口有几种实现方式

未完待续。。。

三、Mybatis中Sql解析执行的原理是什么?

3.1 先来介绍sql 解析的原理

可以看下面的这篇文章:
Mybatis解析动态sql原理分析

其实上面讲的更加透彻,从sqlNode的多个实现类来解释。mybatis根据不同标签,把sql语句切分不同部分,然后对各个部分分别处理。如果是静态文本,就用StaticTextSqlNode,如果是if标签用IfSqlNode,其他类推。就像上一节截图那样,最终sql语句会被解析问sqlSource,传递给configuration。

3.2 再来介绍sql 执行的原理

当然可以先看这个文章:mybatis调用过程
上面的文章是从mapper整个调用过程来切入的,这节我们只介绍sql具体执行,是其中的一小块内容。首先我们从下面的源码切入:

@Override
  public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

第一步肯定就是生成动态sql了,如下:

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

    // check for nested result maps in parameter mappings (issue #30)
    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;
  }

继续跟进getBoundSql方法:

@Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

rootSqlNode.apply(context)一般情况是MixedSqlNode,进入该类:

@Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }

会根据sql语句不同部分,分别调用apply。apply方法完成后,最终会成为一个如下的sql语句:

select count(1) from xxx e left join yyy on e.order_no = rff.biz_no
        where e.sob_id = #{xxx} and e.deal_date >= #{yyy} and e.deal_date <= #{dealDateEnd} and e.active=1 

但是参数值并没有写进去,说明还不是最终要执行的sql。然后由sqlSourceParse.parse方法,会把所有'#{‘开头的匹配出来,变成'?',得到如下的sql:

select count(1) from xxx e left join yyyy rff on e.order_no = rff.biz_no
        where e.sob_id = ? and e.deal_date >= ? and e.deal_date <= ? and e.active=1  
            and e.staff_id = ?

这个sql还是没有参数,还不是最终执行的sql。但是它得到sqlSource变成了StaticSqlSource类型了,因为动态sql其实已经构造完成,它就变成了静态的了,只需要绑定参数即可。

 public  List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

到这里传递进来的BoundSql都是带有"?"的sql语句,看下prepareStatement方法:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

参数的绑定就发生在 handler.parameterize(stmt)里面,它会匹配参数里面的key和查询条件的key,然后把对应的value构造到sql中。
就不深入进去了,太多了。。。

至于sql的执行,mybatis解析完成动态sql后,它的职责就完成了。剩下的就交给dataSource的Connection去真正执行网络请求,通过JDBC组件,构建mysql应用能够识别的应用层协议报文,发送给服务器。然后服务器查询得到结果后,返回给mybatis。

JDBC是如何初始化连接的,如何握手验证密码的,以及如何调用的,并不在本文讨论范围。
当然JDBC里面有一堆控制,比如超时,重试,事务等等控制,并不仅仅只有网络传输。

四、Mybatis如果进行ORM转换,把数据库返回的column-value对象,通过映射转换为我们POJO呢?

我们如下的代码:

@Override
  public  List query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler. handleResultSets(ps);
  }

ps.execute(),这里应该是阻塞式调用。函数返回,表示ps已经收到结果了。如果对JDBC是真正如何执行sql的,强烈建议跟进去execute方法,看看mysql厂商的驱动是怎么构造mysql应用层协议的。然后协议的响应,你在客户端肯定是看不着的,你得去研究mysql的源码才能知道。
我们这里就不展开JDBC了。剩下的我们就开始处理返回的结果,mybatis如何映射为POJO?
很明显转换就在handleResultSets方法。
最后给调用handleRowValues方法:

 public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

最终到typeHandler.getResult(rsw.getResultSet(), columnName)方法:

typeHandler.getResult(rsw.getResultSet(), columnName)

typeHandler有很多实现类,比如:IntegerTypeHandler,ObjectTypeHandler等等,它会那映射关系,一个一个取出对应列的值。

END

你可能感兴趣的:(mybatis如何解析xml和加载Configuration)