注:本文的内容大部分转载自 Mybatis详解 - Java全栈知识体系 
 
这篇文章记录我在学习 Mybaits 源码的一些记录,包含两部分,这是第一部分,主要是学习一下 Mybatis 的总体设计。
 
源码下载和构建 先把源码下载下来,可以参考这篇文章:MyBatis源码阅读准备 。
在测试过程中使用的 Demo 来自 mybatis-cache-demo ,原文链接:聊聊MyBatis缓存机制 - 美团技术团队 。
源码和 Demo 准备好之后,将源码导入到 Demo 的项目中,修改 Demo 依赖中的 mybatis,将其修改为我们下载下来的源码,如下:
1 2 3 4 5 <dependency>     <groupId>org.mybatis</groupId>     <artifactId>mybatis</artifactId>     <version>3.3 .0 -SNAPSHOT</version> </dependency> 
 
最终项目的结构如下:
总体设计 接口层 传统的 Mybatis 工作模式是使用 SqlSession 对象完成和数据库的交互:创建一个 SqlSession,用来和数据库进行交互,然后根据 Statement Id 和参数来操作数据库。 
上面的方式比较简单:new 一个对象,然后调用这个对象的各个方法。但是它不符合面向对象语言的特性,为了适应这一特性,Mybaits 增加了一种支持接口的调用方式,也就是我们现在常用的方式。
如下图,MyBatis 将配置文件中的每一个 <mapper> 节点抽象为一个 Mapper 接口,而这个接口中声明的方法和跟 <mapper> 节点中的 <select|update|delete|insert> 节点相对应,即 <select|update|delete|insert> 节点的 id 值为 Mapper 接口中的方法名称,parameterType 值表示 Mapper 对应方法的入参类型,而 resultMap 值则对应了 Mapper 接口表示的返回值类型或者返回结果集的元素类型。
根据 MyBatis 的配置规范配置好后,通过 SqlSession.getMapper(XXXMapper.class) 方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例。
我们使用 Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定 Statement Id,底层还是通过 SqlSession.select("statementId",parameterObject) 或者 SqlSession.update("statementId",parameterObject) 等等来实现对数据库的操作。
MyBatis 引用 Mapper 接口这种调用方式,纯粹是为了满足面向接口编程的需要。(其实还有一个原因是在于,面向接口的编程,使得用户在接口上可以使用注解来配置 SQL 语句,这样就可以脱离 XML 配置文件,实现“0配置”)。
数据处理层 主要包含三个功能:
参数映射。 这个是指对于 java 数据类型和 jdbc 数据类型之间的转换,包括两个过程:
查询阶段:我们要将java类型的数据,转换成jdbc类型的数据,通过 preparedStatement.setXXX() 来设值; 
结果映射:对 resultset 查询结果集的 jdbcType 数据转换成 java 数据类型。 
 
 
通过传入参数构建动态 SQL 语句。 MyBatis 通过传入的参数值,使用 Ognl 来动态地构造 SQL 语句。 
SQL 语句的执行以及封装查询结果集成 List<E> 
 
框架支持层 
事务管理机制  事务管理机制对于 ORM 框架而言是不可缺少的一部分,事务管理机制的质量也是考量一个 ORM 框架是否优秀的一个标准。 
连接池管理机制  由于创建一个数据库连接所占用的资源比较大, 对于数据吞吐量大和访问量非常大的应用而言,连接池的设计就显得非常重要。  
缓存机制  为了提高数据利用率和减小服务器和数据库的压力,MyBatis 会对于一些查询提供会话级别的数据缓存,会将对某一次查询,放置到 SqlSession 中,在允许的时间间隔内,对于完全相同的查询,MyBatis 会直接将缓存结果返回给用户,而不用再到数据库中查找。  
SQL 语句的配置方式  传统的 MyBatis 配置 SQL 语句方式就是使用 XML 文件进行配置的,但是这种方式不能很好地支持面向接口编程的理念。为了支持面向接口的编程,MyBatis 引入了 Mapper接口的概念。面向接口的引入,对使用注解来配置 SQL 语句成为可能,用户只需要在接口上添加必要的注解即可,不用再去配置 XML 文件了。但是,目前的 MyBatis 只是对注解配置 SQL 语句提供了有限的支持,某些高级功能还是要依赖 XML 配置文件配置 SQL 语句。 
 
