首页 > 其他分享 >为什么Mybatis Mapper不需要实现类?

为什么Mybatis Mapper不需要实现类?

时间:2024-04-23 15:00:54浏览次数:15  
标签:Mapper 为什么 对象 代理 接口 Mybatis 动态

在学习Java动态代理之前,我想让大家先思考这样几个问题。

JDK动态代理为什么不能对类进行代理?
Mybatis Mapper接口为什么不需要实现类?

目录
image

代理模式

说到Java动态代理,就不得不提代理模式。为什么要使用代理模式呢?
生活中对代理模式的使用无处不在,例如明星经纪人对明星业务的代理;律师对原告官司的代理;4s店对汽车制造商的销售代理等等。这些使用场景告诉我们代理模式的本质是:
代理对象为被代理对象的某种行为提供增强服务。
如何理解“增强”两个字?它的含义其实可以理解为“访问控制”或“业务托管”。所以现在你能告诉我代理模式的作用了吗?在某些不适合直接访问目标对象的情况下,代理对象可以为目标对象提供访问控制和业务托管。
image

代理三要素

  • 共同的行为。即接口;
  • 目标对象。即被代理对象或业务对象 ,目标对象实现了接口;
  • 代理对象。即代理对象或增强对象,增强了目标对象的行为。

既然代理模式的作用是访问控制和业务托管,那么将这种模式映射到面向对象的模型里去如何理解?
基于代理模式的特点,我们明白了代理对象如果想要对目标对象的行为增强,首先,它们必须有共同的行为,在代码里我们可以让它们实现同一个接口;其次,在实现目标对象的业务时,代理对象不是自己去实现,而是以某种方式调用目标对象的实现方法,一般常用的方式是将目标对象和代理对象组合在一起,代理对象调用目标对象的方法。

静态代理

实际上代理模式还可以具体划分为静态代理和动态代理。生活中的大多数代理都是类似于静态代理的设计。静态代理有以下几个特点:

特点

目标对象固定,在执行前就能确定目标对象
代理对象会对目标对象的行为增强
每个目标对象都需要一个代理,会造成代理泛滥

代码实现

我们结合代码看下静态代理的实现原理。就拿律师代理为例,首先我们创建一个接口作为原告和律师共同的行为----收集打官司的证据。

    void collect();
}

如果是原告自己为打官司收集证据,我们创建上面接口的实现类作为目标对象。

    @Override
    public void collect() {
        System.out.println("原告收集证据!");
    }
}

此时原告找到自己的律师,让律师代理自己收集证据时,我们创建代理类。

    private LawEvidence lawEvidence;

    public LawEvidenceProxy(LawEvidence lawEvidence) {
        this.lawEvidence = lawEvidence;
    }

    @Override
    public void collect() {
        System.out.print("律师向原告了解案情,并代替" );
        this.lawEvidence.collect();
    }
}

新建一个客户端,看下如何使用代理律师给原告收集打官司的证据。

    public static void main(String[] args) {
        LawEvidence lawEvidenceProxy = new LawEvidenceProxy(new LawEvidenceImpl());
        lawEvidenceProxy.collect();
    }
}

运行结果如图所示。客户端里我们创建了一个目标对象LawEvidenceImpl,然后封装到代理对象LawEvidenceProxy里,调用代理对象的收集证据方法。
image

看下运行结果。本来应该显示原告自己收集证据的,但是这里使用了静态代理,所以就变成了“律师向原告了解案情,并代替原告收集证据”。

客户端的代码告诉我们,虽然静态代理LawEvidenceProxy能够对目标对象LawEvidenceImpl增强,但是如果现在我想要再对原告增强一个其他的行为,例如律师代替原告打官司,这个时候就不得不新增一个打官司的代理对象,如果代理的行为越来越多,就会造成代理泛滥。

动态代理

