基于Mapper接口动态代理实现原理

本文作者:孔维胜,叩丁狼高级讲师。原创文章,转载请注明出处。

基于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接口动态代理实现原理_第1张图片
原生DAO的案例代码图.png

看源码之前的回顾与思考

回顾:

在看这篇文章之前,我们先来回顾一下,没有使用基于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);
    }

以上我们是通过自己的分析,自己写了一个简易版的处理方式。这里就不贴完整代码了,我们直接看代码图。


基于Mapper接口动态代理实现原理_第2张图片
简易版的动态代理的案例代码图.png

那么我们看看MyBatis给我们提供的基于Mappper动态代理的真实调用方式是怎么样的。

基于Mapper动态代理的方式

这里就不贴代码了,我们直接看代码图。如:


基于Mapper接口动态代理实现原理_第3张图片
Mapper动态代理的案例代码图.png

我们通过debug来看一下源码执行流程:

  • 1 . 通过sqlSession对象调用getMapper方法,传入接口的字节码对象。如:


    基于Mapper接口动态代理实现原理_第4张图片
    DepartmentTest.png
  • 2 . 在默认的DefaultSqlSession类中,没有具体的处理,而是调用了全局配置对象(configuration)中的getMapper方法,并把当前对象(DefaultSqlSession)作为参数传入getMapper方法中。如:


    DefaultSqlSession.png
  • 3 . 在全局配置对象(configuration)中,并没有处理方法,而是交给了mapper的注册对象(mapperRegistry)去处理。如:


    Configuration.png
  • 4 . 回忆之前在解析mapper的映射文件时,定义一个map容器(knownMappers),把接口的字节码对象作为key,接口的代理工厂作为value。

    在mapper的注册类(MapperRegistry)中,先取出mapper的代理工厂。通过调用newInstance方法,把sqlSession对象作为参数传入。如:


    基于Mapper接口动态代理实现原理_第5张图片
    MapperRegistry.png
  • 5 . 把sqlSession,mapper的字节码对象作为参数,创建代理类对象。如:


    MapperProxyFactory.png
  • 6 . 然后把代理类对象作为参数,底层通过JDK的动态代理,返回mapper的代理对象。


    MapperProxyFactory.png
  • 7 .通过调用getMapper方法返回了DepartmentMapper的代理对象。接着调用selectAll方法。如:


    基于Mapper接口动态代理实现原理_第6张图片
    DepartmentTest.png
  • 8 . 执行selectAll方法,最终被代理类中的invoke方法拦截。条件不满足,最终执行execute方法。如:


    基于Mapper接口动态代理实现原理_第7张图片
    MapperProxy.png
  • 9 . 在execute方法中,先判断方法的类型,我们这里是查询方法,所以进入SELECT中,然后再判断方法的返回结果类型,我们是查询的全部数据,所以执行executeForMany方法。如:


    基于Mapper接口动态代理实现原理_第8张图片
    MapperMethod.png
  • 10 .在这个方法中最终还是通过sqlSession对象调用selectList方法,来获取数据。如:
    基于Mapper接口动态代理实现原理_第9张图片
    MapperMethod.png

通过分析源码,发现源码中的做法和我们最初思考的,设计的,也很类似,底层都是利用JDK的动态代理方式。接下来我们再用一个完整的流程图来结束这篇文章。
如:


Mapper动态代理的原理分析.png

想获取更多技术干货,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html

你可能感兴趣的:(基于Mapper接口动态代理实现原理)