其實很早就想寫一篇 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, 我基本想說的都說完了。後續可能會額外補充一些內容,但是不會在本文中,會寫新文章。