天天看點

MyBatis源碼解析(一)——MyBatis初始化過程解析

轉載于 https://www.jianshu.com/p/7bc6d3b7fb45

1. 準備工作

為了看清楚MyBatis的整個初始化過程,先建立一個簡單的Java項目,目錄結構如下圖所示:

MyBatis源碼解析(一)——MyBatis初始化過程解析
1.1 Product 産品實體類
public class Product {
    private long id;
    private String productName;
    private String productContent;
    private String price;
    private int sort;
    private int falseSales;
    private long category_id;
    private byte type;
    private byte state;
    // PS:省略setter、getter函數
}
           
1.2 ProductMapper 産品持久化接口
public interface ProductMapper {
    /**
     * 查詢所有的産品
     * @return
     */
    List<Product> selectProductList();
}
           
1.3 ProductMapper.xml 産品映射檔案
<?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
 <mapper namespace="team.njupt.mapper.ProductMapper">
     <select id="selectProductList" resultType="team.njupt.entity.Product">
         select * from product
     </select>
 </mapper>
           
1.4 db.properties 資料庫配置檔案
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/waimai?useUnicode=true&characterEncoding=utf8
username=root
password=xxxxxx
           
1.5 mybatis.xml 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>
    <properties resource="db.properties">
        <!--<property name="username" value="dev_user"/>-->
        <!--<property name="password" value="F2Fa3!33TYyg"/>-->
    </properties>

    <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="team/njupt/mapper/ProductMapper.xml"/>
    </mappers>
</configuration>
           
1.6 Main 主函數
public class Main {
    public static void main(String[] args) throws IOException {

        String resource = "mybatis.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
            List<Product> productList = productMapper.selectProductList();
            for (Product product : productList) {
                System.out.printf(product.toString());
            }
        } finally {
            sqlSession.close();
        }
    }
}
           

2. MyBatis初始化過程

2.1 擷取配置檔案

當系統初始化時,首先會讀取配置檔案,并将其解析成InputStream

String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
           
2.2 建立SqlSessionFactoryBuilder對象

從SqlSessionFactoryBuilder的名字中可以看出,SqlSessionFactoryBuilder是用來建立SqlSessionFactory對象的。

來看一下SqlSessionFactoryBuilder源碼:

MyBatis源碼解析(一)——MyBatis初始化過程解析

SqlSessionFactoryBuilder中隻有一些重載的build函數,這些build函數的入參都是MyBatis配置檔案的輸入流,傳回值都是SqlSessionFactory;由此可見,SqlSessionFactoryBuilder的作用很純粹,就是用來通過配置檔案建立SqlSessionFactory對象的。

2.3 SqlSessionFactory建立過程

下面具體來看一下,build函數是如何建立SqlSessionFactory對象的。

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) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
           

2.3.1 構造XMLConfigBuilder對象

build函數首先會構造一個XMLConfigBuilder對象,從名字上大緻可以猜到,該對象是用來解析XML配置檔案的。下面來看一下XMLConfigBuilder的體系結構。

MyBatis源碼解析(一)——MyBatis初始化過程解析
  • XMLxxxBuilder是用來解析XML配置檔案的,不同類型XMLxxxBuilder用來解析MyBatis配置檔案的不同部位。比如:XMLConfigBuilder用來解析MyBatis的配置檔案,XMLMapperBuilder用來解析MyBatis中的映射檔案(如上文提到的ProductMapper.xml),XMLStatementBuilder用來解析映射檔案中的SQL語句。
  • 這些XMLxxxBuilder都有一個共同的父類——BaseBuilder。這個父類維護了一個全局的Configuration對象,MyBatis的配置檔案解析後就以Configuration對象的形式存儲。
  • 當建立XMLConfigBuilder對象時,就會初始化Configuration對象,并且在初始化Configuration對象的時候,一些别名會被注冊到Configuration的typeAliasRegistry容器中。
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  super(new Configuration());
   ErrorContext.instance().resource("SQL Mapper Configuration");
   this.configuration.setVariables(props);
   this.parsed = false;
   this.environment = environment;
   this.parser = parser;
   }

