Mybatis執行流程原始碼分析

風沙迷了眼發表於2020-12-15

第一部分:專案結構

user_info表:只有id和username兩個欄位

User實體類:

public class User {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

mapper:UserMapper 為根據id查詢使用者資訊

public interface UserMapper {
    User getUserByUsername(String username);
}

UserMapper.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.example.mybatis.mapper.UserMapper">
    <select id="getUserByUsername" resultType="com.example.mybatis.entity.User">
        select * from user where username = #{username}
    </select>
</mapper>

mybaitis的主配置檔案:

<?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="application.properties"/>

    <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>
        <mapper resource="mapper\UserMapper.xml"/>
    </mappers>
</configuration>

資料庫連線的屬性檔案:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/simple_orm?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456

測試類:

public class Test {
	public static void main(String[] args) throws IOException {
		String resourcePath = "SqlMapConfig.xml";
		InputStream inputStream = Resources.getResourceAsStream(resourcePath);
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		SqlSession sqlSession = sqlSessionFactory.openSession();
		sqlSession.selectOne("com.example.mybatis.mapper.UserMapper.getUserByUsername", "test1");
	}
}

第二部分:mybatis重要元件

  • Configuration MyBatis所有的配置資訊都儲存在Configuration物件之中,配置檔案中的大部分配置都會儲存到該類中
  • SqlSession 作為MyBatis工作的主要頂層API,表示和資料庫互動時的會話,完成必要資料庫增刪改查功能
  • Executor MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護
  • StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數等
  • ParameterHandler 負責對使用者傳遞的引數轉換成JDBC Statement 所對應的資料型別
  • ResultSetHandler 負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合
  • TypeHandler 負責java資料型別和jdbc資料型別(也可以說是資料表列型別)之間的對映和轉換
  • MappedStatement MappedStatement維護一條<select|update|delete|insert>節點的封裝
  • SqlSource 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回
  • BoundSql 表示動態生成的SQL語句以及相應的引數資訊

第三部分:初始化原始碼分析

首先我把測試類貼上過來方便一點。

public class Test {
	public static void main(String[] args) throws IOException {
		String resourcePath = "SqlMapConfig.xml";
		InputStream inputStream = Resources.getResourceAsStream(resourcePath);
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		SqlSession sqlSession = sqlSessionFactory.openSession();
		sqlSession.selectOne("com.example.mybatis.mapper.UserMapper.getUserByUsername", "test1");
	}
}

這裡的測試類是採用原始的方式使用mybatis進行測試,通過對這幾行程式碼背後執行的邏輯進行分析,來看一下mybatis基本的查詢流程。首先前兩行就是獲取resouces目錄下的配置檔案,然後通過流的方式讀取為inputStream,這個流交由SqlSessionFactoryBuilderbuild方法進行處理,具體怎麼處理的可以看下面的分析。現在先看一下建立SqlSessionFactory這個執行的邏輯:使用builder模式建立會話工廠,mybatis的所有初始化工作都是這行程式碼完成,那麼我們進去一探究竟,主要程式碼邏輯如下:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

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

主要就是先建立一個XMLConfigBuilder物件來解析主配置檔案,就是剛剛載入的那個mybatis的核心配置檔案,其最外層節點是configuration標籤,初始化過程就是將這個標籤以及他的所有子標籤進行解析,把解析好的資料封裝在Configuration這個類中。而Configuration這個類會包含之後執行過程中需要的所有資訊。

第二步:進入parse()方法

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

XMLConfigBuilder維護一個parsed屬性預設為false,這個方法一開始就判斷這個主配置檔案是否已經被解析,如果解析過了就拋異常。

第三步:進入parseConfiguration()方法

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(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"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

我們可以看出這個方法是對configuration的所有子標籤逐個解析。包括settings屬性配置、typeAliases配置別名、environments是配置資料庫連結和事務等等。然後把解析後的資料封裝在Configuration這個類中,parse()方法返回Configuration物件,parseConfiguration()方法就是主要對相關標籤進行解析並封裝到Configuration中。在這裡我們主要看mappers標籤的解析過程,整個過程就是對SqlMapConfig配置檔案中引用到的XXXMapper檔案進行解析,儲存其中的sql到Statement中。

第四步:進入mapperElement()方法。

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          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) {
            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) {
            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);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

(1)核心配置檔案下面的mappers節點下面可能會有很多mapper節點,所以需要迴圈遍歷每一個mapper節點去解析該節點所對映的xml檔案。

(2)迴圈下面是一個if..else判斷。它先判斷mappers下面的子節點是不是package節點。因為在實際開發中有很多的xml檔案,不可能每一個xml檔案都用一個mapper節點去對映,我們乾脆會用一個package節點去對映一個包下面的所有的xml,這是多檔案對映。

(3)如果不是package節點那肯定就是mapper節點做單檔案對映。單檔案對映有3種方式
a. 第一種是resource屬性直接對映xml檔案;
b. 第二種是url屬性對映磁碟內的某個xml檔案;
c. 第三種是class屬性直接對映某個mapper介面.

(4)這裡通過檢視使用resouce方式進行對映得到的xml檔案解析流程,其他的都比較類似。

第五步:看resource方式解析xml。

if (resource != null && url == null && mapperClass == null) {
     ErrorContext.instance().resource(resource);
     InputStream inputStream = Resources.getResourceAsStream(resource);
     XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
     mapperParser.parse();
}

(1)第一行程式碼的意思是例項化一個錯誤上下文物件,其作用就是把使用mybatis過程中的錯誤資訊封裝起來,如果出現錯誤就會呼叫這個物件的toString方法。這個resource引數就是String型別的xml的名字,在我們的專案中是UserMapper.xml.

(2)然後和讀取核心配置檔案時候一樣的方式讀取這個UserMapper.xml獲取輸入流物件。

(3)然後建立一個mapper的xml檔案解析器,類似XMLConfigBuilder的作用,不過這裡是主要解析Mapper檔案的,XMLConfigBuilder是解析核心配置檔案的。

第六步:進入parse()方法:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

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

首先判斷這個xml是否被解析過了。因為configuration物件會維護一個String型別的set集合loadedResources,這個集合中存放了所有已經被解析過的xml的名字,我們在這裡是沒有被解析的,所以進入if中。

第七步:進入configurationElement()方法。

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"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      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);
    }
  }

