利用Jdk动态代理模拟MyBatis的Mapper功能

本文将先介绍jdk动态代理的基本用法,并对其原理和注意事项予以说明。之后将以两个最常见的应用场景为例,进行代码实操。这两个应用场景分别是拦截器声明性接口,它们在许多开发框架中广泛使用。比如在spring和mybatis中均使用了拦截器模式,在mybatis中还利用动态代理来实现声明性接口的功能。因此,掌握动态代理的原理和代码书写方式,对阅读理解这些开源框架非常有益。

文中的示例代码基于jdk8编写,且都经过验证,但在将代码迁移到博客的过程中,难免存在遗漏。如果您将代码复制到自己的IDE后无法运行,或存在语法错误,请在评论中留言指正 😉

小示例

先来看一个jdk代理的最小demo

点击查看代码

 

上述代码执行后的输出结果如下:

 
运行的代理类为: class com.sun.proxy.$Proxy0
调用的代理方法为: greeting
调用方法的参数为: Hello, Jdk Proxy
请在这里插入代理的逻辑代码...

其中倒数第二行的businessProxy变量,就是一个代理对象,它是BusinessInterface接口的一个实例,但我们并没有编写这个接口的实现类,而是通过Proxy.newProxyInstance方法生成出了该接口的实例。那么这个动态代理实例对应的Class长什么样子呢?上面的结果输出中已经打印出来了,这个代理类名称为com.sun.proxy.$Proxy0。实际上,如果我们再为另外一个接口生成代理对象的话,它的Class名称为com.sun.proxy.$Proxy1,依次类推。

还有一个值得关注的问题:最重要的逻辑代码应该写在哪里?答案是写在InvocationHandler这个接口的invoke()方法中,也就是上面示例代码的第⑵处。由此可以看出:代理对象实际要执行的代码,就是invoke()方法中的代码,换言之,代理对象所代理的所有接口方法,最终要执行的代码都在invoke方法里,因此,这里是一切魔法的入口。

编写一个jdk代理实例的基本步骤如下:

  1. 编写业务接口
    因为jdk代理是基于接口的,因此,只能将业务方法定义成接口,但它可以一次生成多个接口的代理对象

  2. 编写调用处理器
    即编写一个java.lang.reflect.InvocationHandler接口的实现类,代理对象的业务逻辑就写在该接口的invoke方法中

  3. 生成代理对象
    有了业务接口和调用处理器后,将二者作为参数,通过Proxy.newProxyInstance方法便可以生成这个(或这些)接口的代理对象。比如上述示例代码中的businessProxy对象,它拥有greeting()这个方法,调用该方法时,实际执行的就是invoke方法。

代理对象生成原理

代理的目的,是为接口动态生成一个实例对象,该对象有接口定义的所有方法。调用对象的这些方法时,都将执行生成该对象时,指定的“调用处理器”中的方法(即invoke方法)。

生成代理对象的方法签名如下:

 
Proxy.newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)

classloader一般选择当前类的类加载器,interfaces是一个接口数组,newProxyInstance方法将为这组接口生成实例对象,handler中的代码则是生成的实例对象实际要执行的内容,这些代码就位于invoke方法中。在生成代理对象前,会先生成一个Class,这个Class实现了interfaces中的所有接口,且这些方法的内容为直接调用handler#invoke,如下图所示:

JDK代理对象生成原理

特别说明
InvocationHandler的invoke方法签名为:

 
 
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

在该方法的实现代码中,不要调用proxy参数的toString方法, 这会导致递归死循环

下面将以小示例中的BusinessInterface接口和ProxyLogicHandler为基础,用普通Java代码的方式,模拟一下Proxy.newProxyInstance的代码逻辑,如下:

点击查看代码

 

上面的代码是示意性的,并不正确,比如它没有使用到loader和interfaces参数,调用hanlder.invoke方法时,对于method参数只是简单的用'greeting'字符串替代,类型都不正确。但这段示意代码很简单明了地呈现了真实的Proxy.newProxyInstance方法内部的宏观流程。

下面再提供一个与真实的newProxyInstance方法稍微接近一点的模拟实现(需要您对jdk里JavaCompiler类的使用有一定了解)

点击查看代码

 

