第二章 配置文件的解析过程 
首先我们从一个入口的例程开始:
1 2 3 4 5 6 7 8 9 10 11 12 13 public  class  MyApp   {    public  static  void  main (String[] args)  throws  IOException  {         Logger logger = Logger.getLogger(MyApp.class);         InputStream inputStream = Resources.getResourceAsStream("mybatis.xml" );         SqlSessionFactory sqlSessionFactory =              new  SqlSessionFactoryBuilder().build(inputStream);         SqlSession sqlSession = sqlSessionFactory.openSession();         ProductDao productDao = sqlSession.getMapper(ProductDao.class);         Product product= productDao.getProduct(12 );         logger.info(product);     } } 
 
从这个例程我们可以看出:
首先我们通过MyBatis提供的Resources类读取了配置文件。 
然后使用SqlSessionFactoryBuilder,来建造一个SqlSessionFactory 
接着通过这个工厂获取SqlSession实例,就可以使用SqlSession执行各种数据库操作 
 
接下来详细分析SqlSessionFactoryBuilder的build()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public  SqlSessionFactory build (InputStream inputStream,                                   String environment,                                   Properties properties)   {    try  {       XMLConfigBuilder parser =            new  XMLConfigBuilder(inputStream, environment, properties);       return  build(parser.parse());     } catch  (Exception e) {       throw  ExceptionFactory.wrapException("Error building SqlSession." , e);     } finally  {       ErrorContext.instance().reset();       try  {         inputStream.close();       } catch  (IOException e) {                }     }   }   public  SqlSessionFactory build (Configuration config)   {     return  new  DefaultSqlSessionFactory(config);   } 
 
可以看到,主要逻辑是,使用XMLConfigBulider的parse()方法生成一个Configuration对象,Configuration类存放了Mybatis的所有全局配置,根据这个类中的配置,我们就可以使用建造者模式建造一个SqlSessionFactory了。
我们再跟随第九行代码的调用栈向下
1 2 3 4 5 6 7 8 9 10 11 public  Configuration parse ()   {    if  (parsed) {       throw  new  BuilderException("Each XMLConfigBuilder can only be used once." );     }     parsed = true ;     parseConfiguration(parser.evalNode("/configuration" ));     return  configuration;   } 
 
这个方法的逻辑极其简单,首先通过一个标识parsed判断这个配置文件是否已经被解析过了,解析过则直接返回。否则调用parseConfiguration()进行解析。
我们再跟随第8行代码的调用栈向下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private  void  parseConfiguration (XNode root)   {    try  {       	      	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);                                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);     } } 
 
可以看到这个方法按照顺序,从XNode根节点出发,解析了不同的配置文件节点。这里简单解释一下什么是XNode,它是一颗类似DOM结构的树,这棵树存放了XML的初步解析结果。
上述调用栈如下图
接下来我们将具体分析<properties>节点、<settings>节点、<typeAliases>节点和<typeHandler>节点的解析过程,注意:这些节点的解析结果最终都将存放在Configuration中
  2.1 <properties>节点解析过程 
<properties>节点的主要作用是定义一些在后面的节点会使用的变量,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <configuration >     <properties >          <property  name ="jdbc.driver"  value ="com.mysql.cj.jdbc.Driver" />          <property  name ="jdbc.url"  value ="jdbc:mysql://localhost:3306/cosmetic_store?serverTimezone=UTC" />          <property  name ="jdbc.username"  value ="root" />          <property  name ="jdbc.password"  value ="123456" />      </properties >      <settings >          <setting  name ="cacheEnabled"  value ="true" />      </settings >      <typeAliases >          <typeAlias  type ="biz.t0ugh.Model.Product"  alias ="product" />      </typeAliases >      <environments  default ="development" >          <environment  id ="development" >              <transactionManager  type ="JDBC" />              <dataSource  type ="POOLED" >                  <property  name ="driver"  value ="${jdbc.driver}" />                  <property  name ="url"  value ="${jdbc.url}" />                  <property  name ="username"  value ="${jdbc.username}" />                  <property  name ="password"  value ="${jdbc.password}" />              </dataSource >          </environment >      </environments >      <mappers >          <mapper  resource ="ProductMapper.xml" />      </mappers >  </configuration > 
 
