速通 Java 动态代理
什么是代理
要学习动态代理,首先要知道什么是代理。简单说,若我们想访问对象 A,但现在不直接调用,而是访问对象 B,由 B 去调用 A 并且把 A 的返回值返回给我们,此时 B 就是一个代理。那么为什么要脱裤子放屁呢?
看一个现实例子。假设某人有个多余房产希望出售/出租,于是他找了一个房产中介作为代理。潜在的购房人/求租人直接与中介联系即可。那么中介有什么优势?
- 中介分担了业主的职责,业主只需在意成交价格即可。
- 中介承担了一些通用的工作,比如看房,起草协议。就不要求每一个业主都掌握这些了。
对应到代码中,就是:
- 分担被代理对象的职责,被代理对象只需关注自己的核心业务。
- 实现 AOP(面向切面编程),可批量插入代码,例如日志/统计等。
因为代理对象最终要调用实际对象的方法,因此代理对象必须拥有原始对象所有想被代理的方法,体现在 jdk 动态代理中就是它们需要实现相同的接口,接口中的方法就是需要被代理的方法。为什么强调 jdk 动态代理?因为还有一个叫做 cglib 的开源库,也被广泛用于实现动态代理,它通过创建子类做到「代理对象拥有原始对象的所有方法」。
静态代理
所谓静态代理,就是在编译时就已经存在的代理,编译完成后以字节码的形式存在于程序中,是程序的一部分。说人话就是:一个普通的类。
用上面房产中介的例子来实现一个静态代理。
public interface Owner {
String sell(String buyer);
String let(String customer);
}
/* 业主,被代理 */
public class HouseOwner implements Owner {
@Override
public String sell(String buyer) {
return buyer + ",成交!";
}
@Override
public String let(String customer) {
return customer + ",欢迎入住!";
}
}
/* 房产中介,代理 */
public class HouseAgent implements Owner {
private Owner obj; // 原始对象,被代理的对象
public HouseAgent(Owner obj) {
this.obj = obj;
}
@Override
public String sell(String buyer) {
System.out.println("带" + buyer + "看房");
System.out.println("签署销售协议");
return obj.sell(buyer); // 调用被代理对象的方法,透传参数与返回值
}
@Override
public String let(String customer) {
System.out.println("带" + customer + "看房");
System.out.println("签署租赁协议");
return obj.let(customer); // 调用被代理对象的方法,透传参数与返回值
}
}
// 使用
HouseOwner houseOwner = new HouseOwner();
HouseAgent agent = new HouseAgent(houseOwner);
agent.sell("Chenhe");
jdk 动态代理
上面的静态代理的确分担了业主的职责,但不够灵活。比如无论是出租还是销售都先看房,最后通知业主。倘若业主还有更多需要先看房业务:长租/短租/民宿... 那我们不得不把每一个方法都代理一遍,并且都写上看房和通知业主的代码,非常冗余。
动态代理可以解决这个问题。所谓「动态」就是编译时这个代理类并不存在,而是在运行时实时生成字节码。显然这种技术依赖于反射。要使用 jdk 动态代理,需要满足以下条件:
- 目标类(要代理的类)实现了接口。
- 要代理的方法属于这些接口。
依然是实现一个上面房产中介的例子,因为代理是动态生成的,所以无需编写 HouseAgent
类。
public class HouseAgentUtil {
public static Owner getAgent(Owner houseOwner) {
return (Owner) Proxy.newProxyInstance(
HouseAgentUtil.class.getClassLoader(),
new Class[]{Owner.class},
(proxy, method, args) -> {
System.out.println("带" + args[0] + "看房");
switch (method.getName()) {
case "sell" -> System.out.println("签署销售协议");
case "let" -> System.out.println("签署租赁协议");
}
// 调用被代理对象的方法,透传参数与返回值
return method.invoke(houseOwner, args);
}
);
}
}
// 使用
HouseOwner houseOwner = new HouseOwner();
Owner agent = HouseAgentUtil.getAgent(houseOwner) // 动态创建代理对象
agent.sell("Chenhe"); // 此处会触发 InvocationHandler,args 值为 ["Chenhe"]
这里调用 jdk 的 Proxy.newProxyInstance()
方法生成并实例化了一个类。相当于我们在运行时编写 HouseAgent
java 代码,其背后当然比较复杂,幸运的是 jdk 帮我们封装好了。这个方法有三个参数:
ClassLoader
,因为是运行时生成的字节码,肯定需要先被 jvm 加载才可以运行,所以需要一个类加载器。通常使用当前类的加载器就行。Class<?>[] interfaces
要实现的接口,也就是要代理哪些方法。允许实现多个接口所以这里以数组形式传入。InvocationHandler
是最核心的参数,它控制着代理的行为,相当于静态代理中编写的一个个接口实现。
InvocationHandler
是个接口,本质上是一个回调。当这个代理类被调用时会触发,从而执行具体的代理行为。这里使用匿名内部类(lambda 语法)实现。它也接受三个参数:
Object proxy
是代理对象,也就是动态生成的这个代理类的一个实例。注意不是被代理的对象。Method method
调用的方法。Object[] args
传入的参数。
由此看出,静态代理中接口方法的实现被整合在了一个方法中,因此可以方便地执行统一代码(比如看房和通知业主),还可以根据具体的方法(method
)执行不同的逻辑。
cglib 动态代理
使用 jdk API 实现动态代理要求要代理的方法必须在接口中声明,这大大限制了适用场景。尤其是我们希望代理第三方类时,很难保证想要代理的方法正好被抽象到接口中。
cglib 则通过继承来实现动态代理。具体来说 cglib 尝试生成目标类的子类,覆盖(重写)要代理的方法,根据需要通过 super
调用父类(目标类)原始方法,并可加入其他代码。从原理不难看出,cglib 的动态代理有如下要求;
- 目标类必须不是 final 的。
- 要代理的方法也不是 final。
要使用 cglib 首先得添加依赖:
implementation("cglib:cglib:3.3.0")
继续实现房产中介的例子。
public class HouseAgentUtil {
public static HouseOwner getCglibAgent(HouseOwner houseOwner) {
// 创建代理类并设置为 HouseOwner 的子类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HouseOwner.class);
// 设置方法调用回调
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("带" + args[0] + "看房");
switch (method.getName()) {
case "sell" -> System.out.println("签署销售协议");
case "let" -> System.out.println("签署租赁协议");
}
// 调用被代理对象的方法,透传参数与返回值
return proxy.invokeSuper(houseOwner, args);
});
return (HouseOwner) enhancer.create();
}
}
// 使用
HouseOwner houseOwner = new HouseOwner();
HouseOwner agent = HouseAgentUtil.getCglibAgent(houseOwner);
agent.sell("Chenhe");
可以看到在这个实现中完全没有使用到接口 Owner
。MethodInterceptor
相当于是 jdk 动态代理中的 InvocationHandler
,都是方法调用的回调。不同是的它有四个参数,前三个与 jdk 实现类似,最后一个是多出来的。
Object obj
是代理对象,也就是 cglib 动态创建的代理类的对象。Method method
是调用的方法,属于代理对象。Object[] args
传入的参数。MethodProxy proxy
专门用于调用原对象方法的参数,通过proxy.invokeSuper()
实现。