代码运行结果为:

 
执行invocationHandler#invoke()方法
调用的代理方法名为:sayHi
调用时传递的参数为:East Knight

应用场景

上面提到:代理是在运行期,为接口动态生成了一个实现类,和这个实现类的实例。那这个功能有什么用呢?我们直接写一个实现类不也是一样的么?代理类与我们手动写代码的主要差异在于它的动态性,它允许我们在程序的运行期间动态创建Class,这对于框架类程序,为其预设的业务组件增加公共特性提供了技术支持。因为这种额外特性的加持,对业务代码没有直接的侵入性,因此效果非常好。动态代理的两个最常用见应用场景为拦截器和声明性接口,下面分别介绍。

拦截器功能

搭载器就是将目标组件劫持,在执行目标组件代码的前后,塞入一些其它代码。比如在正式执行业务方法前,先进行权限校验,如果校验不通过,则拒绝继续执行。对于此类操作,业界已经抽象出一组通用的编程模型:面向切面编程AOP

接下来,将以演员和导演为业务背景,实现一个简易的拦截器,各个组件介绍如下:

  • Performer <Interface>
    演员接口,有play和introduction方法

  • DefaultActor <Class>
    代表男性演员,它实现了Performer接口,也是拦截器将要拦截的对象

  • Director <Interface>
    导演接口,只有一个getCreations方法, 该方法返回一个字符串列表,它代表导演的作品集

  • DefaultDirector <Class>
    Director接口的实现类,同时也是拦截器将要拦截的对象

  • ProxyForInterceptor <Class>
    拦截器核心类,实现了InvocationHandler接口,拦截器代码位于接口的invoke方法中。

    拦截器将持有Performer和Direcotor的真实实现实例,并在调用Performer的play和introduction方法前,先执行一段代码。这里实现为打印一段文本,接着再调用play或introduction,执行完后,再执行一段代码,也是打印一段文本。Director实例方法的拦截处理逻辑与此相同。这便是最简单的拦截器效果了。

  • IntercepterTestMain <Class>
    拦截器测试类,在main方法中,验证上述组件的拦截器功能效果。这个例子中,特意写了两个接口和两个实现类,就是为了演示,JDK的动态代理是支持多接口的。

下面是各个组件的源代码

Performer

 

DefaultActor

 

Director

 

DefaultDirector

 

ProxyForInterceptor

 

IntercepterTestMain

 

以上代码的执行结果如下:

 
[DirectorActorProxyHandler]: 调用的代理方法为:play
[DirectorActorProxyHandler]: >>> 调用 play 之前的逻辑
[DefaultActor]: 默认男演员正在即兴表演《长板坡》
[DirectorActorProxyHandler]: <<< 调用 play 之后的逻辑
[DirectorActorProxyHandler]: 调用的代理方法为:introduction
[DirectorActorProxyHandler]: >>> 调用 introduction 之前的逻辑
[DirectorActorProxyHandler]: <<< 调用 introduction 之后的逻辑
[IntercepterTestMain ]: 代理对象返回的个人简介内容为: 李白·上李邕: 大鹏一日同风起,扶摇直上九万里。假令风歇时下来,犹能颠却沧溟水。世人见我恒殊调,闻余大言皆冷笑。宣父尚能畏后生,丈夫未可轻年少。
[DirectorActorProxyHandler]: 调用的代理方法为:getCreations
[DirectorActorProxyHandler]: >>> 调用 getCreations 之前的逻辑
[DirectorActorProxyHandler]: <<< 调用 getCreations 之后的逻辑
[IntercepterTestMain ]: 代理对象返回的导演作品列表:
· 活着
· 盲井
· 走出夹边沟
· 少年派的奇幻漂流

可以看到,在main方法中,调用代理类的play方法后(位于代码的①处),在执行真实的DefaultActor#play方法前后,均有额外的文本输出,这些都不是DefaultActor#play方法的逻辑。这便实现了拦截器效果,且对于使用者而言(即编写DefaultActor类的开发者),是无侵入无感知的。

声明性接口

声明性接口的特点是:开发者只需要提供接口,并在接口方法中声明该方法要完成的功能(通常是以多个注解的方式声明),但不用编写具体的功能实现代码,而是通过框架的工厂方法来获取该接口的实例。当然,该实例会完成接口方法中所声明的那些功能。比较典型的产品是MyBatis的Mapper接口。实现手段也是采用jdk动态代理,在InvocationHandler的invoke方法中,完成该接口方法所声明的那些特性功能。