可以看到,我们在<dataSource>中引用了<properties>节点定义的变量
接下来我们从编号2.3的代码段第7行propertiesElement(root.evalNode("properties"));向下,详细分析这个方法。
首先我们知道,properties的内容由两部分构成
可以在子节点<property>中,通过键值对的方式定义,如2.4代码段中从4-7行所示 
也可以通过<properties>节点上的resource属性或者url属性,从其他文件中读入一些properties 
 
那么这个方法实际上也只是做了这项工作
遍历所有<property>子节点,将得到的k-v对存入一个Properties的实例 
从其他文件中读取,将得到的所有k-v对存入第一步的Properties实例。注意:若第二步中某些配置与第一步重名,第一步的这个配置将被覆盖 
最后将得到的Properties实例存入到Configuration中,这样以后MyBatis的其他部分使用这些属性时直接读取即可 
 
源代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 private  void  propertiesElement (XNode context)  throws  Exception  {    if  (context != null ) {                	       	Properties defaults = context.getChildrenAsProperties();                        	String resource = context.getStringAttribute("resource" );       	String url = context.getStringAttribute("url" );       	if  (resource != null  && url != null ) {         	throw  new  BuilderException("xxxx" );       	}       	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);     }   } 
 
  2.2 <settings>节点解析过程 
<settings>节点的主要作用是定义一些MyBatis运行时的行为,如代码块2.4的9-11行中,定义了是否开启缓存
接下来我们从编号2.3的代码段第9行Properties settings = settingsAsProperties(root.evalNode("settings"));向下,详细分析这个方法。
首先我们思考settings解析与properties解析的区别是什么?答案是<setting>节点中的name属性必须是mybatis支持的配置,要言之有物才行。假如我们如下代码定义一个名为hello的节点,mybatis一定会报错,因为它没有hello这个设置。
1 2 3 4 5 <settings >     <setting  name ="cacheEnabled"  value ="true" />           <setting  name ="hello"  value ="world" />  </settings > 
 
那么我们接着思考,如何验证一个设置是否存在呢?最简单的方法是维护一个常量表,它存储所有存在的设置。但是这很不灵活还会造成冗余。Mybatis使用了Java的放射机制,它有一个工具类叫做MataClass,可以读取Configuration中的所有setxxx()方法。这样我们每读取一个设置,就通过MataClass来检查Configuration中是否有对应的setter,没有就报错。
具体步骤如下
解析settings子节点的内容,并将解析结果转成Properties对象 
为Configuration创建元信息对象MetaClass 
通过MetaClass检测Configuration中是否存在某个属性的setter方法 
若通过MetaClass的检测,则返回Properties对象。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private  Properties settingsAsProperties (XNode context)   {    if  (context == null ) {     	return  new  Properties();     }          Properties props = context.getChildrenAsProperties();                    MetaClass metaConfig =          MetaClass.forClass(Configuration.class, localReflectorFactory);               for  (Object key : props.keySet()) {       	if  (!metaConfig.hasSetter(String.valueOf(key))) {       		throw  new  BuilderException("The setting "  + key + " is not known." );       	}     }               return  props; } 
 
这样我们就取得了包含所有设置项的Properties对象,接下来还需要将这个对象中的内容存储到Configuration中,源代码如下,逻辑很简单,就是调用各种setter而已。
1 2 3 4 5 6 7 private  void  settingsElement (Properties props)   {   		configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior" , "PARTIAL" )));      } 
 
  2.3 <typeAliases>节点解析过程 
我们都知道,MyBatis提供一个很方便的功能:我们可以给自己写的类定义别名(aliases),这样当我们在MyBatis中需要使用类名时,只需要写这个别名,而不需要写冗长的全限定类名。
MyBatis中的别名配置方式有两种
配置包名,这个包下的所有类都会被扫描并且根据类名生成别名
1 2 3 <typeAliases > 	<package  name ="com.edu.neu.zady.dao" />  </typeAliases > 
 
 
通过手动的方式,明确为某个类型配置别名
1 2 3 <typeAliases > 	<package  alias ="product"  type ="com.edu.neu.zady.dao.Product" />  </typeAliases > 
 
 
 
除了这些自定义的别名,MyBatis还在Configuration中为一些常用类生成了别名。
在Configuration中,自定义的别名和预定义的别名都存放在了TypeAliasRegister中,它提供注册别名和获取别名的功能。
具体源代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private  void  typeAliasesElement (XNode parent)   {    if  (parent != null ) {       for  (XNode child : parent.getChildren()) {                  if  ("package" .equals(child.getName())) {           String typeAliasPackage = child.getStringAttribute("name" );           configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);         } else  {           String alias = child.getStringAttribute("alias" );           String type = child.getStringAttribute("type" );           try  {             Class<?> clazz = Resources.classForName(type);             if  (alias == null ) {               typeAliasRegistry.registerAlias(clazz);             } else  {               typeAliasRegistry.registerAlias(alias, clazz);             }           } catch  (ClassNotFoundException e) {             throw  new  BuilderException("Error registering typeAlias for '"  + alias + "'. Cause: "  + e, e);           }         }       }     }   } 
 
