天天看點

MyBatis中Executor源碼解析之BatchExecutor搞不懂

為了便于源碼分析,還是先來一個MyBatis的Demo吧

mybatis-mysql-config.xml

1 <?xml version="1.0" encoding="UTF-8"?>
 2 <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
 3         "http://mybatis.org/dtd/mybatis-3-config.dtd">
 4 
 5 <configuration>
 6     <properties>
 7         <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
 8         <property name="url" value="jdbc:mysql://127.0.0.1:3306/gys?serverTimezone=UTC"/>
 9     </properties>
10     <settings>
11         <setting name="defaultExecutorType" value="SIMPLE"/>
12     </settings>
13     <!--環境配置,連接配接的資料庫,這裡使用的是MySQL-->
14     <environments default="dev">
15         <environment id="dev">
16             <!--指定事務管理的類型,這裡簡單使用Java的JDBC的送出和復原設定-->
17             <transactionManager type="JDBC" />
18             <!--dataSource 指連接配接源配置,POOLED是JDBC連接配接對象的資料源連接配接池的實作-->
19             <dataSource type="POOLED">
20                 <property name="driver" value="${driver}"></property>
21                 <property name="url" value="${url}"></property>
22                 <property name="username" value="root"></property>
23                 <property name="password" value="gys"></property>
24             </dataSource>
25         </environment>       
26     </environments>
27     <mappers>   
28         <mapper resource="mapper/user.xml"></mapper>
29     </mappers>
30 </configuration>      

user.xml

1 <?xml version="1.0" encoding="UTF-8"?>
 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 3         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 4 <mapper namespace="dao.IUserMapper">  
 5 
 6     <insert id="insertUser" parameterType="model.User">
 7         insert into user
 8         (name,age)
 9         values