這個方法就是解析一個mapper.xml所有節點資料。比如解析namespace、resultMap、parameterMap、sql片段等等。重點是最後一句

buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

我們進入這個方法中buildStatementFromContext()

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

沒什麼好說的,繼續進入buildStatementFromContext()

 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);
      }
    }
  }

(1)這個方法一開始是一個迴圈,遍歷一個list,這個list裡裝的是xml中的所有sql節點,比如select insert update delete ,每一個sql是一個節點。迴圈解析每一個sql節點。

(2)建立一個xml的會話解析器去解析每個節點。

第八步:進入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);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

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

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    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;
    }

    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);
    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);
  }

看到這個方法很長,其實大致意思就是解析這個sql標籤裡的所有資料,並把所有資料通過addMappedStatement這個方法封裝在MappedStatement這個物件中。這個物件我們在第二部分介紹過,這個物件中封裝了一條sql所在標籤的所有內容,比如這個sql標籤的id ,sql語句,入參,出參,等等。我們要牢記一個sql的標籤對應一個MappedStatement物件。

第九步:進入addMapperStatement()方法

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 = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    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 statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }

乍一看這個方法很長,我們只看最後三行程式碼。

(1) MappedStatement statement = statementBuilder.build();通過解析出的引數構建了一個MapperStatement物件。

(2)configuration.addMappedStatement(statement); 這行是把解析出來的MapperStatement裝到Configuration維護的Map集合中。key值是這個sql標籤的id值,我們這裡應該就是selectUserById,value值就是我們解析出來的MapperStatement物件。

其實我們解析xml的目的就是把每個xml中的每個增刪改查的sql標籤解析成一個個MapperStatement並把解析出來的這些物件裝到Configuration的Map中備用。

第十步: 返回第六步的程式碼:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

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

剛才到第九步都是在執行configurationElement(parser.evalNode("/mapper"));這行程式碼,接下來看下一行程式碼configuration.addLoadedResource(resource); 到第九步的時候我們已經把一個xml完全解析完了,所以在此就會把這個解析完的xml的名字裝到set集合中。

接下來我們看看bindMapperForNamespace(); 這個名字起得就很望文生義,通過名稱空間繫結mapper

