ORM - MyBatis初始化基本过程详解


一、组装All-In-One对象Configuration过程

Mybatis初始化流程,其实就是组装重量级All-In- One对象Configuration的过程,主要分为系统环境参数初始化和Mapper映射初始化,其中Mapper映射初始化尤为重要。

系统环境参数初始化

    inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

Mapper映射初始化

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return new DefaultSqlSessionFactory(parser.parse());
    }

parser.parse()方法,已经返回了组装完毕的Configuration对象。

二、XMLConfigBuilder.parse()方法

流程进入XMLConfigBuilder.parse()方法。

    public Configuration parse() {
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }
    
    private void parseConfiguration(XNode root) {
        try {
            Properties settings = settingsAsPropertiess(root.evalNode("settings"));
            propertiesElement(root.evalNode("properties"));
            loadCustomVfs(settings);
            typeAliasesElement(root.evalNode("typeAliases"));
            pluginElement(root.evalNode("plugins"));
            objectFactoryElement(root.evalNode("objectFactory"));
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            reflectionFactoryElement(root.evalNode("reflectionFactory"));
            settingsElement(settings);
            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);
        }
    }

以上代码,对mybatis-config.xml配置文件内的元素,使用XPath进行逐一读取。

    <?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>
    	<properties resource="jdbc.properties">
    		<property name="username" value="root" />
    		<property name="password" value="123" />
    	</properties>
    	<settings>
    		<setting name="localCacheScope" value="STATEMENT"/>
    		<setting name="cacheEnabled" value="false" />
    		<setting name="lazyLoadingEnabled" value="true" />
    		<setting name="multipleResultSetsEnabled" value="true" />
    		<setting name="useColumnLabel" value="true" />
    		<setting name="useGeneratedKeys" value="false" />
    		<setting name="defaultExecutorType" value="REUSE" />
    		<setting name="defaultStatementTimeout" value="25000" />
    	</settings>
    	<typeAliases>
    		<typeAlias alias="Student" type="com.mybatis3.domain.Student" />
    		<typeAlias alias="Teacher" type="com.mybatis3.domain.Teacher" />
    	</typeAliases>
    	<typeHandlers>
    		<typeHandler handler="com.mybatis3.typehandlers.PhoneTypeHandler" />
    	</typeHandlers>
    	<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="com/mybatis3/mappers/StudentMapper.xml" />
    		<mapper resource="com/mybatis3/mappers/TeacherMapper.xml" />
    	</mappers>
    </configuration>

三、Configuration属性映射表

Xml文件元素和Configuration属性映射表:

  • <properties>元素:Properties variables
  • <settings>元素:Integer defaultStatementTimeout、Integer defaultFetchSize、ExecutorType defaultExecutorType……
  • <typeAliases>元素:TypeAliasRegistry typeAliasRegistry
  • <typeHandlers>元素:TypeHandlerRegistry typeHandlerRegistry
  • <environments>元素:Environment environment。配置多个<environment>元素时,Mybatis只会读取默认的那一个。
  • <mappers>元素:MapperRegistry mapperRegistry

Mapper映射初始化是我们关注的重点,即mapperElement(root.evalNode("mappers"))方法。

org.apache.ibatis.builder.xml.XMLMapperBuilder.parse()方法源码。

    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. Cause: " + e, e);
        }
    }

四、MyBatis 内部数据结构

逐一读取Mapper.xml文件内的各个元素。为了更为直观的了解xml元素至Mybatis的内部数据结构,我做了一个对照图。

图

这些Xml配置元素,Mybatis将它们分别封装成了ParameterMap、ParameterMapping、ResultMap、ResultMapping、MappedStatement、BoundSql等内部数据结构对象。

这些数据库结构对象,均放置于Configuration内部保存起来。

    
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
    protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");

Mybatis初始化流程,经过系统环境参数初始化和Mapper映射初始化,简单的两个步骤就完成了,过程并不复杂。

五、MyBatis 配置参数作用

