Java 动态代理详解:从 InvocationHandler 到 Mapper 接口代理

前言

在学习 Java 动态代理时,最容易产生疑问的代码通常是下面这一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
InvocationHandler handler = (proxy, method, args) -> {
Select select = method.getAnnotation(Select.class);
System.out.println(
select.value().replace("#{uId}", args[0].toString())
);
return args[0] + ",查询成功";
};

return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class<?>[]{mapperInterface},
handler
);

乍一看会有几个问题:

  • handler 到底是什么?它是不是一个函数?
  • proxymethodargs 从哪里来的?
  • 为什么只有接口,没有实现类,也可以调用方法?
  • 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
2
3
public interface IUserDao {
String queryUserInfo(String uId);
}

正常情况下,还需要编写实现类:

1
2
3
4
5
6
7
8
public class UserDaoImpl implements IUserDao {

@Override
public String queryUserInfo(String uId) {
System.out.println("查询数据库,用户编号:" + uId);
return uId + ",张三";
}
}

客户端调用:

1
2
3
4
5
6
7
8
9
10
public class ApiTest {

public static void main(String[] args) {
IUserDao userDao = new UserDaoImpl();

String result = userDao.queryUserInfo("10001");

System.out.println(result);
}
}

调用关系如下:

1
2
3
4
5
6
7
8
9
客户端

│ userDao.queryUserInfo("10001")

UserDaoImpl

│ 执行查询逻辑

返回查询结果

这里的特点是:

1
2
3
4
5
接口 IUserDao

实现类 UserDaoImpl

真正完成方法逻辑

1.2 MyBatis 中为什么经常没有实现类

在使用 MyBatis 时,我们通常只需要定义 Mapper 接口:

1
2
3
4
5
public interface IUserDao {

@Select("select userName from user where id = #{uId}")
String queryUserInfo(String uId);
}

但是我们并不会自己编写:

1
2
public class UserDaoImpl implements IUserDao {
}

客户端却仍然可以调用:

1
2
3
IUserDao userDao = 获取Mapper对象();

String result = userDao.queryUserInfo("10001");

这就产生了一个问题:

1
2
接口只有方法声明,没有方法体,
真正执行 queryUserInfo() 的对象是谁?

答案就是:

框架在运行时为 IUserDao 生成了一个动态代理对象,这个代理对象实现了 IUserDao 接口。

可以先把它想象成:

1
2
3
public class $Proxy0 implements IUserDao {
// JDK 在运行时自动生成
}

客户端拿到的 userDao,真实类型并不是 UserDaoImpl,而是类似:

1
jdk.proxy1.$Proxy0

二、什么是动态代理

2.1 动态代理的基本概念

动态代理是指:

程序运行期间,由 Java 自动创建代理对象;代理对象实现指定接口,并将方法调用统一交给处理器执行。

JDK 提供的动态代理主要依赖两个核心类型:

1
2
java.lang.reflect.Proxy
java.lang.reflect.InvocationHandler

其中:

  • Proxy:负责创建动态代理对象;
  • InvocationHandler:负责处理代理对象接收到的方法调用。

JDK 文档中说明,Proxy.newProxyInstance() 会返回一个实现指定接口列表的代理实例;代理对象上的方法调用,会被分发给与之关联的 InvocationHandler。(Oracle Docs)


2.2 动态代理和静态代理的区别

静态代理需要手动编写代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserDaoProxy implements IUserDao {

private final IUserDao target;

public UserDaoProxy(IUserDao target) {
this.target = target;
}

@Override
public String queryUserInfo(String uId) {
System.out.println("开始查询");
String result = target.queryUserInfo(uId);
System.out.println("查询结束");
return result;
}
}

动态代理则不需要提前编写:

1
2
3
UserDaoProxy
OrderServiceProxy
PaymentServiceProxy

而是在运行时自动生成类似:

1
2
$Proxy0
$Proxy1

两者区别如下:

