分析Mybatis啓動流程

Mybatis

分析啓動流程之前可以先看看mybatis的核心內容

mybatis核心

從MyBatis代碼實現的角度來看,MyBatis的主要的核心部件有以下幾個:

SqlSessionFactory SqlSession 工廠,全局唯一(單庫情況)
Configuration MyBatis所有的配置信息都維持在Configuration對象之中。
SqlSession 作爲MyBatis工作的主要頂層API,表示和數據庫交互的會話,完成必要數據庫增刪改查功能
Executor MyBatis執行器,是MyBatis 調度的核心,負責SQL語句的生成和查詢緩存的維護
StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設置參數、將Statement結果集轉換成List集合。
ParameterHandler 負責對用戶傳遞的參數轉換成JDBC Statement 所需要的參數,
ResultSetHandler 負責將JDBC返回的ResultSet結果集對象轉換成List類型的集合;
TypeHandler 負責java數據類型和jdbc數據類型之間的映射和轉換
MappedStatement MappedStatement維護了一條

xml啓動

mybatis 的啓動單獨使用時通過一個核心配置xml + n個mapper.xml 的 
與 springboot 集成的時候,通常只剩下 n個mapper.xml 了(不用xml也行,但是通常還是會使用xml來定義複雜一點的語句)。 
我們還是從xml分析

基礎概念 
1. SqlSessionFactory 
SqlSession 工廠,全局唯一(單數據源的情況下),啓動實際上就是構建 SqlSessionFactory(但是配置信息全部在 Configuration) 
2. SqlSession 
跟數據庫交互的操作都是基於這個類(即使是 Dao 接口,也是 getMapper 獲取到的動態代理) 
3. Configuration 
核心配置類,所有 mybatis 相關的配置和解析出來的信息都在這裏

入門 
使用xml
 

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

不使用xml

DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration)

上面就是啓動,獲取 SqlSessionFactory 實例後就可以繼續獲取 SqlSession 進行數據操作了 

數據操作

SqlSession session = sqlSessionFactory.openSession();
try {
  Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
  session.close();
}

SqlSession session = sqlSessionFactory.openSession();
try {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
} finally {
  session.close();
}

上面兩種操作方式,通常使用的是第二種,並且這部分操作都會進行封裝(跟spring 集成的時候),在業務裏面直接獲取 Dao 接口進行操作(不需要實現類).

入口分析從 SqlSessionFactoryBuilder 開始

SqlSessionFactoryBuilder

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

重點就是這個方法,還有一個重載方法,參數類型是 InputStream,根據 api 構建,就可以猜到 XMLConfigBuilder 經過 parse 後,就是 Configuration 實例了

獲取 Configuration 實例後

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

// 構建 SqlSessionFactory ,只是將 Configuration 對象傳過來的
public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
}

可以看到 SqlSessionFactory 的實現就是 DefaultSqlSessionFactory

接下來我們看看解析細節

XMLConfigBuilder

這裏的就是就是核心配置文件 
示例

<?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>
  <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="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

// 當parse 完成後,返回 configuration,這個類是核心類,有所有的信息
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

我們不去細看xml解析語法(在構造這個實例的時候就已經解析成一個 Dom 對象了) 
這裏重要的是 parseConfiguration(parser.evalNode("/configuration")); ,直接就能猜到是解析 configuration 節點