第十一步:進入bindMapperForNamespace()方法。

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }

(1)一開始獲取名稱空間,名稱空間一般都是我們mapper的全限定名,它通過反射獲取這個mapper的class物件。

(2)if判斷,Configuration中也維護了一個Map物件,key值是我們剛才通過反射生產的mapper的class物件,value值是通過動態代理生產的class物件的代理物件。

(3)因為Map中還沒有裝我們生產的mapper物件,進入if中,它先把名稱空間存到我們剛才存xml名字的set集合中。然後再把生產的mapper的class物件存到Mapper中。

第十二步:進入addMapper()方法

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

我們發現它呼叫了mapperRegistry的addMapper方法,這個類通過名字就知道是mapper註冊類,我們再點進入看看

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<>(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);
        }
      }
    }
  }

我們可以看出mapperRegistry這個類維護的Map的名字是knownMappers---->(已知的mapper--->就是註冊過的mapper). 我們看他的put,key是我們生成的mapper的class物件,value是通過動態代理生成的mapper的代理物件。

到此mybatis根據主配置檔案初始化就完成了,那說了這麼久到底做了什麼呢?我們總結一下。

1、總的來說就是解析主配置檔案把主配置檔案裡的所有資訊封裝到Configuration這個物件中:
a.通過XmlConfigBuilder解析主配置檔案,然後通過XmlMapperBuild解析mappers下對映的所有xml檔案(迴圈解析)。
b.把每個xml中的各個sql解析成一個個MapperStatement物件裝在Configuration維護的一個Map集合中,key值是id,value是mapperstatement物件.
c.然後把解析過的xml的名字和名稱空間裝在set集合中,通過名稱空間反射生成的mapper的class物件以及class物件的代理物件裝在Configuration物件維護的mapperRegistry中的Map中。

2、簡化一點:主要就是把每個sql標籤解析成mapperstatement物件裝進集合,然後把mapper介面的class物件以及代理物件裝進集合,方便後來使用。

3、注意一點: 我們用resource引入xml的方法是先解析xml ,把各個sql標籤解析成mapperstatement物件裝進集合,然後再把mapper介面的class物件以及代理物件裝進集合,但是引入xml的方式有4種,其中單檔案引入方式還有url方式和class方式,看原始碼可以知道url方式就是直接引入一個xml和resource方式一模一樣。而class方式是引入一個mapper介面卻同(resource和url方式相反)

第十三步:我們看一下使用class方式引入的方法

else if (resource == null && url == null && mapperClass != null) {
  Class<?> mapperInterface = Resources.classForName(mapperClass);
  configuration.addMapper(mapperInterface);
} 

我們可以看出是先反射生產mapper介面的class物件,然後呼叫Configuration的addMpper方法,這個方法是不是很熟悉,我們點進去看一下

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<>(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);
        }
      }
    }
  }

是不是跟上面最後一步一樣,生產mapper的class物件後,再通過動態代理生產代理物件然後裝進集合。那我們介面物件生成了不還沒解析xml呢嘛,別急我們進入parser.parse()這個方法

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      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();
  }

你看它一開始會判斷這個mapper對應的xml是否存在於裝已經解析過的xml的set集合中,肯定沒有,沒有進入if中 重點來了---->loadXmlResource(); 這個方法看名字就知道是載入xml資源,我們點進去看一下

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
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      // #1347
      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) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        xmlParser.parse();
      }
    }
  }

就是一頓往下走,走到 xmlParser.parse();這個方法中 我們點進去看一下:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

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

這個方法是不是很眼熟?沒錯,這就是我們第六步的程式碼。接下來想必大家都知道了,就是上面第六步到第九步。

我們可以看出--->用resource、url 和 class來解析的方式步驟是相反的。

resource和url是直接引入xml,那我們就先解析xml,然後通過xml的名稱空間反射生成mapper的class物件,再通過動態代理生產class物件的代理物件

而class方式填寫的是mapper介面的全限定名,就是上面的那個名稱空間,所以先生成class物件和代理物件,然後通過拼接字串就是全限定名+“.xml”獲取xml的名稱,然後再解析xml。

說到這單檔案對映就說完了,我們再說說多檔案對映。

第十四步:多檔案對映

if ("package".equals(child.getName())) {
    String mapperPackage = child.getStringAttribute("name");
    configuration.addMappers(mapperPackage);
 }

