MyBatis 源碼分析——動態代理


MyBatis框架是如何去執行SQL語句?相信不只是你們,筆者也想要知道是如何進行的。相信有上一章的引導大家都知道SqlSession接口的作用。當然默認情況下還是使用DefaultSqlSession類。關於SqlSession接口的用法有很多種。筆者還是比較喜歡用getMapper方法。對於getMapper方法的實現方式。筆者不能下一個定論。筆者只是想表示一下自己的理解而以——動態代理。

筆者把關於getMapper方法的實現方式理解為動態代理。事實上筆者還想說他可以是一個AOP思想的實現。那么具體是一個什么樣子東西。相信筆者說了也不能代表什么。一切還是有大家自己去查看和理解。從源碼上我們可以看到getMapper方法會去調用Configuration類的getMapper方法。好了。一切的開始都在這里了。

DefaultSqlSession類:

 public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }

對於Configuration類上一章里面就說明他里面存放了所有關於XML文件的配置信息。從參數上我們可以看到他要我們傳入一個Class<T>類型。這已經可以看到后面一定要用到反射機制和動態生成相應的類實例。讓我們進一步查看一下源碼。

Configuration類:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

當筆者點擊進來發現他又調用MapperRegistry類的getMapper方法的時候,心里面有一種又恨又愛的沖動——這就是構架之美和復雜之恨。MapperRegistry類筆者把他理解存放動態代理工廠(MapperProxyFactory類)的庫存。當然我們還是進去看一看源碼吧。

MapperRegistry類:

 1  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
 2     final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
 3     if (mapperProxyFactory == null) {
 4       throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
 5     }
 6     try {
 7       return mapperProxyFactory.newInstance(sqlSession);
 8     } catch (Exception e) {
 9       throw new BindingException("Error getting mapper instance. Cause: " + e, e);
10     }
11   }

好了。筆者相信大家看到這一段代碼的時候都明白——MapperRegistry類就是用來存放MapperProxyFactory類的。我們還是在看一下knownMappers成員是一個什么要樣子的集合類型。

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

knownMappers是一個字典類型。從Key的類型上我們可以判斷出來是一個類一個動態代理工廠。筆者看到這里的時候都會去點擊一個MapperProxyFactory類的源碼。看看他里面又是一些什么東東。

 1 public class MapperProxyFactory<T> {
 2 
 3   private final Class<T> mapperInterface;
 4   private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
 5 
 6   public MapperProxyFactory(Class<T> mapperInterface) {
 7     this.mapperInterface = mapperInterface;
 8   }
 9 
10   public Class<T> getMapperInterface() {
11     return mapperInterface;
12   }
13 
14   public Map<Method, MapperMethod> getMethodCache() {
15     return methodCache;
16   }
17 
18   @SuppressWarnings("unchecked")
19   protected T newInstance(MapperProxy<T> mapperProxy) {
20     return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
21   }
22 
23   public T newInstance(SqlSession sqlSession) {
24     final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
25     return newInstance(mapperProxy);
26   }
27 
28 }

還好。代碼不是很多,理解起來也不是很復雜。略看一下源碼,筆者做了一個很大膽的猜測——一個類,一個動態代理工廠,多個方法代理。我們先把猜測放在這里,然后讓我們回到上面部分吧。我們發現MapperRegistry類的getMapper方法里面最后會去調用MapperProxyFactory類的newInstance方法。這個時候我們又看到他實例化了一個MapperProxy類。MapperProxy類是什么。這個就關系到Proxy類的用法了。所以讀者們自己去查看相關資料了。意思明顯每執行一次XxxMapper(例如:筆者例子里面的IProductMapper接口)的方法都會創建一個MapperProxy類。方法執行之前都會先去調用相應MapperProxy類里面的invoke方法。如下

MapperProxy類:

 1 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 2     if (Object.class.equals(method.getDeclaringClass())) {
 3       try {
 4         return method.invoke(this, args);
 5       } catch (Throwable t) {
 6         throw ExceptionUtil.unwrapThrowable(t);
 7       }
 8     }
 9     final MapperMethod mapperMethod = cachedMapperMethod(method);
10     return mapperMethod.execute(sqlSession, args);
11   }