对比项 静态代理 动态代理
代理类创建时间 编码阶段手写 程序运行时生成
是否需要手写代理类 需要 不需要
方法增强方式 每个代理类分别编写 统一交给处理器
适合场景 类较少、逻辑固定 日志、事务、Mapper、RPC 等通用逻辑

三、InvocationHandler 到底是什么

3.1 InvocationHandler 是一个接口

JDK 中定义了一个接口:

1
2
3
4
5
public interface InvocationHandler {

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

它只有一个需要实现的核心方法:

1
invoke(...)

这个方法的作用是:当代理对象的方法被调用时,统一进入这里处理。


3.2 handler 如何理解

handler 是一个实现了 InvocationHandler 接口的对象,Lambda 是它的 invoke() 方法实现。

例如下面代码:

1
2
3
4
InvocationHandler handler = (proxy, method, args) -> {
System.out.println(method.getName());
return null;
};

完整写法实际上是:

1
2
3
4
5
6
7
8
9
10
11
12
13
InvocationHandler handler = new InvocationHandler() {

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

System.out.println(method.getName());
return null;
}
};

因此:

1
2
3
(proxy, method, args) -> {
...
}

并不是凭空出现的一段语法,而是:

1
invoke(Object proxy, Method method, Object[] args)

这个方法的简写。


3.3 为什么可以使用 Lambda

因为 InvocationHandler 只有一个需要实现的抽象方法:

1
2
Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;

这种接口称为函数式接口。

因此可以使用 Lambda:

1
2
3
InvocationHandler handler = (proxy, method, args) -> {
return null;
};

它与下面写法本质相同:

1
2
3
4
5
6
7
InvocationHandler handler = new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) {
return null;
}
};

可以类比之前接触过的 Java 代码:

1
2
3
Runnable task = () -> {
System.out.println("执行任务");
};

等价于:

1
2
3
4
5
6
7
Runnable task = new Runnable() {

@Override
public void run() {
System.out.println("执行任务");
}
};

区别只是:

1
2
Runnable 的回调方法是 run()
InvocationHandler 的回调方法是 invoke()

3.4 为什么 proxymethodargs 可以直接使用

代码:

1
2
3
InvocationHandler handler = (proxy, method, args) -> {
...
};

展开后是:

1
2
3
4
5
6
7
8
9
10
11
InvocationHandler handler = new InvocationHandler() {

@Override
public Object invoke(
Object proxy,
Method method,
Object[] args
) throws Throwable {
...
}
};

所以:

Lambda 参数 实际类型 含义
proxy Object 当前正在被调用的代理对象
method Method 客户端本次调用的方法信息
args Object[] 客户端本次传入的方法参数

Java 能够根据左侧类型:

1
InvocationHandler handler

推断 Lambda 对应的是 invoke() 方法,因此不必手动写出参数类型。

也可以写成完整形式:

1
2
3
4
InvocationHandler handler =
(Object proxy, Method method, Object[] args) -> {
return null;
};

所以这里调用代码 handler 的使用,不是自动解包,而是回调。

下面代码执行时:

1
2
3
4
5
6
7
8
9
10
InvocationHandler handler = (proxy, method, args) -> {
System.out.println("进入处理器");
return null;
};

IUserDao userDao = (IUserDao) Proxy.newProxyInstance(
IUserDao.class.getClassLoader(),
new Class<?>[]{IUserDao.class},
handler
);

此时只是:

  1. 创建 handler 对象;
  2. 创建代理对象;
  3. handler 绑定给代理对象。

Lambda 内部代码并没有执行。

直到客户端真正调用:

1
userDao.queryUserInfo("10001");

JDK 生成的代理对象才会反过来调用:

1
2
3
4
5
handler.invoke(
当前代理对象,
queryUserInfo 对应的 Method 对象,
new Object[]{"10001"}
);

这种执行方式叫做回调:

  1. 你先把处理规则交给框架;
  2. 将来条件触发时,框架再反过来调用你的处理规则。

