天天看點

Mybatis源碼-解析Mapper

在上一篇 Mybatis源碼-解析配置檔案 文章中介紹了配置檔案中的其他節點,本篇将介紹Mybatis解析Mapper。當解析到mappers的節點時,就能擷取到Maper檔案,進入到mapperElement()方法中開始解析mapper檔案

// 通過解析 mappers 節點,找到Mapper檔案
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 檢視mappers節點中是否有 package 節點,有就解析,否則就解析 mapper子節點
        /*
         <mappers>
          <package name="com.test"/>
          <!-- <mapper resource="mapper/DemoMapper.xml"/> -->
        </mappers>
         */
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          // 解析 mapper 節點,mapper 節點中有三個屬性(resource、url、class),但是隻能存在一個
          /**
           * resource:表示檔案夾下xml檔案
           * <mapper resource="mapper/DemoMapper.xml"/>
           *
           * class:DemoMapper 動态代理接口,DemoMapper.xml檔案
           * <mapper class="com.test.DemoMapper" />
           *
           * url:表示盤符下的絕對路徑,絕對不推薦使用
           * <mapper resource="F:\javaEE\workspace\MyBatisDemo_My\mybatis-3-master\src\main\resources\mapper\DemoMapper.xml"/>
           *
           */
          /*
         <mappers>
          <!-- <package name="com.test"/> -->
          <mapper resource="mapper/DemoMapper.xml"/>
        </mappers>
         */
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 三個屬性隻能是其中的一個有value,否則就抛異常
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 如果resource有值,就解析mapper.xml檔案
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            // 如果url有值,則通過絕對路徑擷取輸入流,擷取mapper.xml并解析
            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有值,就擷取Class對象
            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下的mapper子節點有三個屬性(resource、url、class),但是隻能存在一個,不推薦使用url,如果配置的是class屬性,就擷取Class對象。如果是resource或者url,就要找到這個mapper.xml檔案,開始解析這個mapper.xml檔案裡的節點,進入XMLMapperBuilder裡的parse()方法。

public void parse() {
    // 判斷mapper.xml檔案有沒有解析過,如果解析過了就不再解析
    if (!configuration.isResourceLoaded(resource)) {
      // 解析mapper.xml檔案裡的節點
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
           
// 解析mapper.xml檔案裡的節點
  // 将配置裡面的配置項封裝成一個MapperStatement對象
  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      // 解析cache節點(擷取cache節點裡的屬性)
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析動态sql節點
      /*
        <sql id="user_column">
          id, username,nickname,...
        </sql>
       */
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析增删改查節點
      /*
        <insert id=""/>
        <update id=""/>
        <delete id=""/>
        <select id=""/>
       */
      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);
    }
  }
           

一直往下走,進入到XMLStatementBuilder裡的parseStatementNode()方法。

// #{}和${}
  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    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);
    //是否需要處理嵌套查詢結果,如果有 group by
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    // 替換使用動态sql的時候使用的<include/>标簽
    includeParser.applyIncludes(context.getNode());
    String parameterType = context.getStringAttribute("parameterType");
    // 拿到參數類型的别名去擷取實際的參數類型對象
    Class<?> parameterTypeClass = resolveClass(parameterType);
    // 解析配置的自定義腳本語言的驅動,例如可以自定義的去解析sql語句中#{}或者${}的占位符格式
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    // Parse selectKey after includes and remove them.
    // 解析selectKey
    /*
      <insert id="">
        <selectKey></selectKey>
      </insert>
     */
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // 解析和删除<selectKey>和<include>
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    // id的自增規則
    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且隻有#{}的時候,就直接解析使用?替換#{},當有${}的時候就不解析
    // XMLLanguageDriver.createSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    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<?> resultTypeClass = resolveClass(resultType);
    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");
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
           

判斷mapper.xml檔案中的sql語句裡含有的是#{}或者KaTeX parse error: Expected 'EOF', got '#' at position 16: {},如果沒有動态sql且隻有#̲{}的時候,就直接解析使用?替…{}的時候就不解析,這裡需要進入XMLLanguageDriver的createSqlSource()方法裡對sql語句進行操作。

public SqlSource parseScriptNode() {
    // 動态解析标簽
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    // 如果sql文本中含有${,isDynamic的值就為true
    if (isDynamic) {
      // 不解析
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      // 用占位符的方式來解析(例如:将#{id}替換成?)
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
進入到parseDynamicTags()方法中。
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      // sql是純文字
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        // 判斷sql語句中是否有${},如果有就傳回true
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          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);
  }
           

繼續往下走到TestSqlNode裡的isDynamoic()方法,判斷sql中是否包含${}

public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    // 建立一個檢驗規則,裡面含有${}
    GenericTokenParser parser = createParser(checker);
    // 解析sql文本
    parser.parse(text);
    return checker.isDynamic();
  }
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    // 判斷sql文本中是否有${或者#{,沒有就傳回-1
    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;
    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 {
          // 如果sql文本中含有${,VariableTokenHandler.handleToken将${參數}傳回
          // 如果sql文本中含有${,DynamicCheckerTokenParser.handleToken将isDynamic置為true
          // 如果sql文本中含有${,BindingTokenParser.handleToken解析sql
          // 如果sql文本中含有#{,ParameterMappingTokenHandler.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();
  }
           