從源碼的意思:從緩存中獲得執行方法對應的MapperMethod類實例。如果MapperMethod類實例不存在的情況,創建加入緩存並返回相關的實例。最后調用MapperMethod類的execute方法。

到這里筆者小結一下,上面講到筆者例子里面用到的getMapper方法。getMapper方法就是用來獲得相關的數據操作類接口。而事實數據操作類邦定了動態代理。所以操據操作類執行方法的時候,會觸動每個方法相應的MapperProxy類的invoke方法。所以invoke方法返回的結果就是操據操作類執行方法的結果。這樣子我們就知道最后的任務交給了MapperMethod類實例。

MapperMethod類里面有倆個成員:SqlCommand類和MethodSignature類。從名字上我們大概的能想到一個可能跟SQL語句有關系,一個可能跟要執行的方法有關系。事實也是如此。筆者查看了SqlCommand類的源碼。確切來講這一部分的內容跟XxxMapper的XML配置文件里面的select節點、delete節點等有關。我們都會知道節點上有id屬性值。那么MyBatis框架會把每一個節點(如:select節點、delete節點)生成一個MappedStatement類。要找到MappedStatement類就必須通過id來獲得。有一個細節要注意:代碼用到的id = 當前接口類 + XML文件的節點的ID屬性。如下

 1 public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
 2       String statementName = mapperInterface.getName() + "." + method.getName();
 3       MappedStatement ms = null;
 4       if (configuration.hasStatement(statementName)) {
 5         ms = configuration.getMappedStatement(statementName);
 6       } else if (!mapperInterface.equals(method.getDeclaringClass())) { // issue #35
 7         String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
 8         if (configuration.hasStatement(parentStatementName)) {
 9           ms = configuration.getMappedStatement(parentStatementName);
10         }
11       }
12       if (ms == null) {
13         if(method.getAnnotation(Flush.class) != null){
14           name = null;
15           type = SqlCommandType.FLUSH;
16         } else {
17           throw new BindingException("Invalid bound statement (not found): " + statementName);
18         }
19       } else {
20         name = ms.getId();
21         type = ms.getSqlCommandType();
22         if (type == SqlCommandType.UNKNOWN) {
23           throw new BindingException("Unknown execution method for: " + name);
24         }
25       }
26     }

看到這里的時候,我們就可以回頭去找一找在什么時候增加了MappedStatement類。上面之所以可以執行是建立在XML配置信息都被加載進來了。所以MappedStatement類也一定是在加載配置的時候就進行的。請讀者們自行查看一下MapperBuilderAssistant類的addMappedStatement方法——加深理解。SqlCommand類的name成員和type成員我們還是關注一下。name成員就是節點的ID,type成員表示查尋還是更新或是刪除。至於MethodSignature類呢。他用於說明方法的一些信息,主要有返回信息。

筆者上面講了這多一點主要是為了查看execute方法源碼容易一點。因為execute方法都要用到SqlCommand類和MethodSignature類。

 1   public Object execute(SqlSession sqlSession, Object[] args) {
 2     Object result;
 3     switch (command.getType()) {
 4       case INSERT: {
 5         Object param = method.convertArgsToSqlCommandParam(args);
 6         result = rowCountResult(sqlSession.insert(command.getName(), param));
 7         break;
 8       }
 9       case UPDATE: {
10         Object param = method.convertArgsToSqlCommandParam(args);
11         result = rowCountResult(sqlSession.update(command.getName(), param));
12         break;
13       }
14       case DELETE: {
15         Object param = method.convertArgsToSqlCommandParam(args);
16         result = rowCountResult(sqlSession.delete(command.getName(), param));
17         break;
18       }
19       case 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         }
33         break;
34       case FLUSH:
35         result = sqlSession.flushStatements();
36         break;
37       default:
38         throw new BindingException("Unknown execution method for: " + command.getName());
39     }
40     if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
41       throw new BindingException("Mapper method '" + command.getName() 
42           + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
43     }
44     return result;
45   }

重點部分就是這里,我們會發現我們轉了一圈,最后還是要回到SqlSession接口實例上。完美的一圈!筆者用紅色標出來了。

看到了這里我們就清楚調頭去看一下SqlSession接口實例吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM