美文网首页Mybatis
基于Mapper接口动态代理实现原理

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

作者: 叩丁狼教育 | 来源:发表于2019-01-05 09:58 被阅读601次

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

    基于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的方式

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


    原生DAO的案例代码图.png

    看源码之前的回顾与思考

    回顾:

    在看这篇文章之前,我们先来回顾一下,没有使用基于Mapper接口动态代理的方式。我们是通过采用原生DAO的方式来调用Mybatis的接口的。存在什么问题呢?我们先来看一段代码:

    @Override
    public List<Department> selectAll() {
         // 获取sqlSession对象
         SqlSession sqlSession = MyBatisUtil.openSession();
         // 执行SQL 参数的内容:(namespace的值 + "." + sql 的 id值)
         List<Department> 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<T> implements InvocationHandler {
    
        private Class<T> mapperInterface;
        private SqlSession sqlSession;
    
        public MyMapperProxy(Class<T> mapperInterface , SqlSession sqlSession){
            this.mapperInterface = mapperInterface;
            this.sqlSession = sqlSession;
        }
    
        //针对不同的 sql 类型,需要调用 sqlSession 不同的方法
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       //接口方法中的参数也有很多情况 ,这里只考虑没有有参数的情况 
       List<T> 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<Department> departmentList = instance.selectAll();
            // 遍历结果
            departmentList.stream().forEach(System.out::println);
        }
    

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


    简易版的动态代理的案例代码图.png

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

    基于Mapper动态代理的方式

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


    Mapper动态代理的案例代码图.png

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

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


      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对象作为参数传入。如:


      MapperRegistry.png
    • 5 . 把sqlSession,mapper的字节码对象作为参数,创建代理类对象。如:


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


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


      DepartmentTest.png
    • 8 . 执行selectAll方法,最终被代理类中的invoke方法拦截。条件不满足,最终执行execute方法。如:


      MapperProxy.png
    • 9 . 在execute方法中,先判断方法的类型,我们这里是查询方法,所以进入SELECT中,然后再判断方法的返回结果类型,我们是查询的全部数据,所以执行executeForMany方法。如:


      MapperMethod.png
    • 10 .在这个方法中最终还是通过sqlSession对象调用selectList方法,来获取数据。如:
      MapperMethod.png

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


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

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

    相关文章

      网友评论

        本文标题:基于Mapper接口动态代理实现原理

        本文链接:https://www.haomeiwen.com/subject/kifrrqtx.html