不懂看這——Mybatis執行流程源碼分析

第一部分:項目結構

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操作數據庫的步驟一模一樣。他的封裝就是爲了讓我們可以更方便的傳參和處理結果集。

這時候已經把查詢出來的一條數據放在緩存中了,再次調用第二條查詢語句的話,就不會操作數據庫了,而是直接從緩存中拿這條數據。

來源:https://www.tuicool.com/articles/6juQZ37

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