// 解析 xml 配置文件
  // 解析完成後,所有的信息都在 Configuration 對象上
  private void parseConfiguration(XNode root) {
    try {
      /**
       * 解析順序,所以配置文件的順序,通常來說也是這樣的
       * 1. properties 自定義配置項,resource url 可以設置外置的 properties 文件,也可以在子元素裏面設置,配置將用在接下來的解析,也會設置在 configuration 類裏面(configuration是核心類,所有的東西最終都在這裏)
       * 2. settings 系統配置項,一些開關,全局配置之類的
       * 3. typeAliases
       * 4. plugins
       * 5. objectFactory
       * 6. objectWrapperFactory
       * 7. reflectorFactory
       * 8. environments
       * 9. databaseIdProvider
       * 10. typeHandlers
       * 11. mappers
       */
      //issue #117 read properties first
      // 解析 properties
      /**
       * <properties resource="org/mybatis/example/config.properties">
       *   <property name="username" value="dev_user"/>
       *   <property name="password" value="F2Fa3!33TYyg"/>
       * </properties>
       */
      propertiesElement(root.evalNode("properties"));
      // 解析settings
      /**
       * <settings>
       *   <setting name="cacheEnabled" value="true"/>
       *   <setting name="lazyLoadingEnabled" value="true"/>
       *   <setting name="multipleResultSetsEnabled" value="true"/>
       *   <setting name="useColumnLabel" value="true"/>
       *   <setting name="useGeneratedKeys" value="false"/>
       *   <setting name="autoMappingBehavior" value="PARTIAL"/>
       *   <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
       *   <setting name="defaultExecutorType" value="SIMPLE"/>
       *   <setting name="defaultStatementTimeout" value="25"/>
       *   <setting name="defaultFetchSize" value="100"/>
       *   <setting name="safeRowBoundsEnabled" value="false"/>
       *   <setting name="mapUnderscoreToCamelCase" value="false"/>
       *   <setting name="localCacheScope" value="SESSION"/>
       *   <setting name="jdbcTypeForNull" value="OTHER"/>
       *   <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
       * </settings>
       */
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      // 看看 settings 裏面是否設置類 Vfs 實現類(大部分不需要管,只是部署在特殊的容器裏面可能需要擴展,VFS 就是在不同容器裏面獲取資源的方式和路徑不同)
      loadCustomVfs(settings);
      // 別名設置,這個經常使用,用簡單類名當做全類名的別名
      // 可以直接指定 package   <package name="domain.blog"/>
      // 也可以一個一個指定 <typeAlias alias="Tag" type="domain.blog.Tag"/>
      typeAliasesElement(root.evalNode("typeAliases"));
      // 解析插件,插件是用來擴展 mybatis 的地方,比如分頁插件等
      // 註冊的插件添加到 configuration 上,注意,這裏的 property 不能用上面的 properties 裏面定義的
      /**
       * <plugins>
       *   <plugin interceptor="org.mybatis.example.ExamplePlugin">
       *     <property name="someProperty" value="100"/>
       *   </plugin>
       * </plugins>
       */
      pluginElement(root.evalNode("plugins"));
      // 解析objectFactory
      // MyBatis 每次創建結果對象的新實例時,它都會使用一個對象工廠(ObjectFactory)實例來完成。
      // 默認的對象工廠需要做的僅僅是實例化目標類,要麼通過默認構造方法,要麼在參數映射存在的時候通過參數構造方法來實例化。
      // 如果想覆蓋對象工廠的默認行爲,則可以通過創建自己的對象工廠來實現
      /**
       * <objectFactory type="org.mybatis.example.ExampleObjectFactory">
       *   <property name="someProperty" value="100"/>
       * </objectFactory>
       */
      objectFactoryElement(root.evalNode("objectFactory"));
      // 解析 objectWrapperFactory
      // 這個類就是用來包裝和判斷對象是否包裝過,默認是沒有包裝的,後面看看這裏有在那裏
      /**
       * <objectWrapperFactory type="org..."></objectWrapperFactory>
       */
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      // 解析 reflectorFactory
      // 就是用來包裝 Class 對象,方便反射調用,默認即可
      /**
       * <reflectorFactory type="org..."></reflectorFactory>
       */
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // 這裏設置 settings ,沒有就取默認值
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 解析 environments
      // 這裏配置事物,數據源
      // property 可以直接填直接值,可以使用上面properties 加載的
      // 注意,可以使用多個 environment,用在不同環境,但是隻有 default 指定的環境生效
      /**
       * <environments default="development">
       *   <environment id="development">
       *     <transactionManager type="JDBC">
       *       <property name="..." value="..."/>
       *     </transactionManager>
       *     <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>
       */
      environmentsElement(root.evalNode("environments"));
      // 解析 databaseIdProvider
      //MyBatis 可以根據不同的數據庫廠商執行不同的語句,這種多廠商的支持是基於映射語句中的 databaseId 屬性。
      // MyBatis 會加載不帶 databaseId 屬性和帶有匹配當前數據庫 databaseId 屬性的所有語句。
      // 如果同時找到帶有 databaseId 和不帶 databaseId 的相同語句,則後者會被捨棄
      /**
       * <databaseIdProvider type="DB_VENDOR">
       *   <property name="SQL Server" value="sqlserver"/>
       *   <property name="DB2" value="db2"/>
       *   <property name="Oracle" value="oracle" />
       * </databaseIdProvider>
       */
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // 類型處理
      // 可以一個一個指定,也可以指定一個package
      // 要有 註解標記 @MappedJdbcTypes(JdbcType.VARCHAR)
      // 這裏是 java 類型和 數據庫類型轉換的類
      /**
       *<typeHandlers>
       *   <package name="org.mybatis.example"/>
       * </typeHandlers>
       *
       * <typeHandlers>
       *   <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
       * </typeHandlers>
       */
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析 mappers
      /**
       * 既然 MyBatis 的行爲已經由上述元素配置完了,我們現在就要定義 SQL 映射語句了。但是首先我們需要告訴 MyBatis 到哪裏去找到這些語句。
       * Java 在自動查找這方面沒有提供一個很好的方法,所以最佳的方式是告訴 MyBatis 到哪裏去找映射文件。
       * 你可以使用相對於類路徑的資源引用, 或完全限定資源定位符
       */
      /**
       * <!-- 使用相對於類路徑的資源引用 -->
       * <mappers>
       *   <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
       *   <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
       *   <mapper resource="org/mybatis/builder/PostMapper.xml"/>
       * </mappers>
       *
       * <!-- 使用完全限定資源定位符(URL) -->
       * <mappers>
       *   <mapper url="file:///var/mappers/AuthorMapper.xml"/>
       *   <mapper url="file:///var/mappers/BlogMapper.xml"/>
       *   <mapper url="file:///var/mappers/PostMapper.xml"/>
       * </mappers>
       *
       * <!-- 使用映射器接口實現類的完全限定類名 -->
       * <mappers>
       *   <mapper class="org.mybatis.builder.AuthorMapper"/>
       *   <mapper class="org.mybatis.builder.BlogMapper"/>
       *   <mapper class="org.mybatis.builder.PostMapper"/>
       * </mappers>
       *
       * <!-- 將包內的映射器接口實現全部註冊爲映射器 -->
       * <mappers>
       *   <package name="org.mybatis.builder"/>
       * </mappers>
       */
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

這上面已經有註釋了,除了解析 mappers 裏面還要做特別處理外,其他的解析都不復雜,不繼續向下看。 
這裏面,我們用的多的就是 別名,類型處理,全局設置,自定義配置,環境(事務管理和數據源配置),如果多數據庫支持那麼可以使用數據庫標識(databaseId),如果需要做擴展(分頁)可以註冊 
插件(插件的實現原理我們後面具體分析)。 
所有的解析都是將xml配置信息丟到 Configuration 實例裏面。

接下來我們具體講一下 mappers 的解析(sql 執行相關)
 

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          // package 的直接 Mapper
          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) {
            // 解析 mapper 的 xml
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            // 同上,不過是根據url獲取
            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) {
            // 定義的接口,那就直接 addMapper
            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.");
          }
        }
      }
    }
  }