由上源码可见,对于别名的解析,主要是使用Configuration中的typeAliasRegister属性的各种重载的registerAlias()方法,这些重载比较多。我们挑选代码段2.7的第19行typeAliasRegistry.registerAlias(alias, clazz);作为入口,继续向下,分析一个具体的注册过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private  final  Map<String, Class<?>> typeAliases = new  HashMap<>();public  void  registerAlias (String alias, Class<?> value)   {         if  (alias == null ) {       throw  new  TypeException("The parameter alias cannot be null" );     }               String key = alias.toLowerCase(Locale.ENGLISH);               if  (typeAliases.containsKey(key)          && typeAliases.get(key) != null           && !typeAliases.get(key).equals(value)) {                throw  new  TypeException("The alias '"  +                                alias +                                "' is already mapped to the value '"  +                               typeAliases.get(key).getName() +                               "'." );     }               typeAliases.put(key, value);   } 
 
解析<package>的过程与此类似,这里就不再赘述。
  2.4 <typeHandler>节点解析过程 
在向数据库存储或读取数据时,我们需要将数据库字段类型和java类型进行一个转换。比如数据库中有CHAR和 VARCHAR等类型,但java中没有这些类型,不过java有String类型。所以我们在从数据库中读取CHAR和 VARCHAR类型的数据时,就可以把它们转成String。在 MyBatis中,数据库类型和java类型之间的转换任务是委托给类型处理器TypeHandler去处理的。MyBatis提供了一些常见类型的类型处理器,除此之外,我们还可以自定义类型处理器以非常见类型转换的需求。
了解完TypeHandler的用途,我们继续探究它是如何注册到Configuration的。我们从代码段2.3的32行typeHandlerElement(root.evalNode("typeHandlers"));继续向下,查看typeHandlerElement方法,这个方法主要有三步
读取javaType、jdbcType、handlerType的字符串形式,也就是类名 
将这些类名解析为具体的Class对象 
根据前两步的解析结果选择不同的解析方法,也是一堆register的重载方法 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 private  void  typeHandlerElement (XNode parent)   {    if  (parent != null ) {       for  (XNode child : parent.getChildren()) {                  if  ("package" .equals(child.getName())) {           String typeHandlerPackage = child.getStringAttribute("name" );           typeHandlerRegistry.register(typeHandlerPackage);         } else  {                                 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);                                 if  (javaTypeClass != null ) {             if  (jdbcType == null ) {               typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);             } else  {               typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);             }           } else  {             typeHandlerRegistry.register(typeHandlerClass);           }         }       }     }  } 
 
我们现在知道了,实际的解析过程是通过Configuration的成员变量TypeHandlerRegistry的各种名为register的重载方法进行的。我们从代码段2.9的28行继续向下typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);,这个方法的具体分析我放到了注释中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private  final  Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap     = new  ConcurrentHashMap<>(); private  void  register (Type javaType, JdbcType jdbcType, TypeHandler<?> handler)   {    if  (javaType != null ) {              Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);                     if  (map == null  || map == NULL_TYPE_HANDLER_MAP) {         map = new  HashMap<>();         typeHandlerMap.put(javaType, map);       }                     map.put(jdbcType, handler);     }     allTypeHandlersMap.put(handler.getClass(), handler); } 
 
由代码段2.10可知,注册过程其实就是把这个新的类型处理器放到Map中,仅此而已。不过,值得注意的是,这个Map是一个两层嵌套结构,例子如下图所示。这也启示我们,如果要存储三元组,可以使用Map嵌套Map的方式。