面向对象的代码世界必然比生活中的应用灵活度要更高一些。为了解决静态代理的代理泛滥的问题,我们经常会用到或者看别人用到动态代理模式,例如路由器对光猫访问互联网的代理、Mybatis插件对Mybatis执行器的代理、Mybatis动态Sql,Spring AOP等都是对动态代理的实践。归结起来,动态代理有以下几个特点:

特点

  • 目标对象不确定,在执行时动态创建
  • 代理对象会对目标对象的行为增强

两者的区别

由动态代理和静态代理的特点,我们能够很轻易的得出一个结论:它们最大不同点是目标对象在执行前是否确定。
如何理解“目标对象不确定”这句话?我们再来回顾一下静态代理模式的类图。Proxy(代理)包含的属性是realSubject对象,即目标对象,它并不是一个抽象的实体。那我们如果将realSubject换成Subject接口会怎么样呢?这下应该是动态代理模式了吧?但是这样又会有一个问题,Subject接口的doOperation()方法是固定的。所以为了解决这个问题,我们需要在运行时动态构造一个目标对象,并将它封装到一个动态的代理对象里。
image

doOperation()方法如果想要通用必须至少满足以下三点:

  • 目标对象是谁?
  • 调用目标对象的方法是什么?
  • 调用目标对象的方法参数是什么?

只有知道了这三点,动态代理模式就能像静态代理模式一样调用不同的目标对象方法啦。而这也就是JDK动态代理中InvocationHandler的原理精髓之所在。可以说只要实现了InvocationHandler接口,就能让其自身达到通用目标对象的标准,以达到被通用代理对象使用的目的。

JDK动态代理

动态代理主要有两种实现方式:

JDK动态代理
CGLIB动态代理

我们先来看下JDK动态代理的实现。还是使用律师代理的示例作为蓝本,之前原告找律师代理的是收集证据的行为,现在如果想要让律师代理原告打官司,如何实现?
我们再创建一个打官司的接口,将它作为目标对象和代理对象的共同行为。

    void lawsuit();
}

接着创建一个原告自己打官司的实现类作为目标对象。

    @Override
    public void lawsuit() {
        System.out.println("原告打官司!");
    }
}

如果是采用静态代理模式,我们需要依葫芦画瓢给打官司的行为再创建一个代理类。

    private Lawsuit lawsuit;

    public LawsuitProxy(Lawsuit lawsuit) {
        this.lawsuit = lawsuit;
    }

    @Override
    public void lawsuit() {
        System.out.print("律师向原告了解案情,并代替" );
        this.lawsuit.lawsuit();
    }
}

我们可以写一个客户端看看采用静态代理模式,对“收集证据”和“打官司”的行为代理后是什么效果。

    public static void main(String[] args) {
        // 收集证据
        LawEvidence lawEvidenceProxy = new LawEvidenceProxy(new LawEvidenceImpl());
        lawEvidenceProxy.collect();
        // 打官司
        LawsuitProxy lawsuitProxy = new LawsuitProxy(new LawsuitImpl());
        lawsuitProxy.lawsuit();
    }
}

运行结果如图所示。
image

这个结果完全符合我们预期。因为我们提供了两个代理给原告服务。但是如果原告还需要找律师代理其他业务,难道又要创建新的代理实现类吗?这样显然会造成代理泛滥。所以这一次我们试试JDK动态代理的实现方式。

    private Object target;

    public LawHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.print("律师向原告了解案情,并代替");
        method.invoke(target, args);
        return null;
    }
}

这次我们只需要实现一个InvocationHandler接口。在增强的invoke()方法中,让律师代替原告并调用了原告的方法。写个客户端看看执行效果。

    public static void main(String[] args) {
        // 收集证据
        LawEvidence lawEvidence = new LawEvidenceImpl();
        InvocationHandler evidenceHandler = new LawHandler(lawEvidence);
        LawEvidence evidenceProxy = (LawEvidence) Proxy.newProxyInstance(lawEvidence.getClass().getClassLoader(),
                lawEvidence.getClass().getInterfaces(), evidenceHandler);
        evidenceProxy.collect();
        // 打官司
        Lawsuit lawsuit = new LawsuitImpl();
        InvocationHandler lawsuitHandler = new LawHandler(lawsuit);
        Lawsuit lawsuitProxy = (Lawsuit) Proxy.newProxyInstance(lawsuit.getClass().getClassLoader(),
                lawsuit.getClass().getInterfaces(), lawsuitHandler);
        lawsuitProxy.lawsuit();
    }
}

