本文作者:孔维胜,叩丁狼高级讲师。原创文章,转载请注明出处。
基于Mapper接口动态代理实现原理
看文章前的技术要求
在学习MyBatis的初级篇之前,有两个前提要求,第一.必须学会使用IDEA,因为在文章中,使用的工具为IDEA,文章中的案例也都是基于IDEA的。第二.必须学会使用MAVEN,因为在案例中需要的jar包,都是通过MAVEN来管理的。
文章中的案例的开发环境
JDK 1.8
IDEA 2017.3
MySQL 5.1.38
Apache Maven 3.5.0
Tomcat 9.0.6
MyBatis 3.4.6
文章中的案例需要的表和数据
我们使用MyBatis的目的最终是访问数据库,所以在数据库方面,我们先创建相应的数据库,表,导入相关的数据。如:
1.创建mybatis数据库。
2.在mybatis数据库中创建department(部门表)。
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '部门ID',
`name` varchar(20) DEFAULT NULL COMMENT '部门名称',
`sn` varchar(20) DEFAULT NULL COMMENT '部门缩写',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
3.准备department(部门表相关的数据)
INSERT INTO `department` VALUES (1, '人力资源', 'HR_DEPT');
INSERT INTO `department` VALUES (2, '销售部', 'SALE_DEPT');
INSERT INTO `department` VALUES (3, '开发部', 'DEVELOP_DEPT');
INSERT INTO `department` VALUES (4, '财务部', 'FINANCE_DEPT');
案例的相关代码
原生DAO的方式
这里就不贴代码了,我们直接看代码图。如:
看源码之前的回顾与思考
回顾:
在看这篇文章之前,我们先来回顾一下,没有使用基于Mapper接口动态代理的方式。我们是通过采用原生DAO的方式来调用Mybatis的接口的。存在什么问题呢?我们先来看一段代码:
@Override
public List selectAll() {
// 获取sqlSession对象
SqlSession sqlSession = MyBatisUtil.openSession();
// 执行SQL 参数的内容:(namespace的值 + "." + sql 的 id值)
List empList =
sqlSession.selectList("cn.wolfcode.mapper.DepartmentMapper.selectAll");
// 关闭资源
sqlSession.close();
return empList;
}
这是一个操作数据库中的删除方法。我们都知道selectList方法中的参数。参数是找到sql语句的唯一标识(namespace + "." + sql 的 id值)。
思考:
上述的写法,其实是存在硬编码问题的。为何这样讲呢?我们试想一下,如果namespace 或者 sql的id的值,任何一个被修改了,那么在代码中就要相应的跟着修改。所以我们应该从这里找到线索,或者突破,目的是解决存在的硬编码的问题。
如果能让selectList方法中的参数,通过调用什么方法自动组装起来,而不是写死在其中,这样解决硬编码的思路就有了,我们可以定义一个规则,不让用户随意的去填写namespace的值 和 sql的id的值。我们把这两个值给约束起来,编写时很有规律可循,规定必须使用什么命名方式,这样一来,我们就可以很容易获取到它们。
所以我们首要解决的问题是定义什么规则,既要有规律性,还要保证唯一性。我们现有的有DAO接口和对应的实现类。所以从现有资源上寻找突破口。
sql的id的值的思考:
先来说一下sql的id,我们都知道sql的id是用来区分sql语句的。在同一个映射文件中,要保证其唯一性。那么我们现有的资源,DAO接口就和我们的sql的id的命名要求相符合,在一个接口中,抽象方法是重复的。我们就可以利用这一点,让方法的名称作为sql的id,这样也确保了在一个mapper文件中sql的id的值不会重复。
namespace值的思考:
再来说一下namespace,我一定要确保这值必须是唯一的,因为sql的id的值在整个项目中可能出现重复的现象。DAO接口中的方法名称被我们利用上了。让方法做了sql的id的值,那么我们还可以利用DAO接口的权限定名作为namespace的值,因为DAO接口的权限定名也是唯一的,这样一来也是符合我们的需求的。
所以综上所述,我们使用DAO的接口的权限定名作为namespace的值,使用接口中的方法名作为sql的id的值,这样我们把规则制定好了。接下来就是如何获取namespace的值和sql的id的值的问题。
获取namespace的值和sql的id的值的问题思考:
这个获取的问题还是很好解决的,因为我们前面学过反射技术,我只要拿到DAO接口的字节码对象,我就可以获取对应的权限定名和里面的方法名称。
我们可以把获取的操作封装到一个方法中,调用该方法就可以获取selectList方法中的参数。如:
public static String getParams(Class clz, String methodName) {
// 获取字节码对象的权限定名
String canonicalName = clz.getCanonicalName();
// 拼接数据
return canonicalName + "." + methodName;
}
存在一个不足的地方,就是要在每次执行selectList方法之前调用一次该方法获取参数。
那思考以前学过的知识中,有没有一种方式,在执行selectList方法之前,进行拦截。让其真正执行的是我的拦截的逻辑。是有的,可以使用代理对象。
在java中我们学过代理类(Proxy)。在使用这个Proxy是有个前提的,就是在java中规定,要想产生一个对象的代理对象,那么这个对象必须要有一个接口,而我们正好是符合要求的。所以我们可以使用代理类来完成参数的拼接。
代理类的定义:
定义一个类实现InvocationHandler接口。覆写invoke方法,我们就可以在invoke方法中完成操作。
既然使用代理类,可以完成参数的拼接,那么在DAO的实现类中,除了参数的拼接,还有就是使用SqlSesion对象调用方法查询数据了。那么我们何不让代理类连查询数据的功能都一并完成呢。 这样我们的实现类都可以不用写了。
因此最终我们让代理对象,帮我们完成参数的拼接并访问数据库返回结果。所以我们需要往代理类中传入接口的字节码对象(为了获取接口的权限定名)和sqlSession对象(为了调用查询方法)。在这里我们通过构造器传入这两个参数。
代码如下:
public class MyMapperProxy implements InvocationHandler {
private Class mapperInterface;
private SqlSession sqlSession;
public MyMapperProxy(Class mapperInterface , SqlSession sqlSession){
this.mapperInterface = mapperInterface;
this.sqlSession = sqlSession;
}
//针对不同的 sql 类型,需要调用 sqlSession 不同的方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//接口方法中的参数也有很多情况 ,这里只考虑没有有参数的情况
List list= sqlSession.selectList(
mapperInterface.getCanonicalName() +"."+ method .getName());
//返回位也有很多情况,这里不做处理直接返回
return list;
}
}
注意:不管外部调用调用代理对象的什么方法,最终都是调用invoke方法(这相当于invoke方法拦截到了代理对象的方法调用)。所以我们把逻辑放入到invoke方法中。
定义了好代理类,接下来我们就可以创建一个我们需要的代理对象。 如:
MyMapperProxy myMapperProxy = new MyMapperProxy(IDepartmentDAO.class,sqlSession);
下面我们就可以使用Proxy类调用newProxyInstance方法来获取mapper的接口的代理接口。然后由代理接口去调用相应的方法。
完整的代码如下:
@Test
public void testProxy() {
// 获取 sqlSession
SqlSession sqlSession = factory.openSession();
// 创建代理对象
MyMapperProxy myMapperProxy = new MyMapperProxy(IDepartmentDAO.class, sqlSession);
// 获取 UserMapper接口
IDepartmentDAO instance = (IDepartmentDAO) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),// 类加载器
new Class[]{IDepartmentDAO.class},// 接口的字节码对象
myMapperProxy); //自定义的代理类
// 调用 selectAllEmployee 方法
List departmentList = instance.selectAll();
// 遍历结果
departmentList.stream().forEach(System.out::println);
}
以上我们是通过自己的分析,自己写了一个简易版的处理方式。这里就不贴完整代码了,我们直接看代码图。
那么我们看看MyBatis给我们提供的基于Mappper动态代理的真实调用方式是怎么样的。
基于Mapper动态代理的方式
这里就不贴代码了,我们直接看代码图。如:
我们通过debug来看一下源码执行流程:
-
1 . 通过sqlSession对象调用getMapper方法,传入接口的字节码对象。如:
-
2 . 在默认的DefaultSqlSession类中,没有具体的处理,而是调用了全局配置对象(configuration)中的getMapper方法,并把当前对象(DefaultSqlSession)作为参数传入getMapper方法中。如:
-
3 . 在全局配置对象(configuration)中,并没有处理方法,而是交给了mapper的注册对象(mapperRegistry)去处理。如:
-
4 . 回忆之前在解析mapper的映射文件时,定义一个map容器(knownMappers),把接口的字节码对象作为key,接口的代理工厂作为value。
在mapper的注册类(MapperRegistry)中,先取出mapper的代理工厂。通过调用newInstance方法,把sqlSession对象作为参数传入。如:
-
5 . 把sqlSession,mapper的字节码对象作为参数,创建代理类对象。如:
-
6 . 然后把代理类对象作为参数,底层通过JDK的动态代理,返回mapper的代理对象。
-
7 .通过调用getMapper方法返回了DepartmentMapper的代理对象。接着调用selectAll方法。如:
-
8 . 执行selectAll方法,最终被代理类中的invoke方法拦截。条件不满足,最终执行execute方法。如:
-
9 . 在execute方法中,先判断方法的类型,我们这里是查询方法,所以进入SELECT中,然后再判断方法的返回结果类型,我们是查询的全部数据,所以执行executeForMany方法。如:
- 10 .在这个方法中最终还是通过sqlSession对象调用selectList方法,来获取数据。如:
通过分析源码,发现源码中的做法和我们最初思考的,设计的,也很类似,底层都是利用JDK的动态代理方式。接下来我们再用一个完整的流程图来结束这篇文章。
如:
想获取更多技术干货,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html