引导层   引导层是配置和启动MyBatis配置信息的方式。MyBatis 提供两种方式来引导 MyBatis :基于 XML 配置文件的方式、基于 Java API的方式。
Mybatis 初始化首先按照传统方式来对 Mybatis 进行初始化:
1 2 3 4 5 6 7 8 9 10 String resource = "mybatis-config.xml" ;   InputStream inputStream = Resources.getResourceAsStream(resource);   SqlSessionFactory sqlSessionFactory = new  SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession();   List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo" ); 
 
根据下面的图看一下 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 public  SqlSessionFactory build (InputStream inputStream)    {      return  build(inputStream, null , null );   }   public  SqlSessionFactory build (InputStream inputStream, String environment, Properties properties)    {      try   {                    XMLConfigBuilder parser = new  XMLConfigBuilder(inputStream, environment, properties);                    Configuration config = parser.parse();                    return  build(config);       } 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);   }   
 
初始化过程中涉及到以下几个对象:
SqlSessionFactoryBuilder : SqlSessionFactory 的构造器,用于创建 SqlSessionFactory,采用了 Builder 设计模式  
Configuration:该对象包含了 mybatis-config.xml 文件中所有 mybatis 配置信息  
SqlSessionFactory:SqlSession 工厂类,以工厂形式创建 SqlSession 对象,采用了 Factory 工厂设计模式  
XmlConfigParser:负责将 mybatis-config.xml 配置文件解析成 Configuration 对象,供 SqlSessonFactoryBuilder 使用,创建 SqlSessionFactory 
 
重点看下 mybatis 是如何将 xml 配置文件转换成 Configuration 对象的,先看流程图:
XMLConfigBuilder 会将 XML 配置文件的信息转换为 Document 对象 而 XML 配置定义文件 DTD 转换成 XMLMapperEntityResolver 对象,然后将二者封装到 XpathParser 对象中,XpathParser 的作用是提供根据 Xpath 表达式获取基本的 DOM 节点 Node 信息的操作。看下代码:
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 public  Configuration parse ()   {      if  (parsed) {           throw  new  BuilderException("Each XMLConfigBuilder can only be used once." );       }       parsed = true ;                 XNode configurationNode = parser.evalNode("/configuration" );       parseConfiguration(configurationNode);       return  configuration;   }      private  void  parseConfiguration (XNode root)   {      try  {                    propertiesElement(root.evalNode("properties" ));                   typeAliasesElement(root.evalNode("typeAliases" ));                    pluginElement(root.evalNode("plugins" ));                    objectFactoryElement(root.evalNode("objectFactory" ));                    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory" ));                    settingsElement(root.evalNode("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);       }   }  
 
在上述代码中,有一个非常重要的地方,就是解析 XML 配置文件子节点 <mappers> 的方法 mapperElements(root.evalNode("mappers")), 它将解析我们配置的 Mapper.xml 配置文件,Mapper 配置文件可以说是 MyBatis 的核心,MyBatis 的特性和理念都体现在此 Mapper 的配置和设计上。
从上述代码可知,节点解析有10步,我们重点看两个:对 environments 的解析、对 mappers 的解析。
先看对 environments 的解析,看下 environments 的配置,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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 > 
 
然后结合上面的配置看下代码:
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61    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" );                            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());               }           }       }   } private  boolean  isSpecifiedEnvironment (String id)    {      if  (environment == null )  {           throw  new  BuilderException("No environment specified." );       }       else  if  (id == null )  {           throw  new  BuilderException("Environment requires an id attribute." );       }          else  if  (environment.equals(id))  {           return  true ;       }       return  false ;   }  private  DataSourceFactory dataSourceElement (XNode context)  throws  Exception  {    if  (context != null ) {                  String type = context.getStringAttribute("type" );                  Properties props = context.getChildrenAsProperties();                  DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();         factory.setProperties(props);         return  factory;     }     throw  new  BuilderException("Environment declaration requires a DataSourceFactory." ); }  
 
