Mybatis源码学习(2)--XMLConfigBuilder

一、解析配置文件

上一篇文章说到,SqlSessionFactory的配置都是委托给XMLConfigBuilder的parse方法完成的,本篇就来看看解析工程都做了哪些事情。

  //解析配置
  public Configuration parse() {
    //如果已经解析过了,报错
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
     //根节点是configuration
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

首先检查下是否已经解析过了,如果直接抛异常,在这里贴一份常见的mybatis配置文件,便于理解:

<?xml version="1.0" encoding="UTF-8" ?> 
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-config.dtd"> 
<configuration> 
        <environments default="development"> 
            <environment id="development"> 
                <transactionManager type="JDBC"/> 
                <dataSource type="POOLED"> 
                    <property name="driver" value="${driver}"/> 
                    <property name="url" value="${url}"/> 
                    <property name="username" value="${username}"/> 
                    <property name="password" value="${password}"/> 
                </dataSource> 
            </environment> 
        </environments>
        <mappers> 
            <mapper resource="org/mybatis/example/BlogMapper.xml"/> 
        </mappers> 
</configuration>

Mybatis解析xml用的是自己封装的一个名为XPathParser的工具类(使用的全是JDK的类包),至于怎么具体解析的,这里不做展开。如果是第一次解析,则从“/configuation”节点开始解析,代码如下:

private void parseConfiguration(XNode root) {
  try {
    //分步骤解析
    //1.properties
    propertiesElement(root.evalNode("properties"));
    //2.类型别名
    typeAliasesElement(root.evalNode("typeAliases"));
    //3.插件
    pluginElement(root.evalNode("plugins"));
    //4.对象工厂
    objectFactoryElement(root.evalNode("objectFactory"));
    //5.对象包装工厂
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    //6.设置
    settingsElement(root.evalNode("settings"));
    // read it after objectFactory and objectWrapperFactory issue #631
    //7.环境
    environmentsElement(root.evalNode("environments"));
    //8.databaseIdProvider
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    //9.类型处理器
    typeHandlerElement(root.evalNode("typeHandlers"));
    //10.映射器
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

二、十个解析项

上边的代码就是解析配置的核心,总共10个步骤,我们一一来看看:

2.1、properties元素

在properties下配置如下属性:

<properties resource="org/mybatis/example/config.properties">
    <property name="username" value="dev_user"/>
    <property name="password" value="F2Fa3!33TYyg"/>
</properties>

可以在该配置文件上全局使用,用于替换其它配置的变量,比如:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

另外,配置的Properties还可以传递到SqlSessionFactoryBuilder.build() 方法中使用,如下

SqlSessionFactory factory =
  sqlSessionFactoryBuilder.build(reader, props);

// ... or ...

SqlSessionFactory factory =
  new SqlSessionFactoryBuilder.build(reader, environment, props);

如果相同的配置项存在不同的位置,加载是怎样的?我们从代码里一看究竟,代码如下:

if (context != null) {
  //properties元素下的属性会被首先加载
  Properties defaults = context.getChildrenAsProperties();
  //第二顺序被加载的是类路径下的resource或者url,如果与之前指定的属性重复,会覆盖掉之前加载的属性
  String resource = context.getStringAttribute("resource");
  String url = context.getStringAttribute("url");
  if (resource != null && url != null) {
    throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
  }
  if (resource != null) {
    defaults.putAll(Resources.getResourceAsProperties(resource));
  } else if (url != null) {
    defaults.putAll(Resources.getUrlAsProperties(url));
  }
  //通过方法参数被传递进来的最后被加载,它会覆盖掉前面两步所有与之重复的属性
  Properties vars = configuration.getVariables();
  if (vars != null) {
    defaults.putAll(vars);
  }
  parser.setVariables(defaults);
  configuration.setVariables(defaults);
}

看代码逻辑,属性加载的顺序如下:

  1. properties元素下的属性会被首先加载
  2. 第二顺序被加载的是类路径下的resource或者url,如果与之前指定的属性重复,会覆盖掉之前加载的属性。
  3. 通过方法参数被传递进来的最后被加载,它会覆盖掉前面两步所有与之重复的属性。

2.2、typeAliases元素

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>

别名是为了简化java类型的实体全路径名过长的问题,如上,通过配置,Blog可以在任何地方代替domain.blog.Blog使用.

同理,也可以给J整个包起一个别名,方便使用,如果没有特殊说明使用全小写的类名作为别名:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

另外,也支持通过注解的形式指定别名

@Alias("author")
public class Author {
    ...
}

看一看解析别名的源码

private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        //如果是package
        String typeAliasPackage = child.getStringAttribute("name");
        //(一)调用TypeAliasRegistry.registerAliases,去包下找所有类,然后注册别名(有@Alias注解则用,没有则取类的simpleName)
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } else {
        //如果是typeAlias
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        try {
          Class<?> clazz = Resources.classForName(type);
          //根据Class名字来注册类型别名
          //(二)调用TypeAliasRegistry.registerAlias
          if (alias == null) {
            //alias可以省略
            typeAliasRegistry.registerAlias(clazz);
          } else {
            typeAliasRegistry.registerAlias(alias, clazz);
          }
        } catch (ClassNotFoundException e) {
                    ...
        }
      }
    }
  }
}

可以看出在解析别名过程中会从上至下遍历所有别名,登记到别名的map中。

如果我在配置别名时写成:

<typeAliases>
    <typeAlias alias="goods_alias" type="org.apache.ibatis.wyjdemo.MbGoods"/>
    <package name="org.apache.ibatis.wyjdemo"/>
</typeAliases>

即单独指定和通过包名同时配置,注册时会变成怎么样呢?打断点看下

image-20200709235357972.png

可以看到两种方式都生效了,两个别名指向了一个bean.

2.3、pluginElement元素

Mybatis支持拓展插件实现丰富的功能,它允许你在映射语句执行过程汇中的某一点进行拦截调用,通常来说,Mybatis允许插件在以下几个方法调用拦截:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类方法的详细介绍可以在发行的源码中方法签名查看更多细节。想要编写插件也非常简单,只需要实现Interceptor接口,并指定要拦截的方法签名即可。

我们自己实现一个非常简单的插件来感受下,插件的作用,首先要实现Interceptor接口,

@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class }
)})
public class MySimplePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 执行目标方法的前置处理
        Object[] args = invocation.getArgs();
        for (Object arg : args) {
            System.out.println(arg);
        }
        Object returnObject = invocation.proceed();
        // 执行目标方法的后置处理
        return returnObject;
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

签名这里我配置的是Excutor,并且拦截query方法多一些文章,实现比较简单就是打印出查询参数,然后再配置文件中加上,插件相关配置:

<!-- mybatis-config.xml -->
<plugins>
    <plugin interceptor="org.apache.ibatis.wyjdemo.MySimplePlugin">
    </plugin>
</plugins>

执行输出如下

org.apache.ibatis.mapping.MappedStatement@38082d64
{id=12, param1=12}
org.apache.ibatis.session.RowBounds@dfd3711
null

这个四个值依次对应,@Signature注解中args属性配置的值。

说回正题,我们看下源码中是如何解析插件的配置的

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      interceptorInstance.setProperties(properties);
      //调用InterceptorChain.addInterceptor
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

源码很简单,总体来说就是从配置文件中解拿到插件配置的拦截器名称,然后实例化一个拦截器对象放到拦截器链中等待处理。核心就是调用resovleClass方法根据配置的拦截名获取一个该拦截的实例化对象。它又会调用resolveAlias实际去处理,resolveAlias源码如下:

public <T> Class<T> resolveAlias(String string) {
  try {
    if (string == null) {
      return null;
    }
    String key = string.toLowerCase(Locale.ENGLISH);
    Class<T> value;
    //原理就很简单了,从HashMap里找对应的键值,找到则返回类型别名对应的Class
    if (TYPE_ALIASES.containsKey(key)) {
      value = (Class<T>) TYPE_ALIASES.get(key);
    } else {
      //找不到,再试着将String直接转成Class(这样怪不得我们也可以直接用java.lang.Integer的方式定义,也可以就int这么定义)
      value = (Class<T>) Resources.classForName(string);
    }
    return value;
  } catch (ClassNotFoundException e) {
    throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
  }
}

内容就是先从上一步维护的别名映射map中找寻是否已经拥有该实例(也就是拦截器中的配置也是支持别名的),没有找到的话用反射得到一个实例返回。

2.4、objectFactoryElement元素

对象工厂主要的作用就是生产封装结果集所需要的对象,Mybatis默认的对象工厂,不会对结果集做特殊的处理,仅是实例化一个目标对象,如果需要对结果对象进行定制化的处理,则要继承DefaultObjectFactory,重写父类方法,源码也非常简单,如下

private void objectFactoryElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type");
    Properties properties = context.getChildrenAsProperties();
    ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
    factory.setProperties(properties);
    configuration.setObjectFactory(factory);
  }
}

2.5、objectFactoryElement元素

对象包装工程,官网文档上没有介绍,我自己也没有搞明白,先略过。。。

2.6、settingsElement元素

顾名思义是对Mybatis进行设置,这会影响Mybatis的运行方式,可以理解为是Mybatis的一些不同工能的开关,源码非常简单:

private void settingsElement(XNode context) throws Exception {
    if (context != null) {
      Properties props = context.getChildrenAsProperties();
      // Check that all settings are known to the configuration class
      //检查下是否在Configuration类里都有相应的setter方法(没有拼写错误)
      MetaClass metaConfig = MetaClass.forClass(Configuration.class);
      for (Object key : props.keySet()) {
        if (!metaConfig.hasSetter(String.valueOf(key))) {
          throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
        }
      }
      
      //下面非常简单,一个个设置属性
      //如何自动映射列到字段/ 属性
      configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
      ...
    }
  }
  

源码中可以看到,设置前先检查了setting节点中配置的属性key值的有效性,如果不是Configuration.class中的属性则在启动时就抛出异常,之后就是逐个读配置设置到configuration中了。(另:这个过程中用到的Reflector类,可以学习下)

2.7、environmentsElement元素

Mybatis支持多个环境配置,这允许我们写的SQL可以映射到多个数据库,例如,我们的开发、测试、生产会有不同环境等,在很多场景下都会有不同环境配置的需求,通过这个配置可以满足。

但是要注意,尽管我们可以配置多个环境,但是每个环境只能选择一个sqlSeesionFactory实例。如果我们需要连接两个数据库,就需要创建两个sqlSessionFactory,以此类推。很容易记着:每个数据都需要一个sqlSessionFactroy与之对应

看下配置文件:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

源码部分:

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
//循环比较id是否就是指定的environment
      if (isSpecifiedEnvironment(id)) {
        //7.1事务管理器
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        //7.2数据源
        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());
      }
    }
  }
}

首先判断是否指定了环境,未指定就使用默认环境,然后遍历environment节点进行配置,由上边代码可以看到配置的主要内容:加载事务管理器、数据源分别到TransactionFactory和DataSourceFactory,然后赋值到configuration中。

2.8、databaseIdProviderElement元素

Mybaits还支持根据不同数据库厂商执行不同的SQL语句,其实不是很实用,很少生产项目会用不同厂商的数据库,这里也不多做介绍。

2.9、typeHandlerElement元素

类型处理器,Mybatis无论是在设置参数或者从结果集获取数据都会用到类型处理器,它主要的功能是

获取到的值转换成对应的Java类型数据。

<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

看配置文件就可以知道,这个元素的方式也是同时支持单个处理器配置以及扫描包下的处理器两种方式的,源码如下:

private void typeHandlerElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      //如果是package
      if ("package".equals(child.getName())) {
        String typeHandlerPackage = child.getStringAttribute("name");
        //(一)调用TypeHandlerRegistry.register,去包下找所有类
        typeHandlerRegistry.register(typeHandlerPackage);
      } else {
        //如果是typeHandler
        String javaTypeName = child.getStringAttribute("javaType");
        String jdbcTypeName = child.getStringAttribute("jdbcType");
        String handlerTypeName = child.getStringAttribute("handler");
        Class<?> javaTypeClass = resolveClass(javaTypeName);
        JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
        Class<?> typeHandlerClass = resolveClass(handlerTypeName);
        //(二)调用TypeHandlerRegistry.register(以下是3种不同的参数形式)
        if (javaTypeClass != null) {
          if (jdbcType == null) {
            typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
          } else {
            typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
          }
        } else {
          typeHandlerRegistry.register(typeHandlerClass);
        }
      }
    }
  }
}

循环遍历typeHandlers节点下元素,优先扫描包下处理器,不存在则去单配总获取。可自行实现定制化的typeHandler,参考 自定义typeHandler

2.10、mapperElement元素

既然上边几个步骤我们配置了这么多的元素,最后我们要配置SQL映射了,但是怎么找到配置的映射关系呢?

Mybatis提供四种方法,从配置文件可以看出

<!-- Using classpath relative resources -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

<!-- Using url fully qualified paths -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

<!-- Using mapper interface classes -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

<!-- Register all interfaces in a package as mappers -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

结合源码看:

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        //10.4自动扫描包下所有映射器
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          //10.1使用类路径
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          //映射器比较复杂,调用XMLMapperBuilder
          //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          //10.2使用绝对url路径
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          //映射器比较复杂,调用XMLMapperBuilder
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          //10.3使用java类名
          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. 首先看是否用扫描包的方式配置。
  2. 如果是非包扫描方式,则会从resource、url、classs三种方式中按照顺序加载一种方式。
  3. 上述方式都没有采用的话,则会抛出异常。

总结

至此,XMLConfigbuilder初始化的过程我们已经过了一遍,这些步骤其实就是一个读Mybatis配置文件的过程,为后续的运行进行环境配置以及参数设置,到这一步,Configuration已经被“填充”完毕。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352