Mybatis原始碼簡單解讀----構建

馬+發表於2020-11-13

Mybatis原始碼簡單解讀—構建

參考原始碼:Github搜尋mybatis原始碼第一個,中文註釋
同時maven下載mybatis較新版本的原始碼對照閱讀
參考部落格:https://www.cnblogs.com/javazhiyin/p/12340498.html
其他的小知識點也借鑑了很多其他部落格的內容

首先mybatis的工作流程主要分為兩個部分:

  • 構建(解析xml和註解,對映成物件形成配置類)
  • 執行(執行sql,完成jdbc與資料庫互動)

這一部分只講解mybatis的構建
單獨使用mybatis框架的時候,會需要兩個配置檔案,分別是mybatis-config.xml和mapper.xml,採用官網給出案例mybatis的xml

我們不難看出,在mybatis-config.xml這個檔案主要是用於配置資料來源、配置別名、載入mapper.xml,並且我們可以看到這個檔案的<mappers>節點中包含了一個<mapper>,而這個mapper所指向的路徑就是另外一個xml檔案:DemoMapper.xml,而這個檔案中寫了我們查詢資料庫所用的SQL。

而,MyBatis實際上就是將這兩個xml檔案,解析成配置物件,在執行中去使用它。

xml配置:

<?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>
<?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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

使用mybatis,呼叫程式碼如下:

public static void main(String[] args) throws Exception {
    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //建立SqlSessionFacory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    /******************************分割線******************************/
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //獲取Mapper
    try (SqlSession session = sqlSessionFactory.openSession()) {
      BlogMapper mapper = session.getMapper(BlogMapper.class);
      Blog blog = mapper.selectBlog(101);
	}
    sqlSession.commit();
  }

這段程式顯示通過位元組流讀取了mybatis-config.xml檔案,然後通過SqlSessionFactoryBuilder.build()方法,建立了一個SqlSessionFactory(這裡用到了工廠模式和構建者模式),前面說過,MyBatis就是通過我們寫的xml配置檔案,來構建配置物件的,那麼配置檔案所在的地方,就一定是構建開始的地方,也就是build方法。

進入build方法


建立XMLConfigBuilder解析配置資訊,之後呼叫parser()解析返回Configuration物件,呼叫build方法來建立SqlSessionFactoryfactory介面的預設實現DefaultSqlSessionFactory

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //建立出  XMLConfigBuilder
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //解析xml檔案分析
      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.
      }
    }
  }
  //build()方法--構建者模式建立工廠物件
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

XMLConfigBuilder類

建立XmlConfigbuilder物件,mybaitis中的構建起都繼承了一個BaseBuilder的類,該類維護了三個變數

  • Configuration 配置類–主配置
  • TypeAliasRegistry 型別別名註冊器
  • TypeHandlerRegistry 型別處理器註冊器(用於型別轉換)

configuration實際上就是一個維護了mybatis配置資訊和執行相關的執行器和處理器的配置類(配置資訊對映關係)

//上面6個建構函式最後都合流到這個函式,傳入XPathParser
  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    //首先呼叫父類初始化Configuration
    super(new Configuration());
    //錯誤上下文設定成SQL Mapper Configuration(XML檔案配置),以便後面出錯了報錯用吧
    ErrorContext.instance().resource("SQL Mapper Configuration");
    //將Properties全部設定到Configuration裡面去
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

構建該類初始化了Configuration,用於後面解析配置類解析完注入,特別是多個繼承了BaseBuilder的構建器都用來操作該Configuration

初始化完,呼叫parse()方法

//解析配置
  public Configuration parse() {
    //如果已經解析過了,報錯
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
//  <?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>
    
    //根節點是configuration
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

該配置解析了主配置類中的configuration節點

xml解析使用了java提供的xml解析工具,mybatis進一步封裝了相關的解析方法,原理就是解析xml語法樹


初始化生成Configuration流程圖

在這裡插入圖片描述

進一步檢視parser.evalNode("/configuration")

該方法分步驟解析根節點下的配置資訊(使用 Xpath解析器來解析)

//解析配置
  private void parseConfiguration(XNode root) {
    try {
      //分步驟解析
      //issue #117 read properties first
      //1.properties
      propertiesElement(root.evalNode("properties"));
      //2.型別別名
      typeAliasesElement(root.evalNode("typeAliases"));
      //3.外掛
      pluginElement(root.evalNode("plugins"));
      //4.物件工廠
      objectFactoryElement(root.evalNode("objectFactory"));
      //5.物件包裝工廠
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //6.設定
      settingsElement(root.evalNode("settings"));
      // read it after objectFactory and objectWrapperFactory issue #631
      //7.環境
      environmentsElement(root.evalNode("environments"));
      //8.databaseIdProvider
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //9.型別處理器
      typeHandlerElement(root.evalNode("typeHandlers"));
      //10.對映器
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

解析對映器Mappers

其他部分程式碼不是主要了解內容,我們主要了解mapper對映器以及對映器對應xxxMapper.xml檔案時是如何被解析的

跟蹤對映器部分程式碼
mapperElement(root.evalNode(“mappers”));

首先我們先了解對映器的幾種配置,檢視官方文件可知有如下配置:

<!-- 使用相對於類路徑的資源引用 -->
<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()方法中也分別解析了下面幾種配置方法

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        //10.4自動掃描包下所有對映器
        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) {
          //10.1使用類路徑
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          //對映器比較複雜,呼叫XMLMapperBuilder
          //注意在for迴圈裡每個mapper都重新new一個XMLMapperBuilder,來解析
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          //10.2使用絕對url路徑
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          //對映器比較複雜,呼叫XMLMapperBuilder
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          //10.3使用java類名
          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.");
        }
      }
    }
  }
}

分析上面程式碼可以看出:

  • 對映器配置 使用類介面的-----直接呼叫addMapper()方法
  • 對映器配置 使用xxxMapper.xml則呼叫XmlMapper來解析

分別分析這兩種方式的

Mapper配置使用類介面

跟蹤原始碼

MapperRegistry

public void addMappers(String packageName) {
  mapperRegistry.addMappers(packageName);
}
//查詢包下所有是superType的類
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
  addMapper(mapperClass);
}
//看一下如何新增一個對映
public <T> void addMapper(Class<T> type) {
    //mapper必須是介面!才會新增
    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));
       //在執行解析器之前新增型別很重要
	   //否則,對映器解析器可能會自動嘗試繫結。 如果型別是已知的,則不會嘗試。
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果載入過程中出現異常需要再將這個mapper從mybatis中刪除,這種方式比較醜陋吧,難道是不得已而為之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

從這裡發現對映器的關係通過MapperRegistry序號產生器來維護,該類維護了配置類和對映關係map,

public class MapperRegistry {

  private Configuration config;
  //將已經新增的對映都放入HashMap
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

  public MapperRegistry(Configuration config) {
    this.config = config;
  }
  ...
}

將類加入序號產生器後,使用MapperAnnotationBuilder 來解析parse()

檢視該方法

public void parse() {
  //type即介面類,resource為該類全路徑
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    //解析對應xxxMapper.xml,並標識為已載入了
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    //快取
    parseCache();
    parseCacheRef();
    //解析類上的介面
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        // 如果不是橋接方法
        if (!method.isBridge()) {
          //解析method中的sql註解  
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {  
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      } 
    }
  }
  parsePendingMethods();
}

Method.isBrige()橋接方法

檢視loadXmlResource

private void loadXmlResource() {
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
    //查詢同包下的xml檔案,並解析  
    String xmlResource = type.getName().replace('.', '/') + ".xml";
    InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
    if (inputStream == null) {
      // Search XML mapper that is not in the module but in the classpath.
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e2) {
        // ignore, resource is not required
      }
    }
    if (inputStream != null) {
      //構建xml解析工具來解析  
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
      xmlParser.parse();
    }
  }
}

程式碼分析到這裡,我們可以看出大概的流程:

  1. 將該類介面注入MapperRegistry中,通過Map<Class<?>, MapperProxyFactory<?>> knownMappers集合來維護,把類介面和新生成的MapperProxyFactory代理工廠作為鍵值對
  2. 解析該介面類下的xml檔案
  3. 解析類介面上的註解

Mapper配置使用xml檔案

xml檔案的方式首先通過XMLMapperBuilder解析xml檔案【該類也在介面方式中呼叫】

這裡跟蹤下XMLMapperBuilder是如何解析資訊的

public void parse() {
    //判斷檔案是否之前解析過
    if (!configuration.isResourceLoaded(resource)) {
        //解析mapper檔案節點(主要)(下面貼了程式碼)
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //繫結Namespace裡面的Class物件---實際上就是新增到MapperRegistry中
      bindMapperForNamespace();
    }
    //重新解析之前解析不了的節點
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }


//解析mapper檔案裡面的節點
// 拿到裡面配置的配置項 最終封裝成一個MapperedStatemanet
private void configurationElement(XNode context) {
  try {
      //獲取名稱空間 namespace,這個很重要,後期mybatis會通過這個動態代理我們的Mapper介面
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
        //如果namespace為空則拋一個異常
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    //解析快取節點
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));

    //解析parameterMap(過時)和resultMap  <resultMap></resultMap>
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    //解析<sql>節點 
    //<sql id="staticSql">select * from test</sql> (可重用的程式碼段)
    //<select> <include refid="staticSql"></select>
    sqlElement(context.evalNodes("/mapper/sql"));
    //解析增刪改查節點<select> <insert> <update> <delete>
    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);
  }
}

在這個parse()方法中,呼叫了一個configuationElement程式碼,用於解析XXXMapper.xml檔案中的各種節點,包括<cache><cache-ref><paramaterMap>(已過時)、<resultMap><sql>、還有增刪改查節點,和上面相同的是,我們也挑一個主要的來說,因為解析過程都大同小異。

同時將xml檔案上namespace對應的介面註冊到MappperRegistry序號產生器中。

這裡我們介紹下增刪改查節點的方法——buildStatementFromContext(),和JDBC一樣該Statement就是運算元據庫的物件

  //7.配置select|insert|update|delete
  private void buildStatementFromContext(List<XNode> list) {
    //呼叫7.1構建語句
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  //7.1構建語句
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      //構建所有語句,一個mapper下可以有很多select
      //語句比較複雜,核心都在這裡面,所以呼叫XMLStatementBuilder
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
          //核心XMLStatementBuilder.parseStatementNode--這部分和類介面中解析sql註解的邏輯一樣
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
          //如果出現SQL語句不完整,把它記下來,塞到configuration去
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }


public void parseStatementNode() {
    //獲取<select id="xxx">中的id
    String id = context.getStringAttribute("id");
    //獲取databaseId 用於多資料庫,這裡為null
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    //獲取節點名  select update delete insert
    String nodeName = context.getNode().getNodeName();
    //根據節點名,得到SQL操作的型別
    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);
    //是否需要處理巢狀查詢結果 group by

    // 三組資料 分成一個巢狀的查詢結果
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    //替換Includes標籤為對應的sql標籤裡面的值
    includeParser.applyIncludes(context.getNode());

    //獲取parameterType名
    String parameterType = context.getStringAttribute("parameterType");
    //獲取parameterType的Class
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //解析配置的自定義指令碼語言驅動 這裡為null
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

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

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //設定主鍵自增規則
    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;
    }
/************************************************************************************/
    //解析Sql(重要)  根據sql文字來判斷是否需要動態解析 如果沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?佔位 當有 ${} 不解析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //獲取StatementType,可以理解為Statement和PreparedStatement
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //沒用過
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超時時間
    Integer timeout = context.getIntAttribute("timeout");
    //已過時
    String parameterMap = context.getStringAttribute("parameterMap");
    //獲取返回值型別名
    String resultType = context.getStringAttribute("resultType");
    //獲取返回值烈性的Class
    Class<?> resultTypeClass = resolveClass(resultType);
    //獲取resultMap的id
    String resultMap = context.getStringAttribute("resultMap");
    //獲取結果集型別
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    //將剛才獲取到的屬性,封裝成MappedStatement物件(程式碼貼在下面)
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

