MyBatis 源码解析(三)MyBatis如何解析配置 ?(三)

上一篇博客中我们分析到了加载用户的别名,今天继续往下分析。

XMLConfiguration###parseConfiguration()

//调用各个方法进行解析成Configuration对象 
private void parseConfiguration(XNode root) {
    try {
      //读取用户自定义属性
      propertiesElement(root.evalNode("properties"));
      //读取用户的设置
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //加载用户自定义的VFS实现  
      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);
      //加载用户定义的环境变量
      environmentsElement(root.evalNode("environments"));
      //读取databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //处理类型处理器  
      typeHandlerElement(root.evalNode("typeHandlers"));、
      //处理mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

XMLConfigBuilder###pluginElement()


// <plugins> // <plugin interceptor="com.plugins.ExamplePlugin"> // <property name="someProperty" value="100"/> // </plugin> //</plugins> private void pluginElement(XNode parent) throws Exception { if (parent != null) { //找到所有父节点为`plugins`的子节点 for (XNode child : parent.getChildren()) { //获取其`interceptor`节点 String interceptor = child.getStringAttribute("interceptor"); //获取属性 Properties properties = child.getChildrenAsProperties(); //通过反射加载对应的类,然后通过`newInstance`产生对象 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); //给这个对象注入属性 interceptorInstance.setProperties(properties); //将对象放入`configuration`中 configuration.addInterceptor(interceptorInstance); } } }

这里可以看到,逻辑还是很简单的,先加载配置的类,然后设置属性即可

这里的插件,其实就是MyBatis为了实现可插拔的其他功能,分别在:

  • Executor
  • ParameterHandler
  • ResultSetHandler
  • StatementHandler

进行一些指定操作之前/之后所执行的一些定制化操作,具体的实现方式便是责任链模式,因此这里只用简单的将其add即可,对于真正的使用后续我们会有详细分析。

XMLConfigBuilder###objectFactoryElement()

// <objectFactory type="cn.com.mybatis.test.CartObjectFactory">
//    <property name="someProperty" value="100"/>
// </objectFactory>
 private void objectFactoryElement(XNode context) throws Exception {
    if (context != null) {
      //获取type元素对应的值  
      String type = context.getStringAttribute("type");
      //获取properties  
      Properties properties = context.getChildrenAsProperties();
      //通过反射新建对象 
      ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      //设置属性 
     factory.setProperties(properties);
     //将对象赋值到configuration里面
     configuration.setObjectFactory(factory);
    }
}

这里看到,初始化的方法和plugins简直一模一样。那这个objectFactory又是干嘛的呢?

我们知道,Mybatis是一个半ORM框架,那这里面就必然会涉及到新建对象,而MyBatis的配置都是通过XML配置,那么就必须通过反射来新建对象,然后某些时候我们可以希望在新建对象的时候加入一些其他操作,这个时候就可以通过修改ObjectFactory来实现,因此可以知道ObjectFactory是用来产生对象的。

这里可以更加深入的看看ObjectFactory

protected ObjectFactory objectFactory = new DefaultObjectFactory();

这是Configuration对象的初始化语句,可以看到如果我们不进行配置,则默认使用的是DefaultObjectFactory

DefaultObjectFactory实现了ObjectFactory接口,接口就4个方法:

  //设置属性
  default void setProperties(Properties properties) {
    // NOP
  }

  //创建对象
  <T> T create(Class<T> type);

  //创建带参数的对象
  <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);

  //是否是集合类型
  <T> boolean isCollection(Class<T> type);

可以验证我们的猜想,ObjectFactory是用来反射实现对象的


这里跳过objectWrapperFactoryElement的分析,估计是MyBatis后续的版本的功能,

MyBatis官方文档中并没有提及到它,并且MyBatis中的源码实现也就一句话,调用这个方法就抛出异常。。。

@Override
public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
    throw new ReflectionException("The DefaultObjectWrapperFactory should never be called to provide an ObjectWrapper.");
}

XMLConfigBuilder###reflectorFactoryElement()

配置反射工厂的的代码和上面的一样,这里就不贴出来了。

这里主要说说反射工厂的功能:

反射工作的作用主要是用来做缓存使用,前面说过,MyBatis会将用户的Bean通过反射产生一个ObjectWrapper类,这个类包含了Bean的所有信息以及方法,比如hashSettersetBeanProperty()等功能,但是如果已经产生了一个这样的类的信息,就可以直接拿去就行,因此ReflectorFactory主要作用就是一个简单工厂,有缓存则取缓存,没缓存则新建

XMLConfigBuilder###settingsElement()

private void settingsElement(Properties props) {
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
     //.....
  }

这里代码比较多,主要就是通过setter设置各个属性,这些属性的类型基本都是boolean

XMLConfigBuilder###environmentsElement()

// <environments default="development">
//        <environment id="development">
//            <transactionManager type="JDBC">
//                <property name="" value=""/>
//            </transactionManager>
//            <dataSource type="POOLED">
//                <property name="driver" value="com.mysql.jdbc.Driver"/>
//                <property name="username" value="xxx"/>
//                <property name="password" value="xxx"/>
//            </dataSource>
//        </environment>
//</environments> 
private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
          //如果没有通过参数指定`environment`则使用默认的环境  
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      //读取子节点
      for (XNode child : context.getChildren()) {
          //获取id
          String id = child.getStringAttribute("id");
           //判断id是不是空,如果是空则抛出带有详细信息的异常
          if (isSpecifiedEnvironment(id)) {
          //初始化事务管理器工厂    
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          //初始化数据库管理工厂
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          //创建环境对象    
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          //设置环境属性    
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

这里有一点比较奇怪的是,如果没有配置环境信息按道理来说应该报错的,因为对于数据库没有账号密码信息,MyBatis几乎是不能使用的,因此这里应该有一个检查机制才对,但是这里貌似没有

XMLConfigBuilder###databaseIdProviderElement()

//<databaseIdProvider type="DB_VENDOR">
//  <property name="DB2" value="db2" />
//  <property name="Oracle" value="oracle" />
//  <property name="Adaptive Server Enterprise" value="sybase" />
//  <property name="MySQL" value="mysql" />
//</databaseIdProvider>
private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
      //获取类型信息  
      String type = context.getStringAttribute("type");
      //和老版本兼容
      if ("VENDOR".equals(type)) {
        type = "DB_VENDOR";
      }
      //获取属性
      Properties properties = context.getChildrenAsProperties();
      //新建对象以及设置属性 
      databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
      databaseIdProvider.setProperties(properties);
    }
    //通过DataSource获取当前环境的databaseId
    //调用方法为:DatabaseMetaData.getDatabaseProductName()
    //返回结果为:Oracle (DataDirect)
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
      String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
      //设置databaseId  
      configuration.setDatabaseId(databaseId);
    }
  }

这里要说两点:

  • 第一个是可以看到这里的通过反射新建对象都是通过resolveClass()实现的,而resolveClass()都是先通过TypeAliasHandler查找了对象,因此所有的type字段都可以通过别名指定

  • 第二个是databaseIdProvider的主要作用是获取数据库的名称,主要代码为:

    private String getDatabaseProductName(DataSource dataSource) throws SQLException {
      Connection con = null;
      try {
        con = dataSource.getConnection();
        DatabaseMetaData metaData = con.getMetaData();
        return metaData.getDatabaseProductName();
       //.... 
      }
    }
    

    便是获取数据库的名字,以及为名字设置简写。


本章暂时到这里,下一节分析MyBatis配置中比较关键的TypeHandler以及Mapper配置的代码。

这里简单总结下:

  • MyBatis代码中学排版,在看MyBatis代码的时候,就觉得非常舒服,之前在看《代码整洁之道》这本书的时候,里面提到代码排版应该和报纸一样,从上到下,从左往右像看文章一样顺序看下来,而MyBatis的排版便是这样,当一个方法调用另外一个方法的时候,一般被调用这个方法的定义就应该放在这个方法的后面,让人一看就能明白,就像看一篇小说一样。以后开发应该注意
  • MyBatis的模块划分的非常细,真正的在奉行一个类只做一件事,这样对于大型项目来说,后期维护非常方便,应该学习