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

时间: 2024-10-09 22:54:47

MyBatis 源码分析——动态代理的相关文章

MyBatis 源码分析——动态SQL语句

有几年开发经验的程序员应该都有暗骂过原生的SQL语句吧.因为他们不能一句就搞定一个业务,往往还要通过代码来拼接相关的SQL语句.相信大家会理解SQL里面的永真(1=1),永假(1=2)的意义吧.所以mybatis动态SQL功能在笔者看来是最引吸人的.为了更好的区别XML映射文件上的SQL语句.mybatis把SQL语句分为四类.那么这个笔者已经在前面的章节里面讲过了.但是我们在开发过程中常常用到的也就俩种:静态和动态. 关于静态和动态的定义,笔者是这样子理解的--静态SQL语句显示就是里面没有相

MyBatis源码分析-SQL语句执行的完整流程

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis框架主要完成的是以下2件事情: 根据JD

MyBatis 源码分析 - 配置文件解析过程

* 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAliases和typeHandlers等,本文的篇幅也主要在对这三个配置解析过程的分析上.下面,我们来一起看一下本篇文章的目录结构. 从目录上可以看出,2.3节.2.5节和2.8节的内容比较多.其中2.3节是关于settings配置解析过程的分析,除了对常规的 XML 解析过程分析,本节额外的分析了元

MyBatis 源码分析 - 插件机制

1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 MyBatis 为例,我们可基于 MyBatis 插件机制实现分页.分表,监控等功能.由于插件和业务无关,业务也无法感知插件的存在.因此可以无感植入插件,在无形中增强功能. 开发 MyBatis 插件需要对 MyBatis 比较深了解才行,一般来说最好能够掌握 MyBatis 的源码,门槛相对较高.本篇

Mybatis源码分析

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis框架主要完成的是以下2件事情: 根据JD

mybatis源码分析(四) mybatis与spring事务管理分析

mybatis源码分析(四) mybatis与spring事务管理分析 一丶从jdbc的角度理解什么是事务 从mysql获取一个连接之后, 默认是自动提交, 即执行完sql之后, 就会提交事务. 这种事务的范围是一条sql语句. 将该连接设置非自动提交, 可以执行多条sql语句, 然后由程序决定是提交事务, 还是回滚事务. 这也是我们常说的事务. Connection connection = dataSource.getConnection(); // connection.setTransa

【MyBatis源码分析】环境准备

前言 之前一段时间写了[Spring源码分析]系列的文章,感觉对Spring的原理及使用各方面都掌握了不少,趁热打铁,开始下一个系列的文章[MyBatis源码分析],在[MyBatis源码分析]文章的基础之上,可以继续分析数据库连接池.Spring整合MyBatis源码.Spring事物管理tx等等. [MyBatis源码分析]整个文章结构相较[Spring源码分析]稍微改一改,后者会在每一部分源码分析的开头列出要分析的源码的实例,比如: 分析Bean流程加载,就会先写Bean的代码示例及xml

【MyBatis源码分析】select源码分析及小结

示例代码 之前的文章说过,对于MyBatis来说insert.update.delete是一组的,因为对于MyBatis来说它们都是update:select是一组的,因为对于MyBatis来说它就是select. 本文研究一下select的实现流程,示例代码为: 1 public void testSelectOne() { 2 System.out.println(mailDao.selectMailById(8)); 3 } selectMailById方法的实现为: 1 public M

MyBatis 源码分析系列文章合集

1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章中,有4篇文章字数超过了1万,最长的一篇文章约有2.7万字(含代码).考虑到超长文章对读者不太友好,以及拆分文章工作量也不小等问题.遂决定将博文整理成电子书,方便大家阅读. 经过两周紧张的排版,<一本小小的MyBatis源码分析书>诞生了.本书共7章,约300页.本书以电子书的形式发布,大家可自由