Properties variables`的作用

通常,我们会单独配置jdbc.properties文件,保存于variables变量中,而Xml文件内可以使用${driver}占位符,读取时可动态替换占位符的值。

扫描package参数

    <typeAliases>
       <typeAlias alias="Student" type="com.mybatis3.domain.Student" />
       <typeAlias alias="Teacher" type="com.mybatis3.domain.Teacher" />
       <package name="com.mybatis3.domain" />
    </typeAliases>

前两个typeAlias,很容易理解,那么元素如何处理呢?

    public void registerAliases(String packageName, Class<?> superType){
       ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
       resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
       Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
       for(Class<?> type : typeSet){
          // 排除内部类、接口
          if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
              registerAlias(type);
          }
       }
    }

namespace如何映射Mapper接口

    org.apache.ibatis.builder.xml.XMLMapperBuilder.bindMapperForNamespace()

直接使用Class.forName(),成功找到就注册,找不到就什么也不做。

Mybatis中的namespace有两个功能。

和其名字含义一样,作为名称空间使用。namespace + id,就能找到对应的Sql。

作为Mapper接口的全限名使用,通过namespace,就能找到对应的Mapper接口(也有称Dao接口的)。Mybatis推荐的最佳实践,但并不强制使用。

Mapper接口注册至Configuration的MapperRegistry mapperRegistry内。

    
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>()

Mapper接口将通过MapperProxyFactory创建动态代理对象。

一个MappedStatement被缓存了两个引用的原理及原因

configuration.addMappedStatement(statement);调用上面一句话,往Map里放置一个MappedStatement对象,结果Map中变成两个元素。

    com.mybatis3.mappers.StudentMapper.findAllStudents=org.apache.ibatis.mapping.MappedStatement@add0edd
    findAllStudents=org.apache.ibatis.mapping.MappedStatement@add0edd

我们的问题是,为什么会变成两个元素?同一个对象,为什么要存有两个键的引用?

其实,在Mybatis中,这些Map,都是StrictMap类型,Mybatis在StrictMap内做了手脚。

    protected static class StrictMap<V> extends HashMap<String, V> {
    
        public V put(String key, V value) {
          if (containsKey(key)) {
            throw new IllegalArgumentException(name + " already contains value for " + key);
          }
          if (key.contains(".")) {
            final String shortKey = getShortName(key);
            // 不存在shortKey键值,放进去
            if (super.get(shortKey) == null) {
              super.put(shortKey, value);
            } else {
            // 存在shortKey键值,填充占位对象Ambiguity
              super.put(shortKey, (V) new Ambiguity(shortKey));
            }
          }
          return super.put(key, value);
        }
    }

Mybatis重写了put方法,将id和namespace+id的键,都put了进去,指向同一个MappedStatement对象。如果shortKey键值存在,就填充为占位符对象Ambiguity,属于覆盖操作。

这样做的好处是,方便我们编程。

    Student std  = sqlSession.selectOne("findStudentById", 1);
    Student std  = sqlSession.selectOne("com.mybatis3.mappers.StudentMapper.findStudentById", 1);

上面两句代码,是等价的,Mybatis不强制我们一定要加namespace名称空间,所以,这是存放两个键的良苦用心。

问题:不同namespace空间下的id,能否相同呢?(网上的说法是,不同名称空间下的id可以相同)

明白上述put原理后,就不难得出结论,namespace名称空间不同,而id相同时,使用namespace+id获取Sql,完全可以正确执行。如果只用id获取,那么,将导致错误。

org.apache.ibatis.session.Configuration.StrictMap.get()方法源码。

    
    public V get(Object key) {
       V value = super.get(key);
       if (value == null) {
           throw new IllegalArgumentException(name + " does not contain value for " + key);
       }
       if (value instanceof Ambiguity) {
          throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
          + " (try using the full name including the namespace, or rename one of the entries)");
       }
       return value;
    }

get时,如果得到的是一个占位对象Ambiguity,就抛出异常,要求使用full name进行调用。full name就是namespace+id。Ambiguity意为模糊不清。

解决办法:

  1. 保证shortKey(即id)不重复。(好像有点难度,不推荐)
  2. 使用绑定Mapper接口调用方法,因为它总是转换为full name调用。(Mybatis最佳实践,推荐)
  3. 直接使用字符串full name调用。(退而求其次的方式,不推荐)

初始化过程中的mapped和incomplete对象

翻译为搞定的和还没搞定的。这恐怕是Mybatis框架中比较奇葩的设计了,给人很多迷惑,我们来看看它具体是什么意思。

    <resultMap type="Student" id="StudentResult" extends="Parent">
    	<id property="studId" column="stud_id" />
    	<result property="name" column="name" />
    	<result property="email" column="email" />
    	<result property="dob" column="dob" />
    </resultMap>
    
    <resultMap type="Student" id="Parent">
    	<result property="phone" column="phone" />
    </resultMap>

Mapper.xml中的很多元素,是可以指定父元素的,像上面extends="Parent"。然而,Mybatis解析元素时,是按顺序解析的,于是先解析的id="StudentResult"的元素,然而该元素继承自id="Parent"的元素,但是,Parent被配置在下面了,还没有解析到,内存中尚不存在,怎么办呢?Mybatis就把id="StudentResult"的元素标记为incomplete的,然后继续解析后续元素。等程序把id="Parent"的元素也解析完后,再回过头来解析id="StudentResult"的元素,就可以正确继承父元素的内容。

先标记 后解析能解析到的 回头再解析一边之前标记过的 这个顺序

简言之就是,你的父元素可以配置在你的后边,不限制非得配置在前面。无论你配置在哪儿,Mybatis都能“智能”的获取到,并正确继承。

这便是在Configuration对象内,有的叫mapped,有的叫incomplete的原因。

    protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
    protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
    protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
    protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();

org.apache.ibatis.builder.xml.XMLMapperBuilder.parse()方法内,触发了incomplete的再度解析。

    public void parse() {
       if (!configuration.isResourceLoaded(resource)) {
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          bindMapperForNamespace();
       }
       // 执行incomplete的地方
       parsePendingResultMaps();
       parsePendingChacheRefs();
       parsePendingStatements();
    }

Pending含义为待定的,悬而未决的意思。

引用资料