在配置 dataSource 时使用了 $driver 这种表达式,它是通过 PropertyParser 来解析的:
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 public  class  PropertyParser   {    public  static  String parse (String string, Properties variables)   {         VariableTokenHandler handler = new  VariableTokenHandler(variables);         GenericTokenParser parser = new  GenericTokenParser("${" , "}" , handler);         return  parser.parse(string);     }     private  static  class  VariableTokenHandler  implements  TokenHandler   {         private  Properties variables;         public  VariableTokenHandler (Properties variables)   {             this .variables = variables;         }         public  String handleToken (String content)   {             if  (variables != null  && variables.containsKey(content)) {                 return  variables.getProperty(content);             }             return  "${"  + content + "}" ;         }     } } 
 
再看下对 mapper 的解析:
1 2 3 4 5 <mappers >     <mapper  resource ="mapper/studentMapper.xml" />      <mapper  resource ="mapper/classMapper.xml" />  </mappers > 
 
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 41 private  void  mapperElement (XNode parent)  throws  Exception  {  if  (parent != null ) {     for  (XNode child : parent.getChildren()) {       if  ("package" .equals(child.getName())) {                  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 ) {                      ErrorContext.instance().resource(resource);           try (InputStream inputStream = Resources.getResourceAsStream(resource)) {                          XMLMapperBuilder mapperParser = new  XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());             mapperParser.parse();           }         } else  if  (resource == null  && url != null  && mapperClass == null ) {                      ErrorContext.instance().resource(url);           try (InputStream inputStream = Resources.getUrlAsStream(url)){             XMLMapperBuilder mapperParser = new  XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());             mapperParser.parse();           }         } else  if  (resource == null  && url == null  && mapperClass != null ) {                      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." );         }       }     }   } } 
 
通过 resource 和 url 配置的 mapper 需要进行一步解析操作:mapperParser.parse(),相关代码如下:
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public  void  parse ()   {  if  (!configuration.isResourceLoaded(resource)) {          configurationElement(parser.evalNode("/mapper" ));     configuration.addLoadedResource(resource);          bindMapperForNamespace();   }      parsePendingResultMaps();      parsePendingCacheRefs();      parsePendingStatements(); } private  void  configurationElement (XNode context)   {  try  {     String namespace = context.getStringAttribute("namespace" );     if  (namespace == null  || namespace.isEmpty()) {       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);   } } private  void  bindMapperForNamespace ()   {  String namespace = builderAssistant.getCurrentNamespace();   if  (namespace != null ) {     Class<?> boundType = null ;     try  {       boundType = Resources.classForName(namespace);     } catch  (ClassNotFoundException e) {            }     if  (boundType != null  && !configuration.hasMapper(boundType)) {                            configuration.addLoadedResource("namespace:"  + namespace);       configuration.addMapper(boundType);     }   } } 
 
最后,将上述 Mybatis 的初始化过程用序列图细化:
配置解析过程详解 上面说了构建 configuration 对象的过程,但是其中详细的配置解析过程并没有深入,这里就来看一下。先重新看下解析 Configuration 节点相关的代码:
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 public  Configuration parse ()   {      if  (parsed) {           throw  new  BuilderException("Each XMLConfigBuilder can only be used once." );       }       parsed = true ;                 XNode configurationNode = parser.evalNode("/configuration" );       parseConfiguration(configurationNode);       return  configuration;   }      private  void  parseConfiguration (XNode root)   {      try  {                    propertiesElement(root.evalNode("properties" ));                   typeAliasesElement(root.evalNode("typeAliases" ));                    pluginElement(root.evalNode("plugins" ));                    objectFactoryElement(root.evalNode("objectFactory" ));                    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory" ));                    settingsElement(root.evalNode("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);       }   }  
 
configuration 中包含10个子节点,分别是:properties、typeAliases、plugins、objectFactory、objectWrapperFactory、settings、environments、databaseIdProvider、typeHandlers、mappers。
properties1 2 3 4 5 6 7 8 9 10 <properties >     <property  name ="driver"  value ="com.mysql.jdbc.Driver" />      <property  name ="url"  value ="jdbc:mysql://localhost:3306/test1" />      <property  name ="username"  value ="root" />      <property  name ="password"  value ="root" />  </properties > 
 
properties 节点可以进行两种配置,如上所述。
如果两种方法同时配置了,那么 首先会加载文件中的xml配置,其次是加载外部指定的properties,最后加载Java Configuration的配置 。
因为配置存放在 Properties,它继承自 HashTable 类,当依次将上述几种配置源 put 进去时,后加载的配置会覆盖先加载的配置。所以,最终应用配置时 Configuration 配置优先级最高,其次是外部的 properties 配置文件,最后是当前 xml 中的配置 。
 
TypeHandler可以利用这个实现一个自定义的 [java 类型 <-> jdbc 类型] 转换器 ,如下示例,只需要实现 BaseTypeHandler.class 即可:
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 @MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(String.class) public  class  MySimpleTypeHandler  extends  BaseTypeHandler <String >  {         public  void  setNonNullParameter (PreparedStatement ps, int  i, String parameter, JdbcType jdbcType)  throws  SQLException  {         ps.setString(i, parameter + "@自定义Handler存储" );     }          public  String getNullableResult (ResultSet rs, String columnName)  throws  SQLException  {         return  rs.getString(columnName).split("@" )[0 ] + "@自定义Handler返回" ;     }     public  String getNullableResult (ResultSet rs, int  columnIndex)  throws  SQLException  {         return  rs.getString(columnIndex).split("@" )[0 ] + "@自定义Handler返回" ;     }     public  String getNullableResult (CallableStatement cs, int  columnIndex)  throws  SQLException  {         return  cs.getString(columnIndex).split("@" )[0 ] + "@自定义Handler返回" ;     } } 
 
在 mybatis 中的配置如下,这里的配置是全局生效的:
1 2 3 <typeHandlers >     <typeHandler  handler ="typehandler.MySimpleTypeHandler"  />  </typeHandlers > 
 
也可以在 Mapper 文件中加上对 TypeHandler 的配置,实现局部配置:
1 2 3 4 5 6 7 8 9 10 <resultMap  id ="BaseResultMap"  type ="Student" >          <result  column ="name"  property ="name"  jdbcType ="VARCHAR"   typeHandler ="typehandler.MySimpleTypeHandler" />      <result  column ="id"  property ="id"  jdbcType ="INTEGER" />      <result  column ="age"  property ="age"  jdbcType ="TINYINT" />  </resultMap > <select  id ="getStudentById"  parameterType ="int"  resultMap ="BaseResultMap"  useCache ="true" >     SELECT id,name,age FROM student WHERE id = #{id} </select > 
 
详细介绍见 Mybatis(四): 类型转换器模块详解   和 TypeHandler - Java全栈知识体系 
关于 typeHandler 实现类有几个注意点还需要提一下:
通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型 ,不过这种行为可以通过两种方法改变: 
在类型处理器的配置元素(typeHandler 元素)上增加一个 javaType 属性(比如:javaType="String") 
在类型处理器的类上增加一个 @MappedTypes 注解指定与其关联的 Java 类型列表。 如果在  javaType 属性中也同时指定,则注解上的配置将被忽略。 
 
可以通过两种方式来指定关联的 JDBC 类型: 
在类型处理器的配置元素上增加一个 jdbcType 属性(比如:jdbcType="VARCHAR") 
在类型处理器的类上增加一个 @MappedJdbcTypes 注解指定与其关联的 JDBC 类型列表。 如果在 jdbcType 属性中也同时指定,则注解上的配置将被忽略。 
 
 
如果配置 jdbcType 属性或者 @MappedJdbcTypes 注解之后,insert/update 语句没有走 typeHandler。那么就检查一下 insert/update 语句中变更的属性值是否加上了 jdbcType 的配置,如果没有配置就不会生效,如下:
1 2 3 <insert  id ="addStudent"  parameterType ="entity.StudentEntity"  useGeneratedKeys ="true"  keyProperty ="id" >     INSERT INTO student(name,age) VALUES(#{name, jdbcType=VARCHAR}, #{age}) </insert > 
 
除了最基础的 BaseTypeHandler 外,mybatis 还默认实现了许多特定类型的 TypeHandler,如 EnumTypeHandler,这里有一个案例介绍如何实现一个自己的 EnumTypeHandler,优雅的实现枚举类型与 sql 的交互:如何在MyBatis中优雅的使用枚举 。
参考文章 Mybatis详解 - Java全栈知识体系 
Mybatis(四): 类型转换器模块详解 
如何在MyBatis中优雅的使用枚举