Java 动态代理详解:从 InvocationHandler 到 Mapper 接口代理
Java 动态代理详解:从 InvocationHandler 到 Mapper 接口代理
前言
在学习 Java 动态代理时,最容易产生疑问的代码通常是下面这一段:
1 | InvocationHandler handler = (proxy, method, args) -> { |
乍一看会有几个问题:
handler到底是什么?它是不是一个函数?proxy、method、args从哪里来的?- 为什么只有接口,没有实现类,也可以调用方法?
Proxy.newProxyInstance()到底创建了什么对象?
理解动态代理的关键,不是先背 API,而是先理解一句话:
动态代理会在程序运行时生成一个接口的“替身对象”。客户端调用替身对象的方法时,请求会被统一交给
InvocationHandler处理。
代理模式本身是一种结构型设计模式:代理对象与真实服务遵循相同接口,并在访问真实服务前后增加控制逻辑,例如日志、缓存、权限验证、延迟加载或远程调用。(Refactoring Guru)
而在 MyBatis 风格的场景中,代理对象甚至不需要转发给真实实现类:它可以直接读取接口方法上的 SQL 信息,完成后续操作。bugstack 的示例正是通过动态代理,模拟只定义 DAO 接口、不编写实现类的 Mapper 调用过程。([Bugstack](https://bugstack.cn/md/develop/design-pattern/2020-06-16-重学 Java 设计模式《实战代理模式》.html))
一、从普通接口调用开始理解
1.1 普通接口为什么需要实现类
假设现在需要查询用户信息,可以先定义接口:
1 | public interface IUserDao { |
正常情况下,还需要编写实现类:
1 | public class UserDaoImpl implements IUserDao { |
客户端调用:
1 | public class ApiTest { |
调用关系如下:
1 | 客户端 |
这里的特点是:
1 | 接口 IUserDao |
1.2 MyBatis 中为什么经常没有实现类
在使用 MyBatis 时,我们通常只需要定义 Mapper 接口:
1 | public interface IUserDao { |
但是我们并不会自己编写:
1 | public class UserDaoImpl implements IUserDao { |
客户端却仍然可以调用:
1 | IUserDao userDao = 获取Mapper对象(); |
这就产生了一个问题:
1 | 接口只有方法声明,没有方法体, |
答案就是:
框架在运行时为
IUserDao生成了一个动态代理对象,这个代理对象实现了IUserDao接口。
可以先把它想象成:
1 | public class $Proxy0 implements IUserDao { |
客户端拿到的 userDao,真实类型并不是 UserDaoImpl,而是类似:
1 | jdk.proxy1.$Proxy0 |
二、什么是动态代理
2.1 动态代理的基本概念
动态代理是指:
程序运行期间,由 Java 自动创建代理对象;代理对象实现指定接口,并将方法调用统一交给处理器执行。
JDK 提供的动态代理主要依赖两个核心类型:
1 | java.lang.reflect.Proxy |
其中:
Proxy:负责创建动态代理对象;InvocationHandler:负责处理代理对象接收到的方法调用。
JDK 文档中说明,Proxy.newProxyInstance() 会返回一个实现指定接口列表的代理实例;代理对象上的方法调用,会被分发给与之关联的 InvocationHandler。(Oracle Docs)
2.2 动态代理和静态代理的区别
静态代理需要手动编写代理类:
1 | public class UserDaoProxy implements IUserDao { |
动态代理则不需要提前编写:
1 | UserDaoProxy |
而是在运行时自动生成类似:
1 | $Proxy0 |
两者区别如下:
| 对比项 | 静态代理 | 动态代理 |
|---|---|---|
| 代理类创建时间 | 编码阶段手写 | 程序运行时生成 |
| 是否需要手写代理类 | 需要 | 不需要 |
| 方法增强方式 | 每个代理类分别编写 | 统一交给处理器 |
| 适合场景 | 类较少、逻辑固定 | 日志、事务、Mapper、RPC 等通用逻辑 |
三、InvocationHandler 到底是什么
3.1 InvocationHandler 是一个接口
JDK 中定义了一个接口:
1 | public interface InvocationHandler { |
它只有一个需要实现的核心方法:
1 | invoke(...) |
这个方法的作用是:当代理对象的方法被调用时,统一进入这里处理。
3.2 handler 如何理解
handler 是一个实现了 InvocationHandler 接口的对象,Lambda 是它的 invoke() 方法实现。
例如下面代码:
1 | InvocationHandler handler = (proxy, method, args) -> { |
完整写法实际上是:
1 | InvocationHandler handler = new InvocationHandler() { |
因此:
1 | (proxy, method, args) -> { |
并不是凭空出现的一段语法,而是:
1 | invoke(Object proxy, Method method, Object[] args) |
这个方法的简写。
3.3 为什么可以使用 Lambda
因为 InvocationHandler 只有一个需要实现的抽象方法:
1 | Object invoke(Object proxy, Method method, Object[] args) |
这种接口称为函数式接口。
因此可以使用 Lambda:
1 | InvocationHandler handler = (proxy, method, args) -> { |
它与下面写法本质相同:
1 | InvocationHandler handler = new InvocationHandler() { |
可以类比之前接触过的 Java 代码:
1 | Runnable task = () -> { |
等价于:
1 | Runnable task = new Runnable() { |
区别只是:
1 | Runnable 的回调方法是 run() |
3.4 为什么 proxy、method、args 可以直接使用
代码:
1 | InvocationHandler handler = (proxy, method, args) -> { |
展开后是:
1 | InvocationHandler handler = new InvocationHandler() { |
所以:
| Lambda 参数 | 实际类型 | 含义 |
|---|---|---|
proxy |
Object |
当前正在被调用的代理对象 |
method |
Method |
客户端本次调用的方法信息 |
args |
Object[] |
客户端本次传入的方法参数 |
Java 能够根据左侧类型:
1 | InvocationHandler handler |
推断 Lambda 对应的是 invoke() 方法,因此不必手动写出参数类型。
也可以写成完整形式:
1 | InvocationHandler handler = |
所以这里调用代码 handler 的使用,不是自动解包,而是回调。
下面代码执行时:
1 | InvocationHandler handler = (proxy, method, args) -> { |
此时只是:
- 创建
handler对象; - 创建代理对象;
- 将
handler绑定给代理对象。
Lambda 内部代码并没有执行。
直到客户端真正调用:
1 | userDao.queryUserInfo("10001"); |
JDK 生成的代理对象才会反过来调用:
1 | handler.invoke( |
这种执行方式叫做回调:
- 你先把处理规则交给框架;
- 将来条件触发时,框架再反过来调用你的处理规则。
3.5 详细理解三个参数
proxy:当前代理对象method:当前被调用的方法args:方法参数数组
1 | Object invoke(Object proxy, Method method, Object[] args) |
四、Proxy.newProxyInstance() 参数详解
创建动态代理对象的核心代码如下:
1 | Object proxyObject = Proxy.newProxyInstance( |
它有三个参数:
1 | Proxy.newProxyInstance( |
这三个参数分别解决三个问题:
1 | loader -> 代理类由谁加载 |
最终,Proxy.newProxyInstance() 会返回一个由 JDK 在运行时创建的代理对象。
例如:
1 | IUserDao userDao = (IUserDao) Proxy.newProxyInstance( |
可以理解为 JDK 在运行期间自动生成并创建了类似下面的对象:
1 | IUserDao userDao = new $Proxy0(handler); |
其中:
1 | public class $Proxy0 implements IUserDao { |
五、通过 $Proxy0 彻底理解运行过程
5.1 JDK自动生成代理类
动态代理最难理解的地方,是我们看不到 JDK 自动生成的代理类。
为了学习,可以把代理对象想象成下面这个类。
1 | import java.lang.reflect.InvocationHandler; |
这不是 JDK 实际生成的完整源码,但它能够准确帮助我们理解代理流程。
客户端调用:
1 | userDao.queryUserInfo("10001"); |
可以理解为内部发生了:
1 | handler.invoke( |
因此,原来写在代码里面的 Handler 就会接收到 proxy、method、args 参数,然后回调运行这里的 invoke 逻辑:
1 | InvocationHandler handler = (proxy, method, args) -> { |
所以处理的逻辑:
1 | userDao.queryUserInfo("10001") |
5.2 JDK 动态代理必须基于接口
下面代码可以使用 JDK 动态代理:
1 | public interface IUserDao { |
下面只有普通类的情况,不能直接使用 JDK 动态代理:
1 | public class UserDao { |
因为 JDK 动态代理生成的是接口实现类:
1 | $Proxy0 implements IUserDao |
而不是目标类的子类:
1 | $Proxy0 extends UserDao |
对于没有接口的类代理,通常会使用基于子类生成的代理方式,例如 CGLIB 或其他字节码增强工具。
总结
Java 动态代理的核心,是在运行期间由 Proxy 创建一个实现指定接口的代理对象,并将方法调用统一交给 InvocationHandler 处理。proxy 表示代理对象,method 表示当前调用的方法,args 表示传入的参数。
在 Mapper 场景中,正是因为动态代理能够拦截接口方法、读取注解并处理参数,所以我们只需要定义接口,而不必手动编写实现类。理解 $Proxy0 与 handler.invoke() 的调用关系,也就理解了 JDK 动态代理的基本运行原理。