InvocationHandler 与动态代理综合案例 | JavaSE
InvocationHandler 与动态代理综合案例
一、学习目标
完成本章后,你应该能够:
- 能够解释
InvocationHandler在 JDK 动态代理中的职责。 - 能够准确解释
invoke(proxy, method, args)三个参数。 - 能够理解代理方法调用为什么会自动进入
InvocationHandler.invoke()。 - 能够使用
Method.invoke()调用真实目标对象。 - 能够正确处理方法参数和返回值。
- 能够编写通用
ProxyUtil。 - 能够使用动态代理实现日志、耗时统计和异常记录。
- 能够分析动态代理中的递归调用、类型转换和异常传播问题。
二、核心知识
2.1 InvocationHandler 是什么
InvocationHandler 位于:
java.lang.reflect
包下。
它是一个接口:
public interface InvocationHandler
核心抽象方法:
Object invoke(
Object proxy,
Method method,
Object[] args
) throws Throwable;
每一个 JDK 动态代理实例都会关联一个:
InvocationHandler
当代理对象上的接口方法被调用时,对应的方法调用会被分派给:
invoke(...)
处理。
因此:
InvocationHandler 是代理对象的方法调用处理器。
2.2 invoke() 到底是谁调用的
这是本章最重要的问题之一。
假设:
UserService proxy = ...
客户端:
proxy.login(
"admin",
"123456"
);
我们并没有自己写:
handler.invoke(...)
但它仍然会执行。
调用关系:
客户端调用
proxy.login(...)
↓
运行时生成的代理类接收到调用
↓
代理机制将这次调用封装
↓
自动分派到 InvocationHandler.invoke(...)
也就是说:
invoke()通常不是业务程序员直接调用的,而是代理对象的方法被调用时,由 JDK 动态代理机制自动调用。
2.3 proxy 参数
第一个参数:
Object proxy
表示:
当前被调用的代理对象本身。
注意:
它不是:
target
真实业务对象。
必须明确区分:
proxy
↓
JDK 创建出来的代理对象
target
↓
真正完成业务的真实对象
例如:
UserService target =
new UserServiceImpl();
UserService proxy =
createProxy(target);
进入:
invoke(
Object proxy,
Method method,
Object[] args
)
第一个:
proxy
对应外面的动态代理对象。
2.4 method 参数
第二个参数:
Method method
表示:
客户端当前调用的接口方法对应的反射
Method对象。
例如:
proxy.login(
"admin",
"123456"
);
那么:
method.getName()
可以得到:
login
还可以获得:
method.getParameterTypes();
method.getReturnType();
method.getDeclaringClass();
因此代理处理器甚至不需要提前知道:
这次调用到底是哪一个方法
只需要统一读取:
method
即可。
这就是动态代理能够实现:
通用方法增强
的重要原因。
2.5 args 参数
第三个参数:
Object[] args
表示:
客户端调用方法时传入的实参。
例如:
proxy.login(
"admin",
"123456"
);
那么逻辑上:
args[0] = "admin"
args[1] = "123456"
可以:
System.out.println(
Arrays.deepToString(args)
);
输出参数。
需要特别注意:
如果接口方法没有参数,
args允许为null。
因此通用代码不要假设:
args.length
永远安全。
2.6 invoke() 的返回值
invoke() 返回:
Object
这个返回值会进一步成为:
代理方法调用返回给客户端的结果。
例如接口:
String login(
String username,
String password
);
真实调用:
Object result =
method.invoke(
target,
args
);
应该:
return result;
最终客户端:
String result =
proxy.login(
"admin",
"123456"
);
就能得到真实业务的结果。
调用链:
target.login()
↓
返回 "SUCCESS"
↓
method.invoke()
↓
result
↓
InvocationHandler.invoke() return
↓
proxy.login() return
↓
客户端得到 "SUCCESS"
因此:
有返回值的方法必须认真处理
invoke()的返回结果。
三、使用方法
3.1 一个完整的 UserService
接口:
public interface UserService {
String login(
String username,
String password
);
void deleteUsers(int... ids);
}
真实实现:
import java.util.Arrays;
public class UserServiceImpl
implements UserService {
@Override
public String login(
String username,
String password
) {
if ("admin".equals(username)
&& "123456".equals(password)) {
return "SUCCESS";
}
return "FAIL";
}
@Override
public void deleteUsers(int... ids) {
System.out.println(
"删除用户:"
+ Arrays.toString(ids)
);
}
}
3.2 编写最基础的 ProxyUtil
import java.lang.reflect.Proxy;
public class ProxyUtil {
public static UserService createProxy(
UserService target
) {
return (UserService)
Proxy.newProxyInstance(
target.getClass()
.getClassLoader(),
target.getClass()
.getInterfaces(),
(proxy, method, args) -> {
return method.invoke(
target,
args
);
}
);
}
}
这已经是一个可以工作的动态代理。
但它目前:
只是转发
还没有真正增强。
3.3 增加方法日志
return (UserService)
Proxy.newProxyInstance(
target.getClass()
.getClassLoader(),
target.getClass()
.getInterfaces(),
(proxy, method, args) -> {
System.out.println(
"调用方法:"
+ method.getName()
);
Object result =
method.invoke(
target,
args
);
return result;
}
);
于是所有代理方法都可以统一输出:
login
deleteUsers
...
而不需要:
if ("login".equals(...))
才能记录基本日志。
3.4 输出参数
import java.util.Arrays;
System.out.println(
"参数:"
+ Arrays.deepToString(args)
);
完整一点:
String argsText =
args == null
? "[]"
: Arrays.deepToString(args);
System.out.println(
"参数:" + argsText
);
为什么推荐:
Arrays.deepToString(args)
因为参数本身还可能包含数组。
例如:
deleteUsers(1, 2, 3);
对应的可变参数在反射层面可能表现为一个数组参数。
3.5 记录返回值
Object result =
method.invoke(
target,
args
);
System.out.println(
"返回值:" + result
);
return result;
如果真实方法:
void deleteUsers(...)
则:
Method.invoke(...)
返回:
null
这是正常现象。
3.6 增加耗时统计
推荐使用:
System.nanoTime()
测量一段代码执行所经过的时间。
long start =
System.nanoTime();
Object result =
method.invoke(
target,
args
);
long end =
System.nanoTime();
System.out.println(
"耗时:"
+ (end - start)
+ " ns"
);
return result;
比起:
currentTimeMillis()
这里关注的是:
持续时间(elapsed time)
而不是当前墙上时间,因此 nanoTime() 更符合这个语义。
3.7 为什么应该使用 finally 统计耗时
下面代码:
long start =
System.nanoTime();
Object result =
method.invoke(target, args);
long end =
System.nanoTime();
存在问题。
如果:
method.invoke(...)
过程中出现异常:
long end
后面的代码根本不会执行。
更稳妥:
long start =
System.nanoTime();
try {
return method.invoke(
target,
args
);
} finally {
long cost =
System.nanoTime() - start;
System.out.println(
"耗时:" + cost + " ns"
);
}
这样无论:
成功
还是
异常
都能记录耗时。
3.8 Method.invoke 的异常包装
这里是动态代理一个非常重要的 Debug 点。
假设真实方法:
public void work() {
throw new IllegalArgumentException(
"业务参数错误"
);
}
我们通过:
method.invoke(target, args);
反射调用时,目标方法抛出的异常会被:
InvocationTargetException
包装。
也就是:
真实业务异常
IllegalArgumentException
↓
Method.invoke()
↓
InvocationTargetException
因此如果希望代理继续传播真实业务异常,可以:
catch (InvocationTargetException e) {
throw e.getCause();
}
这样可以拿到原始异常。
3.9 一个完整的通用 ProxyUtil
下面设计一个更通用的代理工具。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Objects;
public final class ProxyUtil {
private ProxyUtil() {
}
public static <T> T createProxy(
Class<T> interfaceType,
T target
) {
Objects.requireNonNull(
interfaceType,
"interfaceType"
);
Objects.requireNonNull(
target,
"target"
);
if (!interfaceType.isInterface()) {
throw new IllegalArgumentException(
"interfaceType 必须是接口"
);
}
if (!interfaceType.isInstance(target)) {
throw new IllegalArgumentException(
"target 必须实现指定接口"
);
}
Object proxy =
Proxy.newProxyInstance(
target.getClass()
.getClassLoader(),
new Class<?>[]{
interfaceType
},
(proxyObject,
method,
args) -> {
long start =
System.nanoTime();
String argsText =
args == null
? "[]"
: Arrays.deepToString(
args
);
System.out.println(
"[CALL] "
+ method.getName()
+ " args="
+ argsText
);
try {
Object result =
method.invoke(
target,
args
);
System.out.println(
"[RETURN] "
+ method.getName()
+ " result="
+ result
);
return result;
} catch (
InvocationTargetException e
) {
Throwable cause =
e.getCause();
System.out.println(
"[ERROR] "
+ method.getName()
+ " -> "
+ cause
);
throw cause;
} finally {
long cost =
System.nanoTime()
- start;
System.out.println(
"[COST] "
+ method.getName()
+ " "
+ cost
+ " ns"
);
}
}
);
return interfaceType.cast(proxy);
}
}
使用:
public class Demo {
public static void main(String[] args) {
UserService target =
new UserServiceImpl();
UserService proxy =
ProxyUtil.createProxy(
UserService.class,
target
);
String result =
proxy.login(
"admin",
"123456"
);
System.out.println(result);
proxy.deleteUsers(
1,
2,
3
);
}
}
调用链:
proxy.login(...)
↓
InvocationHandler.invoke()
↓
记录方法名
↓
记录参数
↓
开始计时
↓
method.invoke(target, args)
↓
UserServiceImpl.login(...)
↓
得到结果
↓
记录返回值
↓
finally 记录耗时
↓
结果返回客户端
这已经是一个真正具有:
通用代理增强
意味的工具了。
四、原理与进阶
4.1 为什么一个 invoke 可以处理所有方法
静态代理中:
login()
deleteUser()
updateUser()
通常都需要分别重写。
而动态代理只需要:
invoke(
proxy,
method,
args
)
因为调用的差异已经被抽象成:
method
+
args
例如:
login("admin", "123456")
↓
method = login
args = ["admin", "123456"]
另一次:
deleteUser(1001)
↓
method = deleteUser
args = [1001]
因此同一个:
InvocationHandler
可以处理大量不同方法。
这就是:
将“具体方法调用”抽象成数据。
这是反射和动态代理非常重要的编程思想。
4.2 method.invoke(target, args) 与代理递归
错误代码:
method.invoke(
proxy,
args
);
调用链可能变成:
proxy.login()
↓
handler.invoke()
↓
method.invoke(proxy)
↓
proxy.login()
↓
handler.invoke()
↓
method.invoke(proxy)
↓
...
不断递归。
最终可能导致:
StackOverflowError
所以通常应该:
method.invoke(
target,
args
);
即:
代理负责拦截,目标对象负责真正执行。
4.3 Object 方法也需要注意
代理对象本身仍然是:
Object
所以业务中可能调用:
proxy.toString();
proxy.hashCode();
proxy.equals(other);
对于 JDK 动态代理,这类方法也需要纳入 InvocationHandler 的设计考虑。
因此在真正通用的代理处理器中,不要默认:
进入
invoke()的一定是业务接口中最核心的业务方法。
必要时可以判断:
method.getDeclaringClass()
或者:
method.getName()
决定是否增强。
4.4 异常传播为什么必须认真处理
InvocationHandler.invoke() 可以:
throws Throwable
但接口方法本身可能只声明:
void work();
如果代理处理器抛出一个:
接口方法没有声明的受检异常
代理调用方可能看到:
UndeclaredThrowableException
因此动态代理中的异常处理必须考虑两层:
真实业务异常
↓
Method.invoke 的包装
↓
InvocationTargetException
以及
handler 最终抛出的异常
↓
是否符合接口方法 throws 契约
这也是成熟代理框架比简单 Demo 复杂很多的原因之一。
4.5 原课程中的泛型 ProxyUtil
原课程常见写法:
public static <T> T createProxy(T obj) {
T proxy =
(T) Proxy.newProxyInstance(
ProxyUtil.class
.getClassLoader(),
obj.getClass()
.getInterfaces(),
handler
);
return proxy;
}
这种写法在教学阶段能够很好地表达:
一个工具可以接收不同接口的业务对象。
但是必须理解:
泛型
T并不会让 JDK 代理对象突然变成目标实现类。
例如最好:
UserService target =
new UserServiceImpl();
UserService proxy =
ProxyUtil.createProxy(target);
让 T 表达接口类型。
而不要形成错误认识:
UserServiceImpl proxy =
ProxyUtil.createProxy(
new UserServiceImpl()
);
代理对象仍然不是:
UserServiceImpl
本章前面提供:
createProxy(
Class<T> interfaceType,
T target
)
是为了让接口类型约束更加明确。
五、实践应用
5.1 统一调用日志
可以输出:
[CALL] login args=[admin, 123456]
[RETURN] login result=SUCCESS
[COST] login 325100 ns
于是每个业务类不需要自己重复记录。
5.2 统一性能监控
所有 Service 方法
↓
代理
↓
统一记录开始时间
↓
真实调用
↓
统一记录结束时间
可以快速发现:
哪些业务方法耗时异常
5.3 权限检查
例如:
if ("deleteUsers".equals(
method.getName()
)) {
checkAdminPermission();
}
然后:
method.invoke(
target,
args
);
这只是一个简化思想。
真正系统通常不会大量依赖字符串方法名硬编码,而会结合:
注解
+
反射
+
代理
形成更通用的规则。
这正好把前几章串起来:
注解
↓
声明规则
反射
↓
读取结构和元数据
动态代理
↓
拦截方法调用
InvocationHandler
↓
执行通用增强
5.4 注解 + 反射 + 动态代理
例如概念上:
@Log
public void login() {
}
代理收到调用:
method
↓
检查 method 上有没有 @Log
↓
有
↓
输出日志
↓
执行真实方法
这就是 Java 框架能力开始真正组合起来的地方。
六、常见问题
6.1 args 一定不是 null 吗?
不是。
无参方法:
void work();
调用时:
args
可以是:
null
所以应安全处理。
6.2 void 方法的反射调用返回什么?
例如:
void deleteUser();
通过:
method.invoke(...)
调用后返回:
null
这是正常的。
6.3 为什么代理方法有返回值,却得到 null?
检查:
Object result =
method.invoke(
target,
args
);
之后有没有:
return result;
如果写成:
method.invoke(
target,
args
);
return null;
代理方法当然无法正常返回真实业务结果。
如果接口返回基本类型,错误返回 null 甚至可能进一步导致运行时异常。
6.4 为什么出现 StackOverflowError?
首先检查是否写成:
method.invoke(
proxy,
args
);
这可能重新调用代理自身,再次进入:
invoke()
形成递归。
通常应该:
method.invoke(
target,
args
);
6.5 为什么业务明明抛 IllegalArgumentException,我看到的却是 InvocationTargetException?
因为:
Method.invoke()
是反射调用。
目标方法抛出的异常会被:
InvocationTargetException
包装。
可以:
catch (InvocationTargetException e) {
Throwable cause =
e.getCause();
}
获取真实原因。
6.6 为什么出现 UndeclaredThrowableException?
一种典型情况是:
InvocationHandler.invoke()
↓
抛出某个受检异常
↓
接口方法的 throws 并没有声明这种异常
代理无法直接按照该接口方法的异常契约把它交给调用方,因此可能包装成:
UndeclaredThrowableException
所以通用代理中的异常传播不能随便处理。
6.7 泛型 ProxyUtil 为什么仍然可能类型转换失败?
因为:
<T>
只是编译期泛型抽象。
JDK 动态代理真正产生的对象:
实现接口,但并不是你的具体业务实现类。
因此变量类型应该围绕接口设计。
七、练习与验收
7.1 知识问答
- 什么是
InvocationHandler? invoke()是谁调用的?proxy参数代表什么?method参数代表什么?args参数代表什么?- 无参方法的
args可能是什么? invoke()的返回值最终去了哪里?- 为什么通常使用
method.invoke(target, args)? - 为什么不能随意使用
method.invoke(proxy, args)? InvocationTargetException是什么?UndeclaredThrowableException可能在什么情况下出现?- 为什么一个
InvocationHandler可以增强多个不同方法?
7.2 代码阅读
阅读:
InvocationHandler handler =
(proxy, method, args) -> {
long start =
System.nanoTime();
try {
Object result =
method.invoke(
target,
args
);
return result;
} finally {
System.out.println(
System.nanoTime()
- start
);
}
};
回答:
proxy是谁?method是谁?args是谁?- 核心业务由哪个对象执行?
- 为什么耗时统计放在
finally中? - 如果目标方法有返回值,如何返回给客户端?
7.3 手写代码
编写通用动态代理工具:
ProxyUtil
要求:
- 接收一个实现接口的目标对象。
- 创建 JDK 动态代理。
- 输出方法名。
- 输出参数。
- 输出返回值。
- 记录执行耗时。
- 发生异常时记录异常。
- 将真实异常继续传播。
7.4 Debug
下面代码存在什么问题?
return Proxy.newProxyInstance(
loader,
interfaces,
(proxy, method, args) -> {
System.out.println("before");
return method.invoke(
proxy,
args
);
}
);
请:
- 分析调用链。
- 判断为什么可能无限递归。
- 修改为调用真实目标对象。
再分析:
(proxy, method, args) -> {
method.invoke(
target,
args
);
return null;
}
如果被代理的方法是:
String login();
会有什么问题?
7.5 综合训练
实现:
UserService
方法包括:
String login(
String username,
String password
);
void deleteUsers(
int... ids
);
要求使用一个通用动态代理完成:
调用前:
方法名
参数
调用:
真实业务
调用后:
返回值
耗时
异常:
异常类型
异常信息
并保证:
- 一个代理处理器能够处理两个不同方法。
- 正确处理无返回值方法。
- 正确处理数组参数。
- 正确传播真实业务异常。
7.6 本章验收
能够完整画出:
客户端
↓
proxy.login(...)
↓
JDK 动态代理类
↓
InvocationHandler.invoke(
proxy,
method,
args
)
↓
前置增强
↓
method.invoke(
target,
args
)
↓
真实业务对象
↓
返回值 / 异常
↓
后置增强
↓
返回客户端
并能够口述:
InvocationHandler 是 JDK 动态代理的方法调用处理器。代理对象的接口方法被调用后,JDK 会把这次调用分派给
invoke(proxy, method, args)。其中 proxy 表示代理对象,method 表示当前被调用的方法,args 表示实参。处理器可以在调用method.invoke(target, args)前后统一加入日志、权限、耗时统计等增强,从而实现通用动态代理。