其實很早就想寫一篇 iBatis 的源碼分析了, 不過有段時間去學習 Go 了, Java 就放下了, 最近 重新撿起 Java 就把以前沒填的坑,填一下.

Init

現在開始正片.

首先是 iBatis 的初始化工作.我們看下面的代碼:

 1// `BlogDataSourceFactory`的主要作用: 通過你的配置文件, 初始化一個DataSource
 2DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
 3// JdbcTransactionFactory一個New就能得到, 沒什麼依賴條件
 4TransactionFactory transactionFactory = new JdbcTransactionFactory();
 5// Environment要你交出數據源和事務工廠還有你的環境是開發還是生產
 6Environment environment = new Environment("development", transactionFactory, dataSource);
 7// Configuration有基本上你所有的配置
 8Configuration configuration = new Configuration(environment);
 9// 添加你的mapper到配置列表中, 等會我們去分析它
10configuration.addMapper(BlogMapper.class);
11// 通過你的配置類,讓我們初始化一個SqlSessionFactory! 我們終於進入正題了!!
12// 可能你覺得很快... 其實本人在這裡面分析還是花了很長時間
13SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

好, 上文有說configuration.addMapper(BlogMapper.class)這個方法, 現在我們來分析一下它.

 1
 2    // 這個是Configuration中的方法, 它實際上是委託mapperRegistry去執行
 3    public <T> void addMapper(Class<T> type) {
 4        mapperRegistry.addMapper(type);
 5    }
 6
 7    public <T> void addMapper(Class<T> type) {
 8        //mapper必須是接口
 9        if (type.isInterface()) {
10        if (hasMapper(type)) {
11            //如果重複添加了,報錯
12            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
13        }
14        boolean loadCompleted = false;
15        try {
16            // 加入一個Mapper的代理生產工廠
17            knownMappers.put(type, new MapperProxyFactory<T>(type));
18            // It's important that the type is added before the parser is run
19            // otherwise the binding may automatically be attempted by the
20            // mapper parser. If the type is already known, it won't try.
21            // 這個是通過註解來構建Mapper, 暫時不看
22            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
23            parser.parse();
24            loadCompleted = true;
25        } finally {
26            //如果加載過程中出現異常需要再將這個mapper從mybatis中刪除
27            if (!loadCompleted) {
28            knownMappers.remove(type);
29            }
30        }
31        }
32    }

SqlSessionFactory

既然有了 SqlSessionFactory,顧名思義,我們可以從中獲得 SqlSession 的實例。 SqlSession 提供了在數據庫執行 SQL 命令所需的所有方法。 你可以通過 SqlSession 實例來直接執行已映射的 SQL 語句。

1try (SqlSession session = sqlSessionFactory.openSession()) {
2  BlogMapper mapper = session.getMapper(BlogMapper.class);
3  Blog blog = mapper.selectBlog(101);
4}

好! 我們快進到曹丕sqlSessionFactory.openSession()

 1    public SqlSession openSession() {
 2        return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
 3    }
 4
 5    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
 6        // Transaction事務,包裝了一個Connection, 包含commit,rollback,close方法
 7    Transaction tx = null;
 8    try {
 9      // 還記得麼, environment裡封裝了我們的數據源;事務工廠;還有環境
10      final Environment environment = configuration.getEnvironment();
11      // 得到一個事務工廠, 如果env或者env裡的事務工廠是空的就返回一個託管事務工廠
12      // 託管事務工廠的特點就是每次執行完成SQL都會關閉連接, 如果你不希望關閉連接要在配置文件裡設置它
13      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
14      //通過事務工廠來產生一個事務
15      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
16      //生成一個執行器(事務包含在執行器裡)
17      final Executor executor = configuration.newExecutor(tx, execType);
18      //然後產生一個DefaultSqlSession
19      return new DefaultSqlSession(configuration, executor, autoCommit);
20    } catch (Exception e) {
21      //如果打開事務出錯,則關閉它
22      closeTransaction(tx); // may have fetched a connection so lets call close()
23      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
24    } finally {
25      //最後清空錯誤上下文
26      ErrorContext.instance().reset();
27    }
28  }

這樣我們就得到了一個SqlSession.

2020.10.12 繼續更新

有了SqlSession之後我們就可以操作數據庫了。