接下来,本文将模拟MyBatis的Mapper功能,组件说明如下:

  • SqlMapper <Annotaton>
    与MyBatis的Mapper注解等效,用于标识一个接口为Sql映射接口,但在本示例中,这个接口并未使用到。因为这个标识接口的真实用途,是在SpringBoot环境中,用于自动扫描和加载Mapper接口的。本示例仅模拟Mapper本身的声明性功能,因此用不上它。保留这个接口,只是为了显得更完整。

  • Select <Annotation>
    与MyBatis的Select注解等效,它有一个sql属性,用于指定要执行的SQL语句,且支持#{}形式的插值

  • ParamName <Annotation>
    与MyBatis的Param注解等效,用于标识Mapper接口的方法参数名称,以便用于Select注解中sql语句的插值替换

  • PerformerMapper <Interface>
    演员实体的数据库访问接口,与开发者使用MyBatis时,日常编写的各类Mapper接口一样。在里边定义各种数据库查询接口方法,并利用Select和ParamName注解,声明数据操作的具体功能。

  • ProxyForDeclaration <Class>
    整个Mapper功能的核心类,实现了InvocationHandler接口,在invoke方法中,完成Mapper的所有功能

  • DeclarationTestMain <Class>
    声明性接口的功能测试类,在main方法中,通过jdk代理获得一个PerformerMapper实例,并调用其中的getQuantityByNameAndAage、getRandomPoetryOf和listAllOfAge方法,分别传入不的SQL和参数,用以验证3种不同的情况。

下面是各个组件的源代码:

SqlMapper

 

Select

 

ParamName

 

PerformerMapper

 

ProxyForDeclaration

 

DeclarationTestMain

 

以上代码的执行结果为:

 
[ProxyForDeclaration]: 调用的方法名为:getQuantityByNameAndAage
[ProxyForDeclaration]: 原始sql为:select count(*) from performer where name=#{name} and age = #{ age }
[ProxyForDeclaration]: 插值替换后的sql为:select count(*) from performer where name='Jane Lotus' and age = 47
[DeclarationTestMain]: 代理实例方法方法的返回值为:40
[ProxyForDeclaration]: 调用的方法名为:getRandomPoetryOf
[ProxyForDeclaration]: 原始sql为:select poetry_item from poetry where performer_name = #{ name }
[ProxyForDeclaration]: 插值替换后的sql为:select poetry_item from poetry where performer_name = '杜甫'
[DeclarationTestMain]: 代理实例方法的返回值为:黄四娘家花满蹊,千朵万朵压枝低。
[ProxyForDeclaration]: 调用的方法名为:listAllOfAge
[ProxyForDeclaration]: 原始sql为:select * from performer where age >= #{age} limit #{ pageSize }
Exception in thread "main" guzb.diy.proxy.ProxyForDeclaration$SqlMapperExecuteException: 未知参数:pageSize
at guzb.diy.proxy.ProxyForDeclaration.interpolateSql(ProxyForDeclaration.java:55)
at guzb.diy.proxy.ProxyForDeclaration.invoke(ProxyForDeclaration.java:29)
at com.sun.proxy.$Proxy1.listAllOfAge(Unknown Source)
at guzb.diy.proxy.DeclarationTestMain.main(JdkProxyStudyMain.java:24)

以上代码共模拟了3个调用Mapper的场景:

  1. 调用getQuantityByNameAndAage()方法根据姓名的年龄查询演员数量。但并未真正执行JDBC查询,只是将SQL进行了插值替换和输出,然后随机返回了一个数字。这足以演示声明性接口这一特性了,真实地执行jdbc查询,那将一个代码量巨大的工作,它的缺失并不影响本示例的主旨。

  2. 调用getRandomPoetryOf()方法查询指定诗人的一段诗句。同样没有真正执行jdbc查询,而是随机返回了一句诗文。

  3. 调用listAllOfAge()方法查询指定年龄的所有演员。该方法有意设计为引发一个异常,因为接口方法上声明的SQL中,pageSize这个插值变量并未在方面签名中声明。 

     

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

woisking2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值