3.5 详细理解三个参数

  1. proxy:当前代理对象
  2. method:当前被调用的方法
  3. args:方法参数数组
1
2
3
4
5
6
7
Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;

// 使用时
InvocationHandler handler = (proxy, method, args) -> {
...
};

四、Proxy.newProxyInstance() 参数详解

创建动态代理对象的核心代码如下:

1
2
3
4
5
Object proxyObject = Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class<?>[]{mapperInterface},
handler
);

它有三个参数:

1
2
3
4
5
Proxy.newProxyInstance(
ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler handler
);

这三个参数分别解决三个问题:

1
2
3
loader      -> 代理类由谁加载
interfaces -> 代理对象要实现哪些接口
handler -> 代理对象的方法调用交给谁处理

最终,Proxy.newProxyInstance() 会返回一个由 JDK 在运行时创建的代理对象。

例如:

1
2
3
4
5
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(
IUserDao.class.getClassLoader(),
new Class<?>[]{IUserDao.class},
handler
);

可以理解为 JDK 在运行期间自动生成并创建了类似下面的对象:

1
IUserDao userDao = new $Proxy0(handler);

其中:

1
2
3
public class $Proxy0 implements IUserDao {
// 内部保存 handler
}

五、通过 $Proxy0 彻底理解运行过程

5.1 JDK自动生成代理类

动态代理最难理解的地方,是我们看不到 JDK 自动生成的代理类。

为了学习,可以把代理对象想象成下面这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class $Proxy0 implements IUserDao {

private final InvocationHandler handler;

public $Proxy0(InvocationHandler handler) {
this.handler = handler;
}

@Override
public String queryUserInfo(String uId) {
try {
// 处理查询用户信息逻辑
Method method = IUserDao.class.getMethod("queryUserInfo", String.class);
Object result = handler.invoke(
this, // proxy
method, // method
new Object[]{uId} // args
);
return (String) result;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}

这不是 JDK 实际生成的完整源码,但它能够准确帮助我们理解代理流程。

客户端调用:

1
userDao.queryUserInfo("10001");

可以理解为内部发生了:

1
2
3
4
5
handler.invoke(
userDao,
IUserDao.class.getMethod("queryUserInfo", String.class),
new Object[]{"10001"}
);

因此,原来写在代码里面的 Handler 就会接收到 proxymethodargs 参数,然后回调运行这里的 invoke 逻辑:

1
2
3
4
5
6
7
8
9
10
InvocationHandler handler = (proxy, method, args) -> {
Select select = method.getAnnotation(Select.class);

String sql = select.value()
.replace("#{uId}", args[0].toString());

System.out.println("SQL:" + sql);

return args[0] + ",模拟用户信息";
};

所以处理的逻辑:

1
2
3
4
5
userDao.queryUserInfo("10001")

$Proxy0.queryUserInfo("10001")

handler.invoke(proxy, method, args)

5.2 JDK 动态代理必须基于接口

下面代码可以使用 JDK 动态代理:

1
2
3
public interface IUserDao {
String queryUserInfo(String uId);
}

下面只有普通类的情况,不能直接使用 JDK 动态代理:

1
2
3
4
5
public class UserDao {
public String queryUserInfo(String uId) {
return null;
}
}

因为 JDK 动态代理生成的是接口实现类:

1
$Proxy0 implements IUserDao

而不是目标类的子类:

1
$Proxy0 extends UserDao

对于没有接口的类代理,通常会使用基于子类生成的代理方式,例如 CGLIB 或其他字节码增强工具。


总结

Java 动态代理的核心,是在运行期间由 Proxy 创建一个实现指定接口的代理对象,并将方法调用统一交给 InvocationHandler 处理。proxy 表示代理对象,method 表示当前调用的方法,args 表示传入的参数。

在 Mapper 场景中,正是因为动态代理能够拦截接口方法、读取注解并处理参数,所以我们只需要定义接口,而不必手动编写实现类。理解 $Proxy0handler.invoke() 的调用关系,也就理解了 JDK 动态代理的基本运行原理。