我們來看看MyBatis是怎麼實現session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);的。

 1  public <T> T selectOne(String statement, Object parameter) {
 2    // Popular vote was to return null on 0 results and throw exception on too many.
 3    //轉而去調用selectList,很簡單的,如果得到0條則返回null,得到1條則返回1條,得到多條報TooManyResultsException錯
 4    // 特別需要主要的是當沒有查詢到結果的時候就會返回null。因此一般建議在mapper中編寫resultType的時候使用包裝類型
 5    //而不是基本類型,比如推薦使用Integer而不是int。這樣就可以避免NPE
 6    List<T> list = this.<T>selectList(statement, parameter);
 7    if (list.size() == 1) {
 8      return list.get(0);
 9    } else if (list.size() > 1) {
10      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
11    } else {
12      return null;
13    }
14  }
15
16  // emm,這裡其實啥都沒有,我們去SelectList看看。
17
18  // 在下來解釋一下這三個參數:
19  // statement 映射語句的位置,比如"org.mybatis.example.BlogMapper.selectBlog"
20  // parameter SQL語句中的參數
21  // RowBounds 分頁限制,相當於SQL中的limit.
22  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
23    try {
24      //根據statement id找到對應的MappedStatement
25      MappedStatement ms = configuration.getMappedStatement(statement);
26      //轉而用執行器來查詢結果,注意這裡傳入的ResultHandler是null
27      // wrapCollection:如果參數是Collection類型,轉換成Map,key為parameter的type.
28      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
29    } catch (Exception e) {
30
31      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
32    } finally {
33      ErrorContext.instance().reset();
34    }
35  }
36
37
38  // 接下來是執行器的部分
39  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
40    //得到綁定sql,就是將參數插入映射語句裡,獲得完整的SQL
41    BoundSql boundSql = ms.getBoundSql(parameter);
42    //創建緩存Key
43    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
44    //查詢
45    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
46 }
47
48 // 執行查詢
49  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
50    // ErrorContext 是每個線程單獨使用的錯誤上下文,它是用ThreadLocal製作的.
51    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
52    //如果已經關閉,報錯
53    if (closed) {
54      throw new ExecutorException("Executor was closed.");
55    }
56    //先清局部緩存,再查詢.但僅查詢堆棧為0,才清。為了處理遞歸調用
57    if (queryStack == 0 && ms.isFlushCacheRequired()) {
58      clearLocalCache();
59    }
60    List<E> list;
61    try {
62      //加一,這樣遞歸調用到上面的時候就不會再清局部緩存了
63      queryStack++;
64      //先根據cachekey從localCache去查
65      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
66      if (list != null) {
67        //若查到localCache緩存,處理localOutputParameterCache
68        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
69      } else {
70        //從數據庫查
71        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
72      }
73    } finally {
74      //清空堆棧
75      queryStack--;
76    }
77    if (queryStack == 0) {
78      //延遲加載隊列中所有元素
79      for (DeferredLoad deferredLoad : deferredLoads) {
80        deferredLoad.load();
81      }
82
83      // issue #601
84      //清空延遲加載隊列
85      deferredLoads.clear();
86      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
87        // issue #482
88    	//如果是STATEMENT,清本地緩存
89        clearLocalCache();
90      }
91    }
92    return list;
93  }

以上是SqlSession.selectOne的流程。然而實際中我們直接使用SqlSession來執行數據庫操作的情況很少。

大多數情況我們會這樣使用MyBatis:

1try (SqlSession session = sqlSessionFactory.openSession()) {
2  BlogMapper mapper = session.getMapper(BlogMapper.class);
3  Blog blog = mapper.selectBlog(101);
4}

這個方法的詳細過程我們使用MyBatis的單元測試來探索。

單元測試代碼:

 1  @Test
 2  public void shouldSelectBlogWithPostsUsingSubSelect() throws Exception {
 3    SqlSession session = sqlSessionFactory.openSession();
 4    try {
 5      BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class);
 6      Blog b = mapper.selectBlogWithPostsUsingSubSelect(1);
 7      assertEquals(1, b.getId());
 8      session.close();
 9      assertNotNull(b.getAuthor());
10      assertEquals(101, b.getAuthor().getId());
11      assertEquals("jim", b.getAuthor().getUsername());
12      assertEquals("********", b.getAuthor().getPassword());
13      assertEquals(2, b.getPosts().size());
14    } finally {
15      session.close();
16    }
17  }

快進到session.getMapper

 1  //返回代理類
 2  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
 3    // 直接得到該類型Mapper代理工廠
 4    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
 5    // 沒有就離譜
 6    if (mapperProxyFactory == null) {
 7      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
 8    }
 9    try {
10      // 通過當前Session生產一個代理
11      return mapperProxyFactory.newInstance(sqlSession);
12    } catch (Exception e) {
13      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
14    }
15  }

實際上生產Mapper的邏輯並不多。

主要是執行代理方法時的動作。