當解析完這一步後,mapper檔案差不多就解析完了,解析到的資料會被封裝到MapperStatement對象中,這個對象是被mybatis全局共享的。

當解析完mapper.xml檔案後,當調用mapper裡的方法時,它的執行流程又是怎麼樣的呢?先來看一個Mybatis的一個例子

Test類

public class TestMybatis {
  public static void main(String[] args) throws IOException {
    // 加載配置檔案
    InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
    // 解析xml檔案
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
    // 擷取SqlSession對象
    SqlSession sqlSession = sqlSessionFactory.openSession();
  
    // 通過動态代理,拿到DemoMapper的一個代理對象
    DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
   
    // 通過代理對象執行方法,就會跳到到MapperProxy的invoke方法中
    System.out.println(mapper.selectUser(1, "zhangsan"));

  }
}
           

DemoMapper接口

public interface DemoMapper {
  Map<String, Object> selectUser(@Param("id") int id, @Param("username") String username);
}
           

DemoMapper.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="com.test.DemoMapper">

  <!--  不使用@Param-->
  <!--<select id="selectUser" resultType="map">
    SELECT * FROM user WHERE id=#{arg0} AND username=#{arg1}
  </select>-->
  <select id="selectUser" resultType="map">
    SELECT * FROM user WHERE id=#{id} AND username=#{username}
  </select>

</mapper>
           

當解析完配置檔案,擷取到SqlSessionFacatory對象,通過SqlSessionFacatory對象擷取SqlSession對象,調用getMapper()方法,擷取到DemoMapper對象。但是DemoMapper是一個接口,是不能建立對象的,這裡其實是通過動态代理擷取到一個DemoMapper的子類對象。進入getMapper()方法中。

Mybatis源碼-解析Mapper

SqlSession有兩個子類DefaultSqlSession和SqlSessionmanager,點選進入到DefaultSqlSession中,一直進入到MapperRegistry類中的getMapper()方法。

Mybatis源碼-解析Mapper

這裡有個(MapperProxyFactory) knownMappers.get(type);方法,這個方法傳回的結果是一個MapperProxyFactory(代理工廠),在來看看knownMappers的定義

knownMappers是一個Map集合,既然這裡能get,那麼肯定就會在一個地方put,在MapperRegistry類裡的addMapper()方法裡進行了knownMappers.put()

Mybatis源碼-解析Mapper

addMaper()方法的調用者就需要回到之前配置檔案解析mappers節點的時候,前面已經分析了,解析配置檔案的各個配置的參數會被封裝到Configuration對象中,當解析mapper.xml檔案時,也會調用Configuration的addMapper()方法将mapper的資訊封裝到MapperRegistry中。在XMLMapperBuilder的mapperElement()方法裡可以找到addMapper()方法的調用。

Mybatis源碼-解析Mapper
Mybatis源碼-解析Mapper

繼續往下執行mapperProxyFactory.newInstance(sqlSession)方法,會發現一個MapperProxy對象,這個就是DemoMapper的代理對象,進入到MapperProxy裡面有一個invoke()方法,當DemoMapper調用它裡面的方法的時候,就會進入這invoke()方法。

// 代理方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 檢視這個方法是否是Object中的方法
      /*
        Object是所有類的父類,其它類都顯式或者隐式的繼承了這個類,
        如果子類中自己定義了方法或者重寫了父類中的方法(例如重寫了toString、equals等),那麼這些方法就不屬于Object了。
        隻有Object類中自己定義且沒有被子類重寫的方法,這裡的判斷才屬于Object中的方法
       */
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
        // 判斷是否是預設方法,在jdk1.8出現的,在接口中定義的:default void test();
        // 如果是預設方法,就直接綁定到代理對象上去執行,預設方法裡該是什麼邏輯就是什麼邏輯,就不進行代理(不執行sql)
      } else if (method.isDefault()) {
        if (privateLookupInMethod == null) {
          return invokeDefaultMethodJava8(proxy, method, args);
        } else {
          return invokeDefaultMethodJava9(proxy, method, args);
        }
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 将method需要的資訊封裝成一個MapperMethod,然後添加到緩存裡
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 執行CRUD操作
    return mapperMethod.execute(sqlSession, args);
  }
           

首先會判斷需要執行的方法是否是Object類裡的方法。Object是所有類的父類,其它類都顯式或者隐式的繼承了這個類,如果子類中自己定義了方法或者重寫了父類中的方法(例如重寫了toString、equals等),那麼這些方法就不屬于Object了。隻有Object類中自己定義且沒有被子類重寫的方法,這裡的判斷才屬于Object中的方法。

接着判斷是否是DemoMapper中的預設方法,在JDK8以後在接口中可以出現預設方法,使用default修飾的方法。

default String test() {
    return "hello world";
  }
           

如果是預設方法,就直接綁定到代理對象上去執行,預設方法裡該是什麼邏輯就是什麼邏輯,就不進行代理(不執行sql)。

如果該方法不是Object中的方法,也不是預設方法,就繼續往下執行,先去 Map<Method, MapperMethod> methodCache中查詢該method是否存在了,如果沒有就将method封裝成MapperMethod,然後緩存進mehtodCache中,最後将MapperMethod傳回。

往下就執行MapperMethod中的execute方法(其實就是執行CRUD操作),execute()方法下一篇會進行介紹。

公衆号二維碼

Mybatis源碼-解析Mapper

繼續閱讀