//將剛才獲取到的屬性,封裝成MappedStatement物件
  public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    //id = namespace
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

      //通過構造者模式+鏈式變成,構造一個MappedStatement的構造者
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

      //通過構造者構造MappedStatement
    MappedStatement statement = statementBuilder.build();
     //將MappedStatement物件封裝到Configuration物件中
    configuration.addMappedStatement(statement);
    return statement;
  }

將xml中的節點解析,並封裝一個MappedStatement物件,並新增在Configuration中和Map集合中

構建過程流程圖:
在這裡插入圖片描述

SQL語句解析

在剛才過程中包含了SQL語句的生成,在這裡進一步分析

//解析Sql(重要)  根據sql文字來判斷是否需要動態解析 如果沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?佔位 當有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

這裡就是生成Sql的入口,以單步除錯的角度接著往下看。

/*進入createSqlSource方法*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    //進入這個構造
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    //進入parseScriptNode
    return builder.parseScriptNode();
}
/**
進入這個方法
*/
public SqlSource parseScriptNode() {
    //#
    //會先解析一遍
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      //如果是${}會直接不解析,等待執行的時候直接賦值
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      //用佔位符方式來解析  #{} --> ?
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    //獲取select標籤下的子標籤
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
          //如果是查詢
        //獲取原生SQL語句 這裡是 select * from test where id = #{id}
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //檢查sql是否是${}
        if (textSqlNode.isDynamic()) {
            //如果是${}那麼直接不解析
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
            //如果不是,則直接生成靜態SQL
            //#{} -> ?
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
          //如果是增刪改
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }
/*從上面的程式碼段到這一段中間需要經過很多程式碼,就不一段一段貼了*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //這裡會生成一個GenericTokenParser,傳入#{}作為開始和結束,然後呼叫其parse方法,即可將#{}換為 ?
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    //這裡可以解析#{} 將其替換為?
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

//經過一段複雜的解析過程
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    //遍歷裡面所有的#{} select ?  ,#{id1} ${}
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
            //使用佔位符 ?
            //注意handler.handleToken()方法,這個方法是核心
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}

//BindingTokenParser 的handleToken
//當掃描到${}的時候呼叫此方法  其實就是不解析 在執行時候在替換成具體的值
@Override
public String handleToken(String content) {
  this.isDynamic = true;
  return null;
}
//ParameterMappingTokenHandler的handleToken
//全域性掃描#{id} 字串之後  會把裡面所有 #{} 呼叫handleToken 替換為?
@Override
public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
}

首先這裡會通過<select>節點獲取到我們的SQL語句,假設SQL語句中只有${},那麼直接就什麼都不做,在執行的時候直接進行賦值。

而如果掃描到了#{}字串之後,會進行替換,將#{}替換為 ?

那麼他是怎麼進行判斷的呢?

這裡會生成一個GenericTokenParser,這個物件可以傳入一個openToken和closeToken,如果是#{},那麼openToken就是#{,closeToken就是 },然後通過parse方法中的handler.handleToken()方法進行替換。

在這之前由於已經進行過SQL是否含有#{}的判斷了,所以在這裡如果是隻有${},那麼handler就是BindingTokenParser的例項化物件,如果存在#{},那麼handler就是ParameterMappingTokenHandler的例項化物件。

分別進行處理。

小結

至此整個MyBatis的查詢前構建的過程就基本說完了,簡單地總結就是,MyBatis會在執行查詢之前,對配置檔案進行解析成配置物件:Configuration,以便在後面執行的時候去使用,而存放SQL的xml又會解析成MappedStatement物件,但是最終這個物件也會加入Configuration中,將Configuration物件通過build()方法來建立工廠物件

相關文章