執行mapper.selectBlogWithPostsUsingSubSelect(1);的邏輯如下:

 1  // 由於是代理生成的,所以調用方法後會進入一下邏輯:
 2  @Override
 3  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 4    // 如果這個方法是來自Object,就直接執行,直接返回
 5    if (Object.class.equals(method.getDeclaringClass())) {
 6      try {
 7        return method.invoke(this, args);
 8      } catch (Throwable t) {
 9        throw ExceptionUtil.unwrapThrowable(t);
10      }
11    }
12
13    // 去緩存中找MapperMethod,第一次的話會new一個
14    final MapperMethod mapperMethod = cachedMapperMethod(method);
15    //執行本體
16    return mapperMethod.execute(sqlSession, args);
17  }

下面就是整個代理的執行數據庫操作的邏輯,比較長:

 1  public Object execute(SqlSession sqlSession, Object[] args) {
 2    Object result;
 3    //可以看到執行時就是4種情況,insert|update|delete|select,分別調用SqlSession的4大類方法
 4    if (SqlCommandType.INSERT == command.getType()) {
 5      Object param = method.convertArgsToSqlCommandParam(args);
 6      result = rowCountResult(sqlSession.insert(command.getName(), param));
 7    } else if (SqlCommandType.UPDATE == command.getType()) {
 8      Object param = method.convertArgsToSqlCommandParam(args);
 9      result = rowCountResult(sqlSession.update(command.getName(), param));
10    } else if (SqlCommandType.DELETE == command.getType()) {
11      Object param = method.convertArgsToSqlCommandParam(args);
12      result = rowCountResult(sqlSession.delete(command.getName(), param));
13    } else if (SqlCommandType.SELECT == command.getType()) {
14      // 我們執行的是查詢,直接跳到這裡
15      if (method.returnsVoid() && method.hasResultHandler()) {
16        // 檢查是不是沒有返回值以及結果處理器: 我們執行的是查詢,是有返回值的
17        executeWithResultHandler(sqlSession, args);
18        result = null;
19      } else if (method.returnsMany()) {
20        //如果結果有多條記錄:我們只查一條
21        result = executeForMany(sqlSession, args);
22      } else if (method.returnsMap()) {
23        //如果結果是map:我們查的只是個對象
24        result = executeForMap(sqlSession, args);
25      } else {
26        //否則就是一條記錄
27        // 我們仔細分析這個convertArgsToSqlCommandParam方法
28        Object param = method.convertArgsToSqlCommandParam(args);
29        // 之後我們又回到了SelectOne這個方法。
30        result = sqlSession.selectOne(command.getName(), param);
31      }
32    } else {
33      throw new BindingException("Unknown execution method for: " + command.getName());
34    }
35    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
36      throw new BindingException("Mapper method '" + command.getName()
37          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
38    }
39    return result;
40  }
41
42
43  // 將參數轉換為SQL命令參數
44  public Object convertArgsToSqlCommandParam(Object[] args) {
45      // 這裡有一個坑:
46      // args 是Mapper方法執行的參數
47      // param 是編寫的SQL語句所需要的命令參數
48      // 它們有什麼不同呢:
49      // 在MyBatis中你需要使用分頁時可以不顯式在SQL語句中使用limit命令
50      // 使用RowBounds對象作為Mapper的額外參數來做到數據分頁
51      // 該參數不用在SQL語句中顯式使用.
52      final int paramCount = params.size();
53      if (args == null || paramCount == 0) {
54        //如果沒參數
55        return null;
56      } else if (!hasNamedParameters && paramCount == 1) {
57        //如果只有一個參數
58        return args[params.keySet().iterator().next().intValue()];
59      } else {
60        //否則,返回一個ParamMap,修改參數名,參數名就是其位置
61        final Map<String, Object> param = new ParamMap<Object>();
62        int i = 0;
63        for (Map.Entry<Integer, String> entry : params.entrySet()) {
64          //1.先加一個#{0},#{1},#{2}...參數
65          param.put(entry.getValue(), args[entry.getKey().intValue()]);
66          // issue #71, add param names as param1, param2...but ensure backward compatibility
67          final String genericParamName = "param" + String.valueOf(i + 1);
68          if (!param.containsKey(genericParamName)) {
69            //2.再加一個#{param1},#{param2}...參數
70            //你可以傳遞多個參數給一個映射器方法。如果你這樣做了,
71            //默認情況下它們將會以它們在參數列表中的位置來命名,比如:#{param1},#{param2}等。
72            //如果你想改變參數的名稱(只在多參數情況下) ,那麼你可以在參數上使用@Param(“paramName”)註解。
73            param.put(genericParamName, args[entry.getKey()]);
74          }
75          i++;
76        }
77        return param;
78      }
79    }

OK, 我基本想說的都說完了。後續可能會額外補充一些內容,但是不會在本文中,會寫新文章。