mappers 裏面我們可以直接指定包名(該包下的接口註冊),單接口註冊(class),或者 mapper.xml 文件路徑(即使註冊了接口,我們通常還是會寫這個文件,註冊接口後 
mybatis也會默認找一下同名mapper.xml 文件) 
其實這裏主要分爲 xml 解析 和 接口註冊,我們先看xml解析過程(不詳細分析) 
順帶說一句 ErrorContext 這個類是記錄解析或者執行的信息,等出席異常的時候方便定爲問題。

xml
 

/ 構造時候的 xml 解析,忽略,只要知道要進行xml格式檢查以及最終的 dom 節點就行了。
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
        configuration, resource, sqlFragments);
  }

  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
  }

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 解析 xml
      configurationElement(parser.evalNode("/mapper"));
      // 標記爲已經解析過的資源
      configuration.addLoadedResource(resource);
      // 這裏嘗試根據 namespace 找到對應接口,註冊 Mapper,所以,namespace 建議跟對應接口名保持一致,id 跟方法名保持一致
      bindMapperForNamespace();
    }

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

解析的時候先解析xml,如果已經解析過了忽略

private void configurationElement(XNode context) {
    try {
      // namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 設置當前 namespace
      builderAssistant.setCurrentNamespace(namespace);
      // 解析對應標籤
      // 公用一個緩存對象的標籤
      cacheRefElement(context.evalNode("cache-ref"));
      // mapper 的緩存設置
      cacheElement(context.evalNode("cache"));
      // 參數map,這個可能以後會廢棄,就不看了
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 結果集map,這個經常用到,特別是自定義返回
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // sql 標籤,公用的sql語句
      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);
    }
  }

緩存相關的2個解析很簡單
 