10         (#{name},#{age})
11     </insert>
12 
13 </mapper>      

入口方法main:

1     public static void main(String[] args) throws Exception {
 2         SqlSessionFactory sqlSessionFactory1=new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-mysql-config.xml"),"dev");
 3         SqlSession sqlSession1= sqlSessionFactory1.openSession(true);
 4         IUserMapper userMapper=sqlSession1.getMapper(IUserMapper.class);
 5         User user=new User();
 6         user.setAge(28);
 7         user.setName("a");
 8         int i=userMapper.insertUser(user);
 9         System.out.println("受影響的行數"+i);
10         sqlSession1.close();
11     }      

這個Executor的代碼離上面Demo執行代碼還有一段很長封裝,如果分析Executor,就要分析分析這段很長的封裝代碼;

這個源碼該怎麼開始才能讓人覺得水到渠成,順其自然呢?

算了,還是硬着頭皮一步一步來吧;

第一步:build過程中,如何擷取到 defaultExecutorType配置

MyBatis中Executor源碼解析之BatchExecutor搞不懂

  第75行執行個體化一個XMLConfigBuilder對象,這是一個xml解析器。

第78行調用第91行的build方法,這個方法的參數是個Configuration對象,那麼parser.parse()方法傳回的一定是一個Configuration對象;

換句話說就是在parser.parse()中讀取的配置檔案,并且指派給configuratiion對象。

parser.parse()源碼:      
MyBatis中Executor源碼解析之BatchExecutor搞不懂

 parseConfiguration()源碼

1 private void parseConfiguration(XNode root) {
 2       try {     
 3         propertiesElement(root.evalNode("properties"));
 4         Properties settings = settingsAsProperties(root.evalNode("settings"));
 5        loadCustomVfs(settings);
 6         loadCustomLogImpl(settings);
 7         typeAliasesElement(root.evalNode("typeAliases"));
 8         pluginElement(root.evalNode("plugins"));
 9         objectFactoryElement(root.evalNode("objectFactory"));
10        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
11        reflectorFactoryElement(root.evalNode("reflectorFactory"));
12     //解析settings中的内容
13        settingsElement(settings);     
14        environmentsElement(root.evalNode("environments"));
15        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
16        typeHandlerElement(root.evalNode("typeHandlers"));
17        mapperElement(root.evalNode("mappers"));
18      } catch (Exception e) {
19        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
20      }
21    }      

根據方法名大概能推斷出處理的都是那些配置;直接看13行的代碼吧。

settingsElement()源碼:

1 private void settingsElement(Properties props) {
 2     configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
 3     configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
 4     configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
 5     configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
 6     configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
 7     configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
 8     configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
 9     configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
10     configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
11     configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
12     configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
13     configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
14     configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
15     configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
16     configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
17     configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
18     configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
19     configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
20     configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
21     configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
22     configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
23     configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
24     configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
25     configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
26     configuration.setLogPrefix(props.getProperty("logPrefix"));
27     configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
28   }      

看看第11行代碼就是給configuration明确defaultExecutorType值的。

getProperty(key,defaultValue)有兩個參數,第一個參數是屬性的鍵值,根據這個鍵值擷取屬性值,如果擷取不到就用第二個參數作為預設值。
如果沒有配置 <setting name="defaultExecutorType" value="SIMPLE"/>,則用SIMPLE值。
說到這就來說說defaultExecutorType有哪些參數      
SIMPLE:就是普通的執行器
REUSE: 執行器會重(讀:蟲)用預處理語句(PreparedStatements)(re+use=重複+使用)
BATCH 執行器将重(蟲)用語句并執行批量更新。

我一開始的看到官方的内容既不知道這三個什麼意思,也不知道那個字到底讀chong,還是讀zhong.
先改參數跑Demo看結果。      
<setting name="defaultExecutorType" value="SIMPLE"/>      
<setting name="defaultExecutorType" value="REUSE"/>      
這兩個配置分别配置後,都可以正常的往資料庫中插入一條資料,并且傳回影響行數是1,隻有      
<setting name="defaultExecutorType" value="BATCH"/>
這條配置傳回的資料是下面這個奇怪的東西;也沒有往資料庫中插入資料,也不抛異常。
      
受影響的行數-2147482646      
1.SIMPLE和REUSE配置都能正常運作,那麼差別在哪?
2.BATCH配置傳回的奇怪的資料是什麼東西,為什麼不成功。

說了這麼久都還沒有說Executor是幹什麼的。
Executor代表執行器,有它來排程StatementdHandler,ParameterHandler,ResultHandler等來執行對應的SQL.
對于JDBC熟悉一點的人對上面幾個Handler名字應該有點熟悉。如果忘記了JDBC,看看這篇部落格:不了解jdbc,何談Mybatis的源碼解析?
Executor既然是總排程,那麼它應該是在MyBatis中Mapper的JDK動态代理的地方開始調用的。
如果不清楚這個動态代理,可以看看這篇部落格:從mybatis源碼看JDK動态代理
可以從Demo中的第四行代碼getMapper()方法往下追。      
MyBatis中Executor源碼解析之BatchExecutor搞不懂
MyBatis中Executor源碼解析之BatchExecutor搞不懂
MyBatis中Executor源碼解析之BatchExecutor搞不懂
MyBatis中Executor源碼解析之BatchExecutor搞不懂

 getMapper()的調用順序如下: 

defaultSqlSession.getMapper()==》configuration.getMapper()==>MapperRegistry.getMapper()==>MapperProxyFactory.newInstance();

根據最後一張圖的46和47行代碼,結合JDK動态代理的調用規則,可以推斷MapperProxy中一定實作了  InvocationHandler 接口;并且實作了invoke方法。

1 @Override
 2   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 3     try {
 4      //調用object中的方法
 5       if (Object.class.equals(method.getDeclaringClass())) {
 6         return method.invoke(this, args);
 7       } else if (method.isDefault()) {
 8    //調用接口中的預設方法(jdk9之後,接口可以有方法體)
 9         if (privateLookupInMethod == null) {
10           return invokeDefaultMethodJava8(proxy, method, args);
11         } else {
12           return invokeDefaultMethodJava9(proxy, method, args);
13         }
14       }
15     } catch (Throwable t) {
16       throw ExceptionUtil.unwrapThrowable(t);
17     }
18     final MapperMethod mapperMethod = cachedMapperMethod(method);//将method包裝成MapperMethod對象
19     return mapperMethod.execute(sqlSession, args);
20   }      

execute()源碼:

1 public Object execute(SqlSession sqlSession, Object[] args) {
 2     Object result;
 3     switch (command.getType()) {
 4       case INSERT: {//sql是insert語句
 5         Object param = method.convertArgsToSqlCommandParam(args);
 6         result = rowCountResult(sqlSession.insert(command.getName(), param));
 7         break;
 8       }
 9       case UPDATE: {//sql是update語句
10         Object param = method.convertArgsToSqlCommandParam(args);
11         result = rowCountResult(sqlSession.update(command.getName(), param));
12         break;
13       }
14       case DELETE: {//sql是delete語句
15         Object param = method.convertArgsToSqlCommandParam(args);
16         result = rowCountResult(sqlSession.delete(command.getName(), param));
17         break;
18       }
19       case SELECT://sql是select語句
20         if (method.returnsVoid() && method.hasResultHandler()) {
21           executeWithResultHandler(sqlSession, args);
22           result = null;
23         } else if (method.returnsMany()) {
24           result = executeForMany(sqlSession, args);
25         } else if (method.returnsMap()) {
26           result = executeForMap(sqlSession, args);
27         } else if (method.returnsCursor()) {
28           result = executeForCursor(sqlSession, args);
29         } else {
30           Object param = method.convertArgsToSqlCommandParam(args);
31           result = sqlSession.selectOne(command.getName(), param);
32           if (method.returnsOptional()
33               && (result == null || !method.getReturnType().equals(result.getClass()))) {
34             result = Optional.ofNullable(result);
35           }
36         }
37         break;
38       case FLUSH:
39         result = sqlSession.flushStatements();
40         break; 
41     return result;
42   }      

代碼加注釋應該能知道判斷本次sql的類型。我們Demo執行的是insert語句;是以我們檢視第六行的sqlsession.insert()

1 @Override
 2   public int insert(String statement, Object parameter) {
 3     return update(statement, parameter);
 4   }
 5 
 6   @Override
 7   public int update(String statement, Object parameter) {
 8     try {
 9       dirty = true;
10       MappedStatement ms = configuration.getMappedStatement(statement);
   //記住這個地方,等會還要回到這個地方;現在我們要去搞明白executor是從哪裡傳過來的,是何種執行器。
11       return executor.update(ms, wrapCollection(parameter));
12     } catch (Exception e) {
13       throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
14     } finally {
15       ErrorContext.instance().reset();
16     }
17   }      

上面的insert方法調用的是update方法。

在update方法中終于看到了executor這個變量,到此終于看到了本篇的主角。

從目前的分析來看,還沒有看到executor是從什麼地方進行傳過來的。

executor是defaultSqlsession的一個屬性,

MyBatis中Executor源碼解析之BatchExecutor搞不懂

 既是私有的、又是final修飾的,沒有辦法進行set指派,是以必然隻能在執行個體化的時候進行指派,上圖源碼第57行應驗了我們的觀點。

現在就是要找到DefaultSqlSession在什麼地方調用執行個體化的,就能知道執行器是怎麼傳的了。

捋一下思路,sqlSession是一次與資料庫會話的玩意,相當于Jdbc中的connection。

這時候我們翻到Demo裡面的代碼看到第三行代碼:SqlSession sqlSession1= sqlSessionFactory1.openSession(true);方法。

從字面就能了解sqlSessionFactory工廠建立一個會話對象。繼續追蹤openSession源碼。

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

openSessionFromDataSource()傳了建構configuration對象時的執行器;getDefaultExecutorType()擷取配置的執行器。

繼續追蹤openSessionFromDataSource()源碼;去除多餘的代碼

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //擷取執行器,因為configuration存的是枚舉,需要根據枚舉轉換成實際的執行器對象
      final Executor executor = configuration.newExecutor(tx, execType);
//執行個體化defaultSqlSession;本次資料庫會話的執行器确定下來了。
      return new DefaultSqlSession(configuration, executor, autoCommit);   
  }      

确定了執行器之後,繼續回到上面  executor.update(ms, wrapCollection(parameter));方法調用的地方。

executor是一個接口變量,他一定指向某個實作類。

MyBatis中Executor源碼解析之BatchExecutor搞不懂

 搞清楚了executor的來源。現在這個地方的update就應該知道是哪個執行器裡買的方法了。

根據配置隻會從SimpleExecutor,ReuseExecutor,BatchExecutor三個類中去執行。

