文章

速通 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 帮我们封装好了。这个方法有三个参数:

  1. ClassLoader,因为是运行时生成的字节码,肯定需要先被 jvm 加载才可以运行,所以需要一个类加载器。通常使用当前类的加载器就行。
  2. Class<?>[] interfaces 要实现的接口,也就是要代理哪些方法。允许实现多个接口所以这里以数组形式传入。
  3. InvocationHandler 是最核心的参数,它控制着代理的行为,相当于静态代理中编写的一个个接口实现。

InvocationHandler 是个接口,本质上是一个回调。当这个代理类被调用时会触发,从而执行具体的代理行为。这里使用匿名内部类(lambda 语法)实现。它也接受三个参数:

  1. Object proxy 是代理对象,也就是动态生成的这个代理类的一个实例。注意不是被代理的对象。
  2. Method method 调用的方法。
  3. 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");

可以看到在这个实现中完全没有使用到接口 OwnerMethodInterceptor 相当于是 jdk 动态代理中的 InvocationHandler,都是方法调用的回调。不同是的它有四个参数,前三个与 jdk 实现类似,最后一个是多出来的。

  1. Object obj 是代理对象,也就是 cglib 动态创建的代理类的对象。
  2. Method method 是调用的方法,属于代理对象。
  3. Object[] args 传入的参数。
  4. MethodProxy proxy 专门用于调用原对象方法的参数,通过 proxy.invokeSuper() 实现。