如图所示,和静态代理实现的结果一模一样。
image

观察客户端的代码可以发现,不论是收集证据还是打官司,亦或是后面如果想要再增加其他代理业务,都只需要两个步骤即可实现代理增强。

  • 实现InvocationHandler接口。如上面例子中的LawHandler,如果代理增强的逻辑是一样的话,都可以用这个处理器类。
  • 使用Proxy#newProxyInstance()生成一个代理类,它会返回代理对象。这个代理对象已经不是从前自己创建的目标对象了,它是被增强过的。例如上例中的evidenceProxy变量就是对lawEvidence变量的增强。

看下代码结构可以看的更加清楚,静态代理模式下有多少代理业务就创建多少代理对象(红框标注)。而JDK动态代理模式下可以共用一个LawHandler处理器(绿框标注),因为它在构造函数里的目标对象参数是抽象的。
image

反编译

JDK动态代理的实现原理是怎么样的呢?我们可以在客户端main()方法的开头加上一行代码,目的是将Proxy生成的代理类写到本地磁盘里,这样我们就能看到代理类长什么样了。

如图所示,文件夹里多了两个class文件Proxy0和Proxy0和Proxy0和Proxy1。动态代理对象的名称是有规律的,它们都以$Proxy前缀开头,后面跟着数字。
image

我把Proxy0关键的代码贴出来大家看下就一目了然了,Proxy0关键的代码贴出来大家看下就一目了然了,Proxy0关键的代码贴出来大家看下就一目了然了,Proxy1的原理和$Proxy0类似。
image

$Proxy0是Proxy的子类,并且实现了LawEvidence接口,这样它既可以是代理对象又可以是目标对象。这就能解释本文一开始就提出的一个问题:JDK动态代理为什么不能对类进行代理?因为在Java语言里不能多继承,所以Proxy#newProxyInstance()生成的对象既然已经默认继承了Proxy类,就不能再继承别的类了。因此这里通过对接口进行代理达到多态的效果。如果实在想要代理对象怎么办呢?后面介绍CGLIB时会提到,CGLIB动态代理支持对类的代理。
再来观察collect()方法,它通过调用InvocationHandler(变量h)的invoke()方法实现。之前提到过,invoke()方法的参数m3就是目标方法,它利用静态代码块在Proxy.class对象构建的时候就初始化了。
image

下图是JDK动态代理的链路图,从整体上梳理了动态代理的流程。
image

Mybatis Mapper

上面JDK动态代理的例子实现了被代理接口LawEvidence,但是众所周知,Mabatis动态Sql只需要一个Mapper接口及其对应的XML配置,并不需要实现类。那么Mybatis是如何运用JDK动态代理实现JDBC操作的呢?
要想弄清楚这个问题,我们首先得知道为什么Mybatis Mapper不需要实现类?
这要从Mybatis的职责说起,Mybatis是用来干什么的?Mybatis在Service层与数据库之间起到了桥梁的作用,你也可以理解为Mybatis是Service层访问数据库的代理。Mybatis为Service层访问数据库的行为提供了便捷的接口,便捷到Service层可以完全忽略JDBC的存在。
image

Mybatis包圆了一切与JDBC的交互:

加载驱动是它
建立连接是它
创建Statement是它
构建SQL语句是它
执行SQL是它
返回结果还是它
...

Service层再也不用从零开始一步一步的与JDBC建立联系。这已经不是对访问JDBC的增强了,这完全就是代替Service层把事情都干了,干的任劳任怨,干的漂漂亮亮,不让Service层做一点重复劳动。
image

从开发者的角度来说,Mybatis Mapper接口也不应该有实现类,如果每个Mapper接口都需要单独创建一个实现类,那么使用Mybatis框架的项目会变得非常的雍总且不够优雅。
Mybatis是如何做到没有实现类就可以完成动态代理的呢?
我们可以看看Mybatis源码是怎么写的。先写一个简单的测试代码。

public void main() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession session = sqlSessionFactory.openSession();
    try {
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.selectUserById(1);
        System.out.println(user);
    } finally {
        session.close();
    }
}

Mybatis执行一次Sql,首先要读取配置文件,通过配置文件构建SqlSessionFactory,从而得到SqlSession,SqlSession是Mybatis提供的面向用户的Api,它是与数据库交互的会话接口,调用它的getMapper()方法就能得到Mapper接口,并使用Mapper接口执行XML中配置的动态Sql。
如上例中的UserMapper接口,它没有实现类,只定义了selectUserById()方法。前面我们学习了JDK动态代理,很容易想到这里用session#getMapper()方法获取到的UserMapper对象应该是一个动态代理对象,它是对UserMapper这个目标接口的增强。所以我们点进去看个究竟。
image

Mybatis默认实现了SqlSession接口,可以看到DefaultSqlSession#getMapper()方法里是调用了Configuration#getMapper()方法,代码继续往下走。
image
Configuration#getMapper()方法调用的是MapperRegistry#getMapper()方法。MapperRegistry是Configuration里的一个专门用于注册Mapper接口信息的类。MapperRegistry会将Mapper接口的Class对象与MapperProxyFactory对象建立联系,MapperProxyFactory对象可以创建Mapper接口的动态代理对象。看到这里就快要接近真相了,通过Mapper接口的Class对象我们可以从配置中获取到Mybatis的Mapper动态代理对象工厂,从而构建动态代理对象。

image

继续往后看,进入MapperRegistry#getMapper()方法后,通过Mapper接口的Class对象查询到其对应的MapperProxyFactory对象,调用MapperProxyFactory#newInstance()方法创建Mapper接口的代理对象。
image

MapperProxyFactory#newInstance()方法主要就干了一件事情,这个代码我们已经很熟悉了,就是利用Proxy#newProxyInstance()方法生成动态代理对象。但是仔细观察你会发现绿色框标注的这部分代码和前文中律师动态代理打官司的代码写法有点不一样。
image

回顾一下律师动态代理打官司的代码,在调用Proxy#newProxyInstance()方法创建动态代理对象时,第二个参数(接口数组)是从Lawsuit实现类的Class数组获取到的。而在MapperProxyFactory中没有实现类,直接new了一个Class数组,数组元素由Mapper接口组成。现在可以得出结论,动态代理有实现类和无实现类的第一个区别是目标接口赋值的方式不一样,前者通过目标接口实现类的getInterfaces()方法获取;后者通过new一个Mapper接口的Class数组赋值。
image

动态代理有实现类和无实现类的第二个区别在于对InvocationHandler#invoke()方法的调用,前者不仅实现了增强,还通过反射调用了实现类的接口;后者仅仅实现了增强,而没有调用实现类接口。
Proxy#newProxyInstance()方法的第三个参数是传一个InvocationHandler接口,Mybatis使用的是MapperProxy这个实现类。MapperProxy#invoke()方法中绿色框前面的部分不用管,一般不会进入这里,重点看绿色框里的代码。这段代码的意思是根据method创建一个MapperMethod对象,并调用其execute()方法执行XML中映射的Sql语句。MapperMethod对象是缓存的,这里利用了享元模式避免了对象频繁的创建和回收。MapperMethod对象是对Mapper接口方法信息的封装,可以方便的获取方法的签名、Sql语句的类型等信息。
image

可以看到MapperMethod#execute()方法并没有任何Mapper接口实现类的逻辑。
image

Mybatis Mapper动态代理的调用时序图如下图,现在看起来是不是变得非常的简单。
image

现在可以解答文章开头的其中一个问题啦,Mybatis Mapper接口为什么不需要实现类?因为执行Sql所需要的所有的JDBC操作都在Mybatis的MapperProxy中实现了,所以不需要实现类。

标签:Mapper,为什么,对象,代理,接口,Mybatis,动态
From: https://www.cnblogs.com/jietang64/p/18152896

相关文章

  • mybatisplus分页中,模糊匹配一个字符串在列a或者列b下都可以筛选出的写法
    话不多说,直接上代码,and那句就对了LambdaQueryWrapper<类>wrapper=newLambdaQueryWrapper<类>().in(逻辑内容).like(正常逻辑内容).and(wrapperNew->wrapperNew.like(StringUtils.isNotEmpty(filter.getLocation()),......
  • 最近火起的 Bean Searcher 与 MyBatis Plus 到底有啥区别?
    上篇:我这样写代码,比直接使用MyBatis效率提高了100倍欢迎公众号转载,但请转 当前最新版 并在显眼处 标明作者 与 注明出处。如果你喜欢本文也欢迎转发分享^_^BeanSearcher号称任何复杂的查询都可以一行代码搞定,但MybatisPlus似乎也有类似的动态查询功能,它们......
  • 搜维尔科技:为什么您应该选择 Movella Xsens 动作捕捉
    Xsens动作捕捉–黄金标准从电影制作人到物理治疗师,Xsens动作捕捉系统正在帮助世界各地的专业人士将动作数字化。要么获得洞察力以改善健康或表现,要么创建角色和视觉特效,将内容制作者的愿景变为现实。图片为什么您应该选择Xsens动作捕捉1.随时随地捕捉运动在任何环境中获......
  • mybatis-plus 代码生成器步骤
    mybatis-plus代码生成器步骤:1.添加依赖到pom.xml<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.4.1</version></dependency>&l......
  • 为什么工业软件很难国产化?
    因为我曾在国内顶尖的工业制造企业工作过,因此根据自己的所见所闻谈谈个人的浅薄观点。首先,我们要知道像法国达索集团这种工业软件设计公司已经有了几十年的垄断历史,可以说在人力、物力、资源积累、技术积累上依旧具有绝对的优势,即使我国现在投资研发国有的工业软件也很难在短时......
  • MyBatis 核心配置讲解(上)
    大家好,我是王有志,一个分享硬核Java技术的互金摸鱼侠。前两篇的文章中我们分别介绍了MyBatis和MyBaits的应用组成,到这里基础篇的内容就结束了。从今天开始,我们正式进入MyBatis学习的第二阶段:MyBatis的应用部分。这个阶段从MyBatis应用程序的核心配置文件mybatis-conf......
  • electron-store会将数据保存为什么格式
    electron-store是一个用于Electron应用程序的轻量级、持久化数据存储库。它将数据以JSON格式保存到本地文件系统上。具体来说:数据结构:electron-store接受任何可序列化的JavaScript数据结构作为存储内容。这包括基本类型(如字符串、数字、布尔值)、数组、对象(包括嵌套对象......
  • 大厂为什么不用MQ实现订单到期关闭?
    消息队列(MessageQueue,简称MQ)是一种高效处理异步消息的技术手段,广泛应用于系统解耦、流量削峰和异步处理等场景。然而,当涉及到特定的业务逻辑,比如订单到期自动关闭这种定时和精确控制业务流程的场景时,很多大型互联网企业往往选择其他技术方案而非MQ。下面将探讨背后的原因。一致......
  • Java面试题:为什么HashMap不建议使用对象作为Key?
    HashMap是一种基于哈希表的动态数据结构,它允许使用任意不可变对象作为键(key)来存储和检索数据。然而,在某些情况下,使用对象作为HashMap的键可能会遇到一些问题。 首先,我们需要明确对象作为HashMap的键需要满足一些条件:不可变性:对象的属性不能被修改,因为如果属性被修改,那......
  • 【转载】Java函数式编程——为什么new Thread()中要用函数式编程
    面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。面向对象的思想:做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,......