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

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


原生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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容