代理模式与静态代理、动态代理的实现(Proxy.newProxyInstance、InvocationHandler)

代理模式

代理模式是23种设计模式中比较常用的一种,属于结构型设计模式。在 Android 领域中,有大量的库都使用了代理模式,例如 Retrofit 使用动态代理来实现 API 接口的调用,Dagger 使用代码生成和反射机制来创建依赖注入的代理对象等等。本文将带你了解代理模式,并且介绍静态代理和动态代理的实现方式。

所谓的代理模式,就是当你需要去访问一个对象时,你不直接去访问这个目标对象,而是访问这个目标对象的代理对象。打个比方,你想要跟一个明星打交道,一般来说,你是不能直接找到明星的,只能去找他的经纪人,这个经纪人就相当于代理。这种模式的好处就是可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

代理的实现方式有三种:静态代理、动态代理以及使用 cglib 或 bytebuddy 开源库创建的代理,不过这里我们现只关注前两个比较常用的。

静态代理

静态代理在使用时 , 需要定义接口或者父类 , 被代理对象 ( 即目标对象 ) 与代理对象一起实现相同的接口或者是继承相同父类。用的时候通过调用代理对象的方法来调用目标对象。例如,我们现定义一个唱歌的接口,然后定义歌手和经纪人两个类来分别实现这个接口。

1. 定义目标对象和代理对象都要实现的接口:

java 复制代码
interface ISinger {
    void sing();
}

2. 定义目标对象的类:

java 复制代码
class Singer implements ISinger {
    @Override
    public void sing() {
        System.out.println("歌手在唱歌...");
    }
}

3. 定义代理对象的类:

java 复制代码
class Agent implements ISinger {

    private Singer singer;

    public Agent(Singer singer) {
        this.singer = singer;
    }

    @Override
    public void sing() {
        System.out.println("经纪人在歌手唱歌之前...");
        singer.sing();
        System.out.println("经纪人在歌手唱歌之后...");
    }
}

4. 使用代理类:

java 复制代码
class Client {
    public static void main(String[] args) {
        Singer singer = new Singer();
        Agent agent = new Agent(singer);
        agent.sing();
    }
}

输出:

经纪人在歌手唱歌之前...
歌手在唱歌...
经纪人在歌手唱歌之后...

简单总结一下,静态代理中代理对象与目标对象要实现相同的接口 , 然后通过调用相同的方法来调用目标对象的方法。这里不过通过这里也能看到静态代理的缺点, 由于代理对象和目标对象都要实现相同的接口,所以必然会存在代码冗余,扩展性差等特点。

静态代理虽然简单直观,但在面对复杂系统和需求变化时存在明显的缺点。这个时候,动态代理就出现了,动态代理通过在运行时生成代理类,提供了更高的灵活性和扩展性,是实际开发中更常用的代理模式实现方式。

动态代理

在动态代理中,代理对象不需要实现接口,但是目标对象还是需要实现接口。代理对象的生成,是利用 JDK 的 API ,动态的在内存中构建代理对象。那么是哪个 API 用来生成代理对象呢?

1. Java 中用于生成代理对象的API

在 Java 中,java.lang.reflect.Proxy 类为对象生成代理提供了方法:

java 复制代码
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • 参数一 loader:用于指定用哪个类加载器,去加载生成的代理类;
  • 参数二 interfaces:指定这个代理类能够代理目标对象的哪些方法和接口;
  • 参数三 h:用来指定生成的代理对象在方法被调用时如何进行处理;

下面就将上面的静态代理例子改成动态代理。

2. 确定目标对象接口和目标对象类

这个步骤的代码是 ISinger 接口和 Singer 类,跟上面静态代码是一样的

java 复制代码
interface ISinger {
    void sing();
}

class Singer implements ISinger {
    @Override
    public void sing() {
        System.out.println("歌手在唱歌...");
    }
}

3. 调用 newProxyInstance 方法生成代理对象:

java 复制代码
Singer singer = new Singer();
ISinger proxy = (ISinger) Proxy.newProxyInstance(
    singer.getClass().getClassLoader(),
    singer.getClass().getInterfaces(), 
    new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("Before method: " + method.getName());
            Object result = method.invoke(singer, args);
            System.out.println("After method: " + method.getName());
            return result;
        }
    });

这里 Proxy.newProxyInstance 的三个参数前面已经说过了,但是第三个参数是一个 InvocationHandler 接口,当外部调用代理对象的方法时,都会调用到这个接口中的 invoke 方法。这个方法的三个参数意义如下:

java 复制代码
public class InvocationHandler {
    /**
     * @param proxy:代理实例,即当前代理对象本身。当 invoke 方法被调用时,这个参数就是代理对象的一个引用。
     * @param method:被调用的方法对象。它表示代理实例上被调用的方法,在 invoke 方法中可以需要调用 method.invoke 以触发方法。
     * @param args:调用方法时传递的参数数组。若没有参数,则该数组为 null。
     */
    public Object invoke(Object proxy, Method method, Object[] args);
}

这里要注意一点,如果在 invoke 中调用了 proxy 对象的方法,就很有可能造成代理对象方法的递归调用,这样会出现栈溢出的异常。

4. 使用代理对象:

java 复制代码
//通过代理对象,调用目标对象的方法
proxy.sing();

//输出:
//Before method: sing
//歌手在唱歌...
//After method: sing

可见,动态代理使得我们可以在运行时为目标对象添加额外的功能,而无需修改目标对象的代码。通过这个例子,咱们也了解了如何创建动态代理。

但是有人又问了,即使是动态代理,也需要目标对象实现一些接口,那有没有办法可以做任何对象的代理,而不要求目标对象时间接口呢?还真有这种方法,那就是使用 cglib 或是 ByteBuddy 库。

关于 cglib 或 ByteBuddy 库

前面说到,静态代理和动态代理模式都要求目标对象是实现一个接口 , 但是有时候目标对象只是一个单独的对象, 并没有实现任何的接口 , 这个时候想要实现代理,就必须使用 cglib(https://github.com/cglib/cglib) 或是 ByteBuddy(https://bytebuddy.net/#/) 库。

但是本文不讲解这两个库,只是想让大家知道有这么两个东西能够实现这种代理,如果需要,还需要大家自行查阅相关资料。

其中 cglib 已经不再维护了,而且在新版本的 Java 中似乎有些问题,因此现在更推荐 ByteBuddy 这个库。

相关推荐
儿时可乖了6 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol7 分钟前
java基础概念37:正则表达式2-爬虫
java
xmh-sxh-131424 分钟前
jdk各个版本介绍
java
XINGTECODE37 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码43 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶43 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django