現在可以把這三個類單獨拿出來分析了。

SimpleExecutor.java

這個裡面沒有找到update(),可能是繼承的原因,在BaseExecutor裡面去找。

MyBatis中Executor源碼解析之BatchExecutor搞不懂

調用的是doUpdate()方法 

MyBatis中Executor源碼解析之BatchExecutor搞不懂

 它是一個抽象方法,點選左邊的繼承按鈕。

MyBatis中Executor源碼解析之BatchExecutor搞不懂

 繼續回到SimpleExecutor中的doUpdate()方法。

1   @Override
 2   public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
 3     Statement stmt = null;
 4     try {
 5       Configuration configuration = ms.getConfiguration();
 6 //StatementHandler後面再說吧
 7       StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
 8 //擷取Statement具體對象
 9       stmt = prepareStatement(handler, ms.getStatementLog());
10       return handler.update(stmt);
11     } finally {
12       closeStatement(stmt);
13     }
14   }      

還是繼續追蹤prepareStatement()源碼

1  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
2     Statement stmt;
3     Connection connection = getConnection(statementLog);
4     //對sql進行預編譯,并傳回預編譯對象。
5     stmt = handler.prepare(connection, transaction.getTimeout());
6    //對sql中的?參數進行設定
7     handler.parameterize(stmt);
8     return stmt;
9   }      

這裡面的getConnection(),preparse(),parameterize()方法的執行還有很長的調用鍊,後面就是參數如何轉換的問題,牽扯的TypeHandler子產品了,後面在單獨開辟一篇分析吧。

至此SimpleExecutor執行器分析完了; 

繼續分析ReuseExecutor.java

用和SimpleExecutor同樣的方法找到prepareStatement()方法

1  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
 2     Statement stmt;
 3     BoundSql boundSql = handler.getBoundSql();
 4   //擷取sql語句
 5     String sql = boundSql.getSql();
 6 //查詢這條sql是否預編譯過,有的話直接擷取預編譯對象
 7     if (hasStatementFor(sql)) {
 8       stmt = getStatement(sql);
 9       applyTransactionTimeout(stmt);
10     } else {
11 //第一次預編譯,直接建立一個預編譯對象
12       Connection connection = getConnection(statementLog);
13       stmt = handler.prepare(connection, transaction.getTimeout());
14    //緩存預編譯對象
15       putStatement(sql, stmt);
16     }
17   //sql中的參數替換
18     handler.parameterize(stmt);
19     return stmt;
20   }      

ReuseExecutor的prepareStatement方法比SimExecutor的prepareStatement()方法多了一個sql的預編譯對象的儲存。

至此終于明白了為什麼 REUSE配置叫 重(蟲)用sql語句了;re(重複)+use(使用)=reuse。

BatchExecutor分析。

//代碼很長大,但是隻要看最後兩行代碼就行了。
 @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);//fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    //fix Issues 322
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }      

首先這個方法本身就很奇怪;不管執行結果如何,直接傳回  BATCH_UPDATE_RETURN_VALUE ;

我們看看BATCH_UPDATE_RETURN_VALUE 是什麼東西。

MyBatis中Executor源碼解析之BatchExecutor搞不懂

 直接就是int的最小值加1002;不明白為什麼要傳回這麼個特定的值;為什麼不直接傳回1,2,張三,阿毛、阿狗?

現在終于知道上面的insert為什麼在BATCH配置的情況下,傳回下面這個截圖内容了

MyBatis中Executor源碼解析之BatchExecutor搞不懂

handler.batch(stmt)源碼

MyBatis中Executor源碼解析之BatchExecutor搞不懂

 直接addBatch()就結束了,沒有向資料庫發送執行操作的代碼,比如ps.

executeBatch()方法。

現在也終于知道為什麼資料庫裡面一直都沒有新增資料了。

可是為什麼要這麼設計呢?是代碼沒寫完麼?

1.SimpleExecutor是對jdbc中PreParement的封裝

2.ReuseExecutor是對Jdbc中PreParement封裝的基礎上進行緩存,以達到sql活預編譯對象的重複使用

3.希望知道答案的博友們告知一下,為什麼Batch執行器不寫操作資料庫的代碼,如果不寫,那要這個執行器還有什麼用呢?