天天看點

Mybatis源碼解析《一》

導語

在目前的日常開發中,mybatis這樣的一個架構的使用,是很多程式員都無法避開的。大多數人都知道mybatis 的作用是為了避免了幾乎所有的 JDBC 代碼和手動設定參數以及擷取結果集。因為在開始接觸使用Java操作資料庫的時候,我們都是使用JDBC的。

自從有了持久化架構之後,使用持久化架構已經是“理所當然”的了,雖然我們已經脫離了使用JDBC是階段了,但是這畢竟是基礎的知識,是以本篇文章将會從JDBC入手。其實Mybatis就是對JDBC進行的封裝。那就言歸正傳吧!

一、原始JDBC的使用

廢話不多數,先來段代碼來說明問題:

public class TestMain {
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "zfy123456");
        connection.setAutoCommit(false);
        PreparedStatement ps = connection.prepareStatement("insert into dept values(?,?,?)");
        ps.setInt(1,10000);
        ps.setString(2,"test");
        ps.setString(3,"test");
        try{
            ps.executeUpdate();
        }catch (Exception e) {
            connection.rollback();
            e.printStackTrace();
        }finally {
            if(ps != null) {
                ps.close();
            }
            if (connection != null) {
                connection.close();
            }
        }


    }
}
           

對于上面的代碼中其大體流程就是:

  1. 加載驅動并進行初始化
  2. 連接配接資料庫
  3. 執行SQL語句
  4. 處理資料庫響應并傳回的結果
  5. 最後釋放資源

二、Mybatis操作資料庫

mybatis學習日常文檔:http://www.mybatis.org/mybatis-3/zh/index.html,以下代碼參考mybatis官網。

測試類:

public class MybatisTest {

    @Test
    public void test() throws Exception {

        User user = new User();
        user.setAddress("北京市海澱區");
        user.setBirthday(new Date(2000-10-01));
        user.setSex("男");
        user.setUsername("李清源");

        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        sqlSession.insert("insertUser", user);
        sqlSession.commit();
        sqlSession.close();

    }
}
           

實體類:

public class User {
    private int id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    // 省略get、set、toString方法
}
           

Mapper接口:

public interface UserMapper {
    void insertUser(User user) throws Exception;
}
           

Mapper配置檔案:

<?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.zfy.mybatis.mapper.UserMapper">
    <insert id="insertUser">
      insert into user(username,birthday,sex,address) values (#{username},#{birthday},#{sex},#{address})
    </insert>
</mapper>
           

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="config.properties"> </properties>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeAliases>
        <package name="com.zfy.mybatis.bean"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.zfy.mybatis.mapper"/>
    </mappers>
</configuration>
           

此處由于代碼太多就先省略config.properties配置檔案,網上可自己參考。從上面的測試類代碼中可以看出,mybatis的操作流程大體如下:

  • 讀取mybatis配置檔案
  • 使用SqlSessionFactoryBuilder中的build方法根據讀取到的檔案流資訊,建立Configuration對象,并将資料存儲在其中。
  • 再建立SqlSession對象提供屬性
  • 執行SQL
  • SqlSession.commit()
  • SqlSession.close()

三、mybatis核心配置檔案的加載

對于Resources.getResourceAsStream("mybatis-config.xml")代碼中,關于對配置檔案加載成輸入流的代碼,就不贅述了,直接來看SqlSessionFactoryBuilder中的build方法吧。來看看mybatis的核心配置檔案時如何被加載的。那就先來看下build方法的源碼:

SqlSessionFactoryBuilder.java
           
// 調用讀取流的方法入口,這裡的讀取流就是指向所建立的工程中的核心配置檔案
  public SqlSessionFactory build(InputStream inputStream) {
    // 調用重載的build方法,這三個參數的含義分别是:讀取的配置檔案的資訊、将要指定的環境、所要使用的web的屬性檔案,
    // 不過這裡後面兩個參數都是為null
    return build(inputStream, null, null);
  }
           
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 首先建立XML解析對象,這對象實際上是對XPathParser封裝的工具對象,這個對象主要是針對核心配置檔案進行相關讀取的
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // parser.parse()才是對XML進行真正的解析,解析完之後然後調用重載方法把parser對象放到DefaultSqlSessionFactory中去
      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.
      }
    }
  }
           