public Configuration() {
   typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
   typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
   
   typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
   typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
   typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
   
   typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
   typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
   typeAliasRegistry.registerAlias("LRU", LruCache.class);
   typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
   typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
   ……
}
           
2.3.2 解析配置檔案

當有了XMLConfigBuilder對象之後,接下來就可以用它來解析配置檔案了。

private void parseConfiguration(XNode root) {
  try {
    // 解析<properties>節點
    propertiesElement(root.evalNode("properties"));
    // 解析<settings>節點
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    // 解析<typeAliases>節點
    typeAliasesElement(root.evalNode("typeAliases"));
    // 解析<plugins>節點
    pluginElement(root.evalNode("plugins"));
    // 解析<objectFactory>節點
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    // 解析<reflectorFactory>節點
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // 解析<environments>節點
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    // 解析<mappers>節點
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}
           

從上述代碼中可以看到,XMLConfigBuilder會依次解析配置檔案中的

<properties>

< settings >

< environments>

< typeAliases >

< plugins >

< mappers >

等屬性。下面介紹下幾個重要屬性的解析過程。

2.3.2.1

<properties>

節點的解析過程

<properties>

節點的定義如下:

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

<properties>

節點的解析過程:

/**
  * @Param context <properties>節點
  */
private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 擷取<properties>節點的所有子節點
    Properties defaults = context.getChildrenAsProperties();
    // 擷取<properties>節點上的resource屬性
    String resource = context.getStringAttribute("resource");
    // 擷取<properties>節點上的url屬性
    String url = context.getStringAttribute("url");
    // resource和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) {
      // 擷取resource屬性值對應的properties檔案中的鍵值對,并添加至defaults容器中        
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      // 擷取url屬性值對應的properties檔案中的鍵值對,并添加至defaults容器中
      defaults.putAll(Resources.getUrlAsProperties(url));
    }
    // 擷取configuration中原本的屬性,并添加至defaults容器中
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);
    }
    parser.setVariables(defaults);
    // 将defaults容器添加至configuration中
    configuration.setVariables(defaults);
  }
}
           

首先讀取

<resources>

節點下的所有

<resource>

節點,并将每個節點的name和value屬性存入Properties中。

然後讀取

<resources>

節點上的resource、url屬性,并擷取指定配置檔案中的name和value,也存入Properties中。(PS:由此可知,如果resource節點上定義的屬性和properties檔案中的屬性重名,那麼properties檔案中的屬性值會覆寫resource節點上定義的屬性值。)

最終,攜帶所有屬性的Properties對象會被存儲在Configuration對象中。

2.3.2.2

<settings>

節點的解析過程

<settings>

節點的定義如下:

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
</settings>
           

<settings>

節點的解析過程:

<settings>

屬性的解析過程和

<properties>

屬性的解析過程極為類似,這裡不再贅述。最終,所有的setting屬性都被存儲在Configuration對象中。

2.3.2.3

<typeAliases>

屬性的解析過程

<typeAliases>

屬性的定義方式有如下兩種:

方式1:

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

方式2:

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

采用這種方式時,MyBatis會為指定包下的所有類起一個别名,該别名為首字母小寫的類名。

<typeAliases>

節點的解析過程如下:

private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    // 周遊<typeAliases>下的所有子節點
    for (XNode child : parent.getChildren()) {
      // 若目前結點為<package>
      if ("package".equals(child.getName())) {
        // 擷取<package>上的name屬性(包名)
        String typeAliasPackage = child.getStringAttribute("name");
        // 為該包下的所有類起個别名,并注冊進configuration的typeAliasRegistry中          
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } 
      // 如果目前結點為< typeAlias >
      else {
        // 擷取alias和type屬性
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        // 注冊進configuration的typeAliasRegistry中
        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);
        }
      }
    }
  }
}
           

如果

<typeAliases>

節點下定義了

<package>

節點,那麼MyBatis會給該包下的所有類起一個别名(以類名首字母小寫作為别名)

如果

<typeAliases>

節點下定義了

<typeAlias>

節點,那麼MyBatis就會給指定的類起指定的别名。

這些别名都會被存入configuration的typeAliasRegistry容器中。

2.3.2.4

<mappers>

節點的解析過程

<mappers>

節點的定義方式有如下四種:

方式1:

<mappers>
  <package name="org.mybatis.builder"/>
</mappers>
           

方式2:

<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
           

方式3:

<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
           

方式4:

<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
           

<mappers>

節點的解析過程如下:

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 周遊<mappers>下所有子節點
    for (XNode child : parent.getChildren()) {
      // 如果目前節點為<package>
      if ("package".equals(child.getName())) {
        // 擷取<package>的name屬性(該屬性值為mapper class所在的包名)
        String mapperPackage = child.getStringAttribute("name");
        // 将該包下的所有Mapper Class注冊到configuration的mapperRegistry容器中
        configuration.addMappers(mapperPackage);
      } 
      // 如果目前節點為<mapper>
      else {
        // 依次擷取resource、url、class屬性
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        // 解析resource屬性(Mapper.xml檔案的路徑)
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          // 将Mapper.xml檔案解析成輸入流
          InputStream inputStream = Resources.getResourceAsStream(resource);
          // 使用XMLMapperBuilder解析Mapper.xml,并将Mapper Class注冊進configuration對象的mapperRegistry容器中
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, 
          resource, configuration.getSqlFragments());
          mapperParser.parse();
        } 
        // 解析url屬性(Mapper.xml檔案的路徑)
        else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, 
          configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } 
        // 解析class屬性(Mapper Class的全限定名)
        else if (resource == null && url == null && mapperClass != null) {
          // 将Mapper Class的權限定名轉化成Class對象
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          // 注冊進configuration對象的mapperRegistry容器中
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url,
           resource or class, but not more than one.");
        }
      }
    }
  }
}
           

MyBatis會周遊

<mappers>

下所有的子節點,如果目前周遊到的節點是

<package>

,則MyBatis會将該包下的所有Mapper Class注冊到configuration的mapperRegistry容器中。

如果目前節點為

<mapper>

,則會依次擷取resource、url、class屬性,解析映射檔案,并将映射檔案對應的Mapper Class注冊到configuration的mapperRegistry容器中。

其中,

<mapper>

節點的解析過程如下:

XMLMapperBuilder mapperParser = 
new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
           

在解析前,首先需要建立XMLMapperBuilder,建立過程如下:

private XMLMapperBuilder(XPathParser parser, Configuration configuration, 
String resource, Map<String, XNode> sqlFragments) {

  // 将configuration賦給BaseBuilder
  super(configuration);
  // 建立MapperBuilderAssistant對象(該對象為MapperBuilder的協助者)
  this.builderAssistant = new  MapperBuilderAssistant(configuration, resource);
  this.parser = parser;
  this.sqlFragments = sqlFragments;
  this.resource = resource;
}
           

首先會初始化父類BaseBuilder,并将configuration賦給BaseBuilder;

然後建立MapperBuilderAssistant對象,該對象為XMLMapperBuilder的協助者,用來協助XMLMapperBuilder完成一些解析映射檔案的動作。

當有了XMLMapperBuilder後,便可進入解析

<mapper>

的過程:

public void parse() {
  // 若目前的Mapper.xml尚未被解析,則開始解析
  // PS:若<mappers>節點下有相同的<mapper>節點,那麼就無需再次解析了
  if (!configuration.isResourceLoaded(resource)) {
    // 解析<mapper>節點
    configurationElement(parser.evalNode("/mapper"));
    // 将該Mapper.xml添加至configuration的LoadedResource容器中,下回無需再解析
    configuration.addLoadedResource(resource);
    // 将該Mapper.xml對應的Mapper Class注冊進configuration的mapperRegistry容器中
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
           

configurationElement函數

private void configurationElement(XNode context) {
try {
  // 擷取<mapper>節點上的namespace屬性,該屬性必須存在,表示目前映射檔案對應的Mapper Class是誰
  String namespace = context.getStringAttribute("namespace");
  if (namespace == null || namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
  }
  // 将namespace屬性值賦給builderAssistant
  builderAssistant.setCurrentNamespace(namespace);
  // 解析<cache-ref>節點
  cacheRefElement(context.evalNode("cache-ref"));
  // 解析<cache>節點
  cacheElement(context.evalNode("cache"));
  // 解析<parameterMap>節點
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  // 解析<resultMap>節點
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  // 解析<sql>節點
  sqlElement(context.evalNodes("/mapper/sql"));
  // 解析sql語句      
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
  throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
           

resultMapElements函數

該函數用于解析映射檔案中所有的

<resultMap>

節點,這些節點會被解析成ResultMap對象,存儲在Configuration對象的resultMaps容器中。

<resultMap>

節點定義如下:

<resultMap id="userResultMap" type="User">
  <constructor>
     <idArg column="id" javaType="int"/>
     <arg column="username" javaType="String"/>
  </constructor>
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>
           

<resultMap>

節點的解析過程:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) 
throws Exception {

  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  // 擷取<ResultMap>上的id屬性
  String id = resultMapNode.getStringAttribute("id",
    resultMapNode.getValueBasedIdentifier());
  // 擷取<ResultMap>上的type屬性(即resultMap的傳回值類型)
  String type = resultMapNode.getStringAttribute("type",
    resultMapNode.getStringAttribute("ofType",
        resultMapNode.getStringAttribute("resultType",
            resultMapNode.getStringAttribute("javaType"))));
  // 擷取extends屬性
  String extend = resultMapNode.getStringAttribute("extends");
  // 擷取autoMapping屬性
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  // 将resultMap的傳回值類型轉換成Class對象
  Class<?> typeClass = resolveClass(type);
  Discriminator discriminator = null;
  // resultMappings用于存儲<resultMap>下所有的子節點
  List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
  resultMappings.addAll(additionalResultMappings);
  // 擷取并周遊<resultMap>下所有的子節點
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    // 若目前節點為<constructor>,則将它的子節點們添加到resultMappings中去
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    }
    // 若目前節點為<discriminator>,則進行條件判斷,并将命中的子節點添加到resultMappings中去
    else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    }
    // 若目前節點為<result>、<association>、<collection>,則将其添加到resultMappings中去
    else {
      // PS:flags僅用于區分目前節點是否是<id>或<idArg>,因為這兩個節點的屬性名為name,
      // 而其他節點的屬性名為property
      List<ResultFlag> flags = new ArrayList<ResultFlag>();
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  // ResultMapResolver的作用是生成ResultMap對象,并将其加入到Configuration對象的resultMaps容器中(具體過程見下)
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id,
   typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}
           

ResultMapResolver這個類很純粹,有且僅有一個函數resolve,用于構造ResultMap對象,并将其存入Configuration對象的resultMaps容器中;而這個過程是借助于MapperBuilderAssistant.addResultMap完成的。

public ResultMap resolve() {
  return assistant.addResultMap(this.id, this.type, this.extend,  
  this.discriminator, this.resultMappings, this.autoMapping);
}
           

sqlElement函數

該函數用于解析映射檔案中所有的

<sql>

節點,并将這些節點存儲在目前映射檔案所對應的XMLMapperBuilder對象的sqlFragments容器中,供解析sql語句時使用。

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
           

buildStatementFromContext函數

該函數會将映射檔案中的sql語句解析成MappedStatement對象,并存在configuration的mappedStatements。

2.3.3 建立SqlSessionFactory對象
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) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
           

回過頭來再看一下SqlSessionFactory的build函數,剛才說了半天,介紹了XMLConfigBuilder解析映射檔案的過程,解析完成之後parser.parse()函數會傳回一個包含了映射檔案解析結果的configuration對象,緊接着,這個對象将作為參數傳遞給另一個build函數,如下:

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
           

這個函數将configuration作為參數,建立了DefaultSqlSessionFactory對象。

DefaultSqlSessionFactory是接口SqlSessionFactory的一個實作類,SqlSessionFactory的體系結構如下圖所示:

MyBatis源碼解析(一)——MyBatis初始化過程解析

此時,SqlSessionFactory建立完畢!

繼續閱讀