它首先或得xml所在的包名,然後呼叫configuration的addMappers物件,是不是有點眼熟,單檔案對映是addMapper,多檔案對映是addMappers 你看人家這名字取得 絕了。我們點進去看看

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


public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }


public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }

我們看第三段程式碼,這是什麼意思呢?就是通過ResolverUtil這個解析工具類找出該包下的所有mapper的名稱通過反射生產mapper的class物件裝進集合中,然後看出迴圈呼叫addMapper(mapperClass)這個方法,這就和單檔案對映的class型別一樣了,把mapper介面的class物件作為引數傳進去,然後生產代理物件裝進集合然後再解析xml。

到此mybatis的初始化就說完了。

第四部分:獲取session會話物件原始碼分析

我們上一部分是mybatis的初始化,走的程式碼是:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

其實我們點進去會發現最後返回的是 DefaultSqlSessionFactory物件

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

獲取會話物件走的程式碼是:

SqlSession session = sqlSessionFactory.openSession();

直接open一個session,我們知道session是我們與資料庫互動的頂級api,所有的增刪改查都要呼叫session.我們進入openSession()

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);

  SqlSession openSession(Connection connection);

  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

我們發現這個一個介面,不慌我們找他的實現類-->DefaultSqlSessionFactory

@Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }


private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

我們看第二段程式碼:因為我們解析主配置檔案把所有的節點資訊都儲存在了configuration物件中,它開始直接或得Environment節點的資訊,這個節點配置了資料庫連線和事務。之後通過Environment建立了一個事務工廠,然後通過事務工廠例項化了一個事務物件。 重點來了------> 最後他建立了一個執行器Executor ,我們知道session是與資料庫互動的頂層api,session中會維護一個Executor 來負責sql生產和執行和查詢快取等。我們再來看看new這個執行器的時候的過程

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

這個過程就是判斷生成哪一種執行器的過程,mybatis的執行器有三種--->

public enum ExecutorType {
  SIMPLE, REUSE, BATCH
}

SimpleExecutor: 簡單執行器,是 MyBatis 中預設使用的執行器,每執行一次 update 或 select,就開啟一個 Statement 物件,用完就直接關閉 Statement 物件(可以是 Statement 或者是 PreparedStatment 物件)

ReuseExecutor: 可重用執行器,這裡的重用指的是重複使用 Statement,它會在內部使用一個 Map 把建立的 Statement 都快取起來,每次執行 SQL 命令的時候,都會去判斷是否存在基於該 SQL 的 Statement 物件,如果存在 Statement 物件並且對應的 connection 還沒有關閉的情況下就繼續使用之前的 Statement 物件,並將其快取起來

因為每一個 SqlSession 都有一個新的 Executor 物件,所以我們快取在 ReuseExecutor 上的Statement 作用域是同一個 SqlSession。

BatchExecutor: 批處理執行器,用於將多個SQL一次性輸出到資料庫

(貼上過來的) 我們如果沒有配置或者指定的話預設生成的就是SimpleExecutor。

執行器生成完後返回了一個DefaultSqlSession,這裡面維護了Configuration和Executor。

第五部分:查詢過程原始碼分析

首先我們把查詢的程式碼貼上過來

sqlSession.selectOne("com.example.mybatis.mapper.UserMapper.getUserByUsername", "test1");

sqlSession.selectOne("com.example.mybatis.mapper.UserMapper.getUserByUsername", "test1");

我為什麼要寫兩個一模一樣的查詢呢?因為mybatis有一級快取和二級快取,預設二級快取是不開啟的,可以通過配置開啟。而一級快取是開啟的,一級快取是session級別的快取,mybatis在查詢的時候會根據sql的id和引數等生產一個快取key,查詢資料庫的時候先查詢快取key是不是存在於快取中,如果沒有就查詢資料庫,如果存在就直接返回快取中的資料。需要注意的是除了查詢,其他的新增,更新,刪除都會清除所有快取,包括二級快取(如果開啟的話).

我們看控制檯資訊可以發現,第一次查的時候有sql語句列印,就是我紅線框的地方,然後輸出了 “我是第一次查詢的User(id=1, name=achuan, age=15)” 接著分割線下面直接輸出了 “我是第二次查詢的User(id=1, name=achuan, age=15)”,因為第一次查詢的時候拿著快取key去快取中查,沒有查到對應該key的快取,就查詢資料庫返回並把查出的資料放在快取中,第二次查詢的生成的key與第一次一樣,去快取中查到資料直接返回,沒有查詢資料庫,這樣可以提高查詢效率。

好了說了這麼多我們來開始分析原始碼-->selectOne() 我們進入selectOne()方法

<T> T selectOne(String statement, Object parameter);

@Override
  public <T> T selectOne(String statement) {
    return this.selectOne(statement, null);
  }

@Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

我們點進去發現是一個介面,不慌找它的實現類DefaultSqlSession,我們發現它進入了上面第三段程式碼,我們發現它呼叫了selectList()方法,其實查詢一個或者多個都是呼叫selectList方法,我們進入selectList()方法中

@Override
  public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }


@Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

重點來了,我們看下這行程式碼

MappedStatement ms = configuration.getMappedStatement(statement);

我們呼叫selectOne的時候傳的引數是sql的id值 :selectUserById 和 sql的引數:1,在這行程式碼中引數statement的值就是selectUserById , 我們回憶一下,mybatis初始化的時候是不是把每個sql標籤解析成一個個的MapperStatement,並且把這些MapperStatement裝進configuration物件維護的一個Map集合中,這個Map集合的key值就是sql標籤的id,value是對應的mapperstatement物件,我們之前說裝進集合中備用就是在這裡用的,這裡用sql標籤的id值從Map中取出對應的MapperStatement物件。

比如我們現在selectOne方法呼叫的的是selectUserById 這個sql,所以現在通過selectUserById 這個key值從configuration維護的Map中取出對應的MapperStatement物件。為什麼要取出這個物件呢?因為mybatis把一個sql標籤的所有資料都封裝在了MapperStatement物件中。比如:出參型別,出參值,入參型別,入參值還有sql語句等等。

然後我們取出MapperStatement物件看下一行程式碼

executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

MapperStatement被當做引數傳入query方法,這個query方法是執行器呼叫的,我們知道執行器的作用是sql的生成執行和查詢快取等操作,在這個query方法中我們會查詢快取和執行sql語句,我們進入query()方法

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;


@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }



@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

我們點進去發現是進入的Executor介面,不慌,找他的實現類,它先走的是CachingExcutor快取執行器,我們研究一下程式碼,我們看第二段程式碼他一開始從MapperStatement中獲取BoundSql 這個物件,因為真正的sql語句封裝在這個物件中,而且這個物件也負責把sql中的佔位符替換成我們傳的引數,只是MapperStatement維護了BoundSql 的引用而已。

然後我們繼續看createCacheKey,這個的意思就是根據這些引數生成一個快取key,當我們呼叫同一個sql,並且傳的引數是一樣的時候,生成的快取key是相同的。

然後我們看第三段程式碼,它一開始就是獲取快取,但是他這個快取並不是我們儲存查詢結果的地方(具體是快取什麼的我也不太清楚,我猜測這裡查的是二級快取,具體我沒測試,不出意外的話應該是二級快取,我們沒有開啟二級快取,所以這裡為null),它查詢快取為null,就會走最後一句程式碼

return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

我們發現它又呼叫了delegate的query方法,delegate是什麼呢?我們看一下CachingExcutor的屬性

private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

我們發現delegate是一個執行器的引用,在這裡其實是SimpleExcutor簡單執行器的引用,我們知道獲取一個會話session的時候會建立一個執行器,如果沒有配置的話預設建立的就是SimpleExcutor,在這裡把SimpleExcutor的引用維護到CachingExcutor中。實際這裡用到了委託者模式----->大致意思就是我自己不行我就找行的來做[手動滑稽] ,這裡就是快取執行器不行未能執行sql就交給SimpleExcutor來執行,我們進入這個query方法內。

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;



@SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

一點進去發現是Executor介面,不慌我們看他的實現類,他有兩個實現類快取執行器和基礎執行器,而基礎執行器有三個正常的兒子,他先回撥用爸爸基礎執行器裡面的query方法,也就是上面第二段程式碼,乍一看好像有點看不懂,沒事我們來分析一下,我們直接看try裡面的程式碼很容易明白

List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

