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 接口实例吧。