1. 程式人生 > >MyBatis初始化過程解析----廣西11選5平臺出租源碼解析

MyBatis初始化過程解析----廣西11選5平臺出租源碼解析

solver 原本 file code 1.3 lds elements ret variables

  • 準備工作
  • 為了看清楚廣西11選5平臺出租的 Q1446595067 整個初始化過程,先創建一個簡單的Java項目,目錄結構如下圖所示:

    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();
            }
        }
    }
    1. MyBatis初始化過程
      2.1 獲取配置文件

    當系統初始化時,首先會讀取配置文件,並將其解析成InputStream
    java
    String resource = "mybatis.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    2.2 創建SqlSessionFactoryBuilder對象

    從SqlSessionFactoryBuilder的名字中可以看出,SqlSessionFactoryBuilder是用來創建SqlSessionFactory對象的。
    來看一下SqlSessionFactoryBuilder源碼:

    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的體系結構。

    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 resource="org/mybatis/example/config.properties">
    <property name="username" value="dev_user"/>
    <property name="password" value="F2Fa3!33TYyg"/>
    </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>
        <setting name="cacheEnabled" value="true"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="multipleResultSetsEnabled" value="true"/>
    </settings>
    
    節點的解析過程: 
    <settings>屬性的解析過程和 <properties>屬性的解析過程極為類似,這裏不再贅述。最終,所有的setting屬性都被存儲在Configuration對象中。

    2.3.2.3 屬性的解析過程

    <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);
          } else {// 如果當前結點為< typeAlias > 
            // 獲取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>節點的定義方式有如下四種:

    方式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);
          }else {// 如果當前節點為<mapper> 
            // 依次獲取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();
            } else if (resource == null && url != null && mapperClass == null) {
    
              // 解析url屬性(Mapper.xml文件的路徑)
              ErrorContext.instance().resource(url);
              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屬性(Mapper Class的全限定名) 
              // 將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);
        }else if("discriminator".equals(resultChild.getName())) {
          // 若當前節點為<discriminator>,則進行條件判斷,並將命中的子節點添加到resultMappings中去  
          discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        }else {
          // 若當前節點為<result>、<association>、<collection>,則將其添加到resultMappings中去 
          // 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>

    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初始化過程解析----廣西11選5平臺出租源碼解析