private void cacheRefElement(XNode context) {
    if (context != null) {
      // 先配置類添加這個配置
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
        // 這裏的解析,只是看看引用的namespace 在前面解析沒(就是有對應namespace的緩存配置不),如果沒解析那就添加一些未解析,等後面解析
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
  }

  private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      // type 可以是別名(內建的cache),也可以是cache類全名(自定義cache可以用)
      String type = context.getStringAttribute("type", "PERPETUAL");
      // 根據別名獲取class,如果存在,就返回對應 class,如果不存在,就加載類名
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      // 回收策略,這裏的 Cache 是對上面的包裝
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      // 刷新間隔
      Long flushInterval = context.getLongAttribute("flushInterval");
      // 對象數量
      Integer size = context.getIntAttribute("size");
      // 只讀
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      // 阻塞
      boolean blocking = context.getBooleanAttribute("blocking", false);
      // 其他自定義屬性
      Properties props = context.getChildrenAsProperties();
      // 添加緩存實例到 配置中
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

所謂 cacheRef 代表使用另一個mapper 裏面的緩存配置(公用一個) 
cache 表示本mapper 使用的配置(所以,當開啓了二級緩存的時候,對應mapper裏面也要配置才起作用)

parameterMap 標籤用的比較少,就不看了 
sql 標籤是用來寫公用sql 的(譬如表裏面的字段名) 
resultMap 定義返回映射(常用,特別是複雜映射) 
然後就是增刪改查解析

resultMap 解析出來的是 ResultMapping list,其中每一個字段的映射都對應一個 ResultMapping 實例,如果有嵌套,那麼 ResultMapping 是有組合的 
具體解析過程就不看了,解析小複雜,過於細節,跳過。

sql 標籤,解析出來的是可以共用的 sql 片段,比較簡單,跳過 
重點在實際sql解析 增刪改查
 

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    // 數據庫不匹配,不解析
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    // 解析屬性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // 這裏默認的是 Xml langDriver 獲取 sqlSource
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 解析出來的是 MappedStatement ,key 是 namespace
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

不深入分析細節,觀察整個流程

解析了看看 Mapper 的註冊(接口)

接口

// 定義的接口,那就直接 addMapper
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

這裏,只能註冊接口,不能重複註冊,添加的是 MapperProxyFactory,到 getMapper 的時候返回的是代理,這個後面分析。 
然後解析 MapperAnnotationBuilder,解析還是解析 SqlSource 和 相關屬性的解析

public void parse() {
    // 註解解析
    String resource = type.toString();
    // 如果對應接口的xml資源未解析(同名。xml,非必須),先去解析
    if (!configuration.isResourceLoaded(resource)) {
      // 加載xml,可能沒這個資源,無所謂
      loadXmlResource();
      // 設置爲已經加載的資源
      configuration.addLoadedResource(resource);
      // 設置當前解析的 namespace
      assistant.setCurrentNamespace(type.getName());
      // 解析緩存註解配置 @CacheNamespace
      parseCache();
      // 解析共享緩存註解配置 @CacheNamespaceRef
      parseCacheRef();
      // 每個方法單獨解析
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          // 泛型擦除的原因,要過濾掉橋接方法
          // 什麼是泛型擦除,就是 <T> 如果指定泛型 String,實際上會有兩個方法 一個 Object 一個 String ,Object 那個方法就是橋接方法
          if (!method.isBridge()) {
            // 解析方法上的註解了
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

void parseStatement(Method method) {
    // 獲取參數類型, RowBounds ResultHandler 相當於系統參數,排除這兩個類型的參數,如果是多參數,那麼就是內部的 ParamMap 包裝,否則就是該參數類型
    Class<?> parameterTypeClass = getParameterType(method);
    // 這個通常不會該,默認就是 xml  PS:什麼是 LanguageDriver,mybatis 的本質就是將sql寫在文本,然後加載成 SqlSource,默認是寫在xml,也可以寫 Velocity 模板,或者原生 sql
    LanguageDriver languageDriver = getLanguageDriver(method);
    // 這裏就是解析 Sql 了,通過解析註解
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      // 設置參數(系統參數)
      Options options = method.getAnnotation(Options.class);
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = null;
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        // 字段生成
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        } else {
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      // 設置參數
      if (options != null) {
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        resultSetType = options.resultSetType();
      }

      String resultMapId = null;
      // resultMap
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        String[] resultMaps = resultMapAnnotation.value();
        StringBuilder sb = new StringBuilder();
        for (String resultMap : resultMaps) {
          if (sb.length() > 0) {
            sb.append(",");
          }
          sb.append(resultMap);
        }
        resultMapId = sb.toString();
      } else if (isSelect) {
        // 生成 resultMap
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

注意一點,就是無論 xml 還是 註解,解析完成後都會有一個 MapperedStatement 對象(裏面包含 SqlSource,代表定義的sql原始信息)

這裏其實就解析完成了,理一理流程

SqlSessionFactoryBuilder 使用 Congiguration 構建
解析核心配置,設置到 Configuration 實例上(自定義配置,全局配置,別名,擴展配置,對象工廠,反射工廠,環境,數據庫標識,類型處理,mappers)
解析mappers獲得 MappedStatement 對象,裏面包含 SqlSource 對象和其他一些配置信息
註冊 Mapper 接口,註冊使用的是 MapperProxyFactory,還是會解析 MappedStatement 對象(每個方法上的註解),同時解析同名 mapper.xml 文件
其中 SqlSource 對象是根據 LanguageDriver 創建的,默認的是 XMLLanguageDriver 實現,通常這個也不會去修改
參考:https://blog.csdn.net/luolei1994/article/details/81056759#mybatis

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章