上面的代碼中,首先通過所傳入的inputStream, environment, properties這幾個參數建立XMLConfigBuilder 對象,然後嗲用這個對象中的parse()方法來進行解析,最後把解析完的對象放到DefaultSqlSessionFactory對象中去。代碼如下:

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

前面說到parser.parse()才是對mybatis核心配置檔案的解析,那麼繼續看這個方法的代碼到底做了什麼。代碼如下:

XMLConfigBuilder.java 
           
public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // parser.evalNode("/configuration")是為了定位核心配置檔案中'configuration'元素的節點(根目錄标簽)
    // 在獲得根标簽之後,然後對根标簽下的資訊進行解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
           

上面代碼中首先隻是做了一個防止多線程加載的操作,然後把parsed 設定為true,然後定位到mybatis核心配置檔案的根标簽configuration,在定位好根标簽後,再對其根标簽下的所有字标簽進行逐一的解析。最後傳回一個configuration對象。那來繼續看下parseConfiguration方法中是如何解析根标簽下的所有字标簽。這裡就先隻對mappers字标簽進行解析,對于上面開始的核心配置代碼中的properties、typeAliases、environments,就先不贅述了,否則本篇文章将會太長。先不多說,繼續看代碼:

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 對核心檔案中的各種标簽進行解析
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      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);
    }
  }
           

上面的代碼中就是對configuration根标簽下的所有字标簽進行解析的,這裡就以mappers标簽的解析為例。那來繼續看下 mapperElement(root.evalNode("mappers"))方法:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      // 因為mapper标簽中的子标簽存在兩種寫法,分别是:package、mapper
      for (XNode child : parent.getChildren()) {
        // 如果子标簽的存在"package"的名稱,則走此段代碼
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          // 在和擷取mapperPackage資訊後,然後把它添加到configuration對象中,其實mapperPackage就是目前檔案的路徑
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 在擷取到resource、url、mapperClass資訊之後,下面便對這些資訊是否存在進行判斷,然後走相應的邏輯代碼
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 當resource != null時,在擷取到對應的resource資訊,後然後放到建立的XMLMapperBuilder對象中
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 最後通過mapperParser對象去解析,這後面所做的一切工作就是把mapper檔案中的資訊解析出來後,然後放到configuration對象中去,為後續最準備
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            // 同上
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            // 同"package".equals(child.getName())的情況
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
}
           

這段代碼首先判斷parent != null的情況下,才執行後續操作,否則則什麼都不做。當不為null的情況下,便開始對mappers下的所有子标簽進行周遊并解析。後面的操作大體流程就是,如果子标簽的存在"package"的名稱,則執行相關代碼,否則進入else代碼,在else代碼塊中,先擷取esource、url、mapperClass,然後對各自是否為空的條件,執行相關代碼。具體看代碼中的注釋。因為開始所給的配置檔案中的mappers标簽下的子标簽是package,所有這裡我們就隻對這個邏輯下的代碼進行解析。

當子标簽是package時,先擷取其mapperPackage,然後放到mapper裡。這裡主要的工作在onfiguration.addMappers(mapperPackage),那就繼續看下這塊代碼。

Configuration.java 

public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }
           

MapperRegistry.java

public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }
           
public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    // 擷取此路徑下字尾為.lass 的檔案
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }
           

上面代碼中,先擷取字尾為.lass 的檔案,然後再把這些檔案資訊傳遞給Set,最後周遊Set,同時調用addMapper(mapperClass)方法。

public <T> void addMapper(Class<T> type) {
    // 判斷所擷取到的類類型是否為Interface類型
    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.
        // 将type和config資訊放到建立的MapperAnnotationBuilder對象中,config中主要包含environment這些資訊
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        // 然後繼續解析
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
           

上述代碼中,首先判斷鎖擷取到的類是否為接口類型的,如果是則先把loadCompleted 為false,然後以type為key放到knownMappers中去,後面再通過config和type建立MapperAnnotationBuilder,這裡這個對象是設計注解的,因為我們沒有用到注解,這裡就不贅述了。那就看下parser.parse()的代碼是如何做操作的。

public void parse() {
    String resource = type.toString();
    // 判斷configuration是否包含resource資訊
    if (!configuration.isResourceLoaded(resource)) {
      // 重點:這裡才真正的加載字尾為.xml檔案的資訊
      loadXmlResource();
      // 把resource添加到configuration中
      configuration.addLoadedResource(resource);
      // 設定MapperBuilderAssistant目前的namespace
      assistant.setCurrentNamespace(type.getName());
      // 解析緩存
      parseCache();
      // 解析緩存引用
      parseCacheRef();
      Method[] methods = type.getMethods();
      // 對接口中的方法進行解析
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    // 解析方法
    parsePendingMethods();
  }
           

上面代碼的邏輯很清晰,首先判斷configuration是否包含resource資訊,如果不包含,那麼就繼續後續的流程。當進入後續流程時,首先就是加載xml,這裡的就開始正式的加載mapper的xml了。然後再進行一些設定和解析緩存等一些東西。這裡主要看下loadXmlResource()這個方法:

private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    // 判斷configuration中是否包含了"namespace:" + type.getName())的mapper檔案
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      // 擷取到字尾資訊為.xml的檔案路徑
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      InputStream inputStream = null;
      try {
        // 擷取mapper檔案流
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e) {
        // ignore, resource is not required
      }
      if (inputStream != null) {
        // 如果流資訊不為空,把流資訊、assistant.getConfiguration()、xmlResource、configuration.getSqlFragments()和type的name放到XMLMapperBuilder中
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        // 然後進行解析
        xmlParser.parse();
      }
    }
  }
           

這裡也沒有什麼複雜的邏輯,無非還是先判斷下是否包含,然後擷取到字尾為.xml的檔案,當擷取到後再通過鎖擷取到的xmlResource和ype.getClassLoader()這兩個參數擷取檔案的輸入流,在獲得輸入流後,再通過輸入流等一些相關資訊,去建立一個xml的解析對象,建立完成後便通過此對象中的解析方法去解析,那就來看看這個方法:

public void parse() {
    // 判斷resource是否在configuration中
    if (!configuration.isResourceLoaded(resource)) {
      // 1.首先定位到mapper檔案中的根節點mapper,2.然後對該節點下的所有節點進行解析
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 綁定mapper到工作空間
      bindMapperForNamespace();
    }

    // 解析mapper檔案中ResultMaps節點下的資訊
    parsePendingResultMaps();
    // 解析緩存引用
    parsePendingCacheRefs();
    // 解析Statement
    parsePendingStatements();
  }
           

這段代碼很簡單,無非就是一些方法調用的邏輯,但這裡所要關注的重點還是configurationElement(parser.evalNode("/mapper"))這個方法,這裡便開始對于每個mapper檔案的正式調用了,來看看這個方法中具體做了什麼。代碼如下:

private void configurationElement(XNode context) {
    try {
      // 擷取mapper節點的namespace
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      // 接下來就是對mapper節點下的各種節點進行解析了,不準備贅述,但或許講下buildStatementFromContext方法
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      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);
    }
  }
           

上面代碼中,基本上就是對mapper配置檔案中的一些标簽的解析,由于我開始所提供的配置檔案中之涉及了<insert标簽,那麼在這裡還是隻關注這個标簽的解析代碼,請看代碼:

private void buildStatementFromContext(List<XNode> list) {
    // 如果configuration.getDatabaseId() != null,走此段代碼,否則跳過
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    // 上面的判斷是判斷是否存在其他資料源的設定,我們這裡沒有設定,是以就看這段代碼了
    buildStatementFromContext(list, null);
  }
           
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      // 建立XMLStatementBuilder對象,這個對象包含資訊有configuration(這個對象貌似無處不在)、builderAssistant(mapper resource這些重要資訊)、所讀取到的insert這些标簽的資訊(context)、
      // 最後就是資料庫的操作SQL資訊
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        // statement節點的解析
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }
           

上面的兩段代碼的邏輯還是很簡單的,還是隻是做一個簡單的判斷,符合相關條件就執行相關的邏輯代碼,buildStatementFromContext方法中所周遊的list的内容,其實就是"select|insert|update|delete"等标簽下的SQL模闆,然後再通過一些相關參數建立XMLStatementBuilder對象,建立對象之後再調用statementParser.parseStatementNode(),繼續看這個方法的代碼:

XMLStatementBuilder.java      
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();
    // 擷取SQL指令的類型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 判斷目前的SQL指令類型是否是select類型的
    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
    // 關于include标簽的解析
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    // 解析 selectKey 标簽
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // 解析 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;
    }

    // 上面所有所擷取到的資訊,都是在這裡使用的,到這裡select|insert|update|delete這些标簽的解析應該算是差不多了
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
           

這段代碼看上好像真的很複雜,因為這裡涉及到很多參數的擷取和标簽的解析,但是在這段代碼中現在所需要關注的隻是SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass)這行代碼,其他代碼功能請看注釋吧,有興趣的同學可以自己在去細看一下裡面的代碼。

XMLLanguageDriver.java
      
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }
           

這段代碼沒什麼說的,主要的還是builder.parseScriptNode()的調用。代碼如下:

XMLScriptBuilder.java
      
public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    // 判斷是否是動态SQL
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      // 不是動态SQL
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
           

這裡就是判斷所使用的是否是動态SQL,這裡并不是動态SQL,那就看非動态的邏輯代碼了。

RawSqlSource.java      
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    // 在這個構造函數裡做getSql的操作
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }
           
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    // 生成SQLSource解析器
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    // 對SQL進行具體的解析,這裡的sqlSource中包含sql語句、字段屬性映射資訊、configuration資訊
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }
           

上面第一個方法代碼裡,主要要關注的是getSql(configuration, rootSqlNode)這個方法,下面的那個就是this 的構造函數了,先繼續探索吧!

private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    // 根據configuration對象,建立一個動态對象
    DynamicContext context = new DynamicContext(configuration, null);
    // 把節點中SQL模闆變成一個字元串
    rootSqlNode.apply(context);
    return context.getSql();
  }
           

這段代碼還是隻是根據一些參數建立對象,context.getSql()和rootSqlNode.apply(context)其實隻是把配置檔案裡的SQL模闆變成個字元串,代碼同學們可以自己看一下。那我們先看下構造函數代碼做了什麼。在構造函數的代碼裡會根據configuration來建立個SQLSource解析器對象,然後通過sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>())方法來進行具體是解析。代碼如下:

SqlSourceBuilder.java
      
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 解析SQL模闆中的#{username},#{birthday},#{sex},#{address}這些标簽
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 開始真正的SQL解析,其實就是字元串拼接過程,不信你點進去看看
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }
           

在這段代碼裡首先建立ParameterMappingTokenHandler這個對象,然後通過這個對象先解析SQL模闆中的#{username},#{birthday},#{sex},#{address}這些标簽,然後就開始真正的SQL解析,其實就是字元串拼接過程,不信你點進去看看。

GenericTokenParser.java
      
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken, 0);
    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);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          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();
  }
           

是吧,我沒有騙你吧,從return builder.toString()這裡就可以看出來了,這段代碼的主要的流程隻是拼接字元串而已,沒什麼好說的,如果對字元串拼接感興趣的小夥伴感興趣的話,可以自己去研究下。

在SqlSourceBuilder.java代碼中的parse方法中,最後所傳回的就是一個StaticSqlSource對象,這裡就是這個對象中包含,我們所解析出來的SQL語句就是如下圖中的sql這個所指向的SQL語句,已經不是originalSql所指向的SQL模闆了。

Mybatis源碼解析《一》

本篇文章就先結束了,由于篇幅原因,對于SQL語句的執行這些操作,将會在下一篇文章進行解析。謝謝同學們的閱讀,如果有錯誤,也請同學們指正。

繼續閱讀