一開始宣告瞭一個集合list,然後通過我們之前建立的快取key去本地快取localCache中查詢是否有快取,下面判斷,如果集合不是null就處理一下快取資料直接返回list,如果沒有快取,他回從資料庫中查,你看他們這名字起的一看就知道是什麼意思queryFromDatabase,我們現在執行的是第一條selectOne,沒有快取我們進入queryFromDatabase方法

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

你看這段程式碼,先在本地快取中佔個位,然後執行doQuery從資料庫中查資料,然後移除剛才的快取中的佔位,最後把查出來的資料put進本地快取中,我不知道他這個佔位又移除到底想搞什麼么蛾子,反正我們明白,那不重要,重要的是他執行了doQuery從資料庫中查到資料並放入快取中,我們接著看一下doQuery這個方法的程式碼

protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;



@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

點進去是BaseExecutor抽象類,不慌找他的兒子SimpleExecutor,找到doQuery。doQuery方法一開始從configuration 中拿出會話處理器,會話處理器我們上面的元件介紹提到過,作用是 裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數等,那我們現在複習一下jdbc運算元據庫的步驟:

1 註冊驅動 2 獲取連線 3 建立會話物件 也就是上面提到的statement 或者是可以防止注入攻擊的prepareStatement 4 執行sql語句 5 處理結果集 6 關閉連線

他獲取會話處理器後,執行了prepareStatement(handler, ms.getStatementLog());這個是重點,熟悉的東西來了,我們進入這個方法看看

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

一開始就是獲取資料庫連線,然後執行handler.prepare();這個方法的作用就是根據連線事務啥的建立 會話物件 就是上面jdbc操作中的 第3 步。我們進入這個方法,跟之前一樣用到了委託者模式然後也是有兩個實現類,一個抽象類有三個實現類。

Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;



 @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    return delegate.prepare(connection, transactionTimeout);
  }


 @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

點進去一看是一個介面,不慌走RoutingStatementHandler,這裡用到了委託者模式,委託給BaseStatementHandler, 到此就執行到了上面的第三段程式碼,我們觀察這段程式碼try中的三行程式碼

statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
  

下面兩個就是設定會話物件的屬性不重要,重要的是instantiateStatement(connection),我們點進去看看

protected abstract Statement instantiateStatement(Connection connection) throws SQLException;



@Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
  }

點進去是抽象類,不慌,在PrepareStatmentHandler,我們發現return的全是prepareStatement預編譯會話物件,說明mybatis預設就可以防止注入攻擊。

然後我們返回獲取會話物件之前的程式碼

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

會話物件獲取完之後,又執行 了handler.parameterize(stmt);這個執行的步驟基本跟獲取會話物件的步驟一模一樣,最終執行的是三個兒子之一的PrepareStatementHandler中的parameterize方法

@Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }

你看這裡用到了parameterHandler 引數處理器 ,這個處理器作用是:負責對使用者傳遞的引數轉換成JDBC Statement 所對應的資料型別 , 就是把String轉成varchar之類的。

到這裡 我們 獲取了資料庫連線 ,又獲得了會話物件,引數也設定好了,是不是該執行sql了,prepareStatement這個方法就執行完了,我們再返回撥用prepareStatement這個方法的方法

@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

看,之前的操作就是為了返回預編譯的會話物件,返回後直接執行query方法,我們進入query方法:

@Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }

我們點進去最終執行還是PrepareStatementHandler的query方法,把會話物件轉換成PreparedStatement預編譯的會話物件(這裡又轉換了一次,那之前的理解可能有點誤差),然後直接用會話物件呼叫execute方法,是不是jdbc一模一樣,在jdbc中我們獲取了會話物件也是呼叫execute方法。

sql執行了是不是該處理結果集了,我們看他的return, 用到了resultSetHandler,結果集處理器,這個元件上面的元件介紹提到過,作用是:負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合,就是把我們查到的資料轉換成list型別,我們現在是selectOne,所以這個集合中只有一條資料。

到此就把一次查詢的步驟說完了,其實說到底就是封裝了jdbc運算元據庫的步驟,最終還是和jdbc運算元據庫的步驟一模一樣。他的封裝就是為了讓我們可以更方便的傳參和處理結果集。

這時候已經把查詢出來的一條資料放在快取中了,再次呼叫第二條查詢語句的話,就不會運算元據庫了,而是直接從快取中拿這條資料。

相關文章