重修设计模式-结构型-代理模式

重修设计模式-结构型-代理模式

在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。

代理模式通过创建一个代理对象,使得客户端对目标对象的访问都通过代理对象间接进行,从而可以在不修改目标对象的前提下,增加额外的功能操作,如权限控制、日志记录、事务处理等。代理模式又分为静态代理和动态代理。

静态代理(Static Proxy):

在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了,即静态代理是硬编码在程序中的。

静态代理有两种实现方式,一种是代理类和原始类实现同一接口,一种是代理类继承原始类实现。

比较常用的是通过接口实现方式,因为这种方式更符合接口隔离原则,面向接口而非实现编程,代码可读性更高,且接口会起到一种规范的作用。如果原始类没有定义接口,也无法修改原始类,就可以通过继承实现。

举个例子,应用在做性能监控,需要埋点每个接口的请求时间并记录,上报逻辑已封装成 ReportUtil,按需求直接实现代码如下:

kotlin 复制代码
//请求封装,忽略接口请求逻辑
class ServiceRequest {
    fun login(phone: String, pwd: String): Any? {
        val start = System.currentTimeMillis()
        println("login:$phone - $pwd") //接口调用逻辑
        ReportUtil.report("login", System.currentTimeMillis() - start)
        return "success"
    }

    fun register(phone: String, pwd: String): Any? {
        val start = System.currentTimeMillis()
        println("register:$phone - $pwd") //接口调用逻辑
        ReportUtil.report("register", System.currentTimeMillis() - start)
        return "success"
    }
}

代码实现非常简单,但有很明显的两个问题。

  1. 接口业务请求和埋点上报逻辑耦合在一起,不符合单一职责,接口时长上报是单独的逻辑,业务类应该只聚焦业务的处理。
  2. 需求增改困难,如果修改埋点上报方法参数,需要改到每个接口中的代码,不符合开闭原则。

接口实现方式:

下面用接口实现的静态代理将代码改造,首先需要定义出通用接口:

kotlin 复制代码
interface IService {
    fun login(phone: String, pwd: String): Any?
    fun register(phone: String, pwd: String): Any?
}

原始类继承该接口,并实现请求逻辑:

kotlin 复制代码
//请求封装,忽略接口请求逻辑
class ServiceRequestImpl: IService {
    override fun login(phone: String, pwd: String): Any? {
        println("login:$phone - $pwd") //接口调用逻辑
        return "success"
    }

    override fun register(phone: String, pwd: String): Any? {
        println("register:$phone - $pwd") //接口调用逻辑
        return "success"
    }
}

代理类继承同一接口,并持有原始类,在接口现实时,增加额外代码并调用原始类方法:

kotlin 复制代码
class ServiceRequestProxy(val request: ServiceRequestImpl): IService {
    override fun login(phone: String, pwd: String): Any? {
        val start = System.currentTimeMillis()
        val result = request.login(phone, pwd) //调用原逻辑
        ReportUtil.report("login", System.currentTimeMillis() - start)
        return result
    }

    override fun register(phone: String, pwd: String): Any? {
        val start = System.currentTimeMillis()
        val result = request.register(phone, pwd) //调用原逻辑
        ReportUtil.report("register", System.currentTimeMillis() - start)
        return result
    }
}

调用处:

kotlin 复制代码
val requestProxy = ServiceRequestProxy(ServiceRequestImpl())
requestProxy.login("name", "123")

由于实现了同一接口,将原始类替换为代理类也只需要改动很少的代码。

继承方式实现:

如果原始类并没有定义接口,且代码也无法改动(比如三方库中代码),那么就需要代理类去继承原始类,以扩展附加功能。

kotlin 复制代码
class ServiceRequestProxy: ServiceRequest() {
    override fun login(phone: String, pwd: String): Any? {
        val start = System.currentTimeMillis()
        val result = super.login(phone, pwd)
        ReportUtil.report("login", System.currentTimeMillis() - start)
        return result
    }

    override fun register(phone: String, pwd: String): Any? {
        val start = System.currentTimeMillis()
        return super.login(phone, pwd).also { //用also简化代码
            ReportUtil.report("register", System.currentTimeMillis() - start)
        }
    }
}

调用处:

kotlin 复制代码
val requestImpl = ServiceRequestProxy()
requestImpl.login("name", "123")

继承的方式对 final 类和 final 方法是无效的,这种方式缺点是子类和父类耦合严重,代码可读性也会变差,且 Java 不支持多继承,这可能影响一些场景;优点是不需要原始类继承接口,适合更灵活的场景。

动态代理(Dynamic Proxy):

不事先为每个原始类编写代理类,而是在运行的时候动态创建,然后在系统中用代理类替换掉原始类。

静态代理虽然解决了一些问题,但也带来了新的问题:

  1. 如果原始类中新增接口方法,又要改动代理类中代码,新增代理逻辑。
  2. 在代理类中,需要将原始类的所有方法都实现一遍,并为每个方法都增加相似逻辑,有很多重复的模板代码。
  3. 如果要增加附加功能的类有很多,就需要为每个原始类都创建一个代理类,导致类的数量成倍增加。

这时就可以用动态代理 解决上面问题,不提前创建代理类,而是在运行时动态创建,并在使用时替换掉原始类

在 Java 开发中,代理技术有 JDK 动态代理和 CGLib 动态代理两种方式,JDK 动态代理是 Java 自带的,需要被代理对象必须实现一个或多个接口,底层是利用Java的反射机制,在运行时动态地创建代理类,因此在生成代理对象时会消耗一定的时间。但在执行方法时,因为是接口代理,所以调用速度相对较快。CGLib 动态代理需要引用 cglib 和 asm 库,通过继承实现,可以代理没有实现接口的类,但无法代理 final 类和方法,底层通过ASM框架生成字节码,创建被代理类的子类,重写非final方法,在调用时通过MethodInterceptor拦截,执行增强逻辑,适用于被代理类没有实现任何接口且无法更改的场景。

最常用的是 JDK 动态代理,主要依赖于java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。以上面的接口埋点上报为例,利用 JDK 动态代理实现如下:

  1. 定义接口,也就是上面的 IService

  2. 实现 InvocationHandler 接口,该接口中的 invoke 方法会在代理对象的方法被调用时自动执行,在此实现附加逻辑。这里增加了接口时长的埋点逻辑。

    kotlin 复制代码
    class DynamicProxyHandler(val target: Any?) : InvocationHandler {  //target是原始类对象
        //proxy是代理类对象本身,一般不会用到
        override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
            // 在方法调用之前可以添加自定义操作
            val start = System.currentTimeMillis()
    
            // 调用目标对象的实际方法 参数1:原始类对象 参数2:方法参数
            val result = method?.invoke(target, args)
    
            // 在方法调用之后可以添加自定义操作
            ReportUtil.report("${method?.name}", System.currentTimeMillis() - start)
    
            return result
        }
    }
  3. 创建代理实例并使用,通过 Proxy 类的 newProxyInstance 静态方法创建代理实例。

    参数1:类加载器(通常使用目标对象的类加载器)参数2:目标对象实现的接口数组 参数三:实现了InvocationHandler接口的处理器对象

    kotlin 复制代码
    val request = ServiceRequestImpl()  //原始类
    val requestProxy = Proxy.newProxyInstance(
        IService::class.java.classLoader, arrayOf<Class<*>>(IService::class.java),
        DynamicProxyHandler(request)
    ) as IService
    requestProxy3.login("name", "123")

相对于静态代理,动态代理不仅节省了编码工作量,还能在原始类和接口还未知的时候就确定了代理行为,实现了解耦,让开发者可以用面向切面编程(AOP, Aspect Oriented Programming)的思想进行开发工作。

动态代理的应用:Retrofit

Android 端著名的网络请求封装库 Retrofit 的核心就是基于动态代理实现的,它的核心源码如下:

java 复制代码
public <T> T create(final Class<T> service) {
  validateServiceInterface(service);
  return (T)
      Proxy.newProxyInstance(
          service.getClassLoader(),
          new Class<?>[] {service},
          new InvocationHandler() {
            private final Platform platform = Platform.get();
            private final Object[] emptyArgs = new Object[0];

            @Override
            public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                throws Throwable {
              // If the method is a method from Object then defer to normal invocation.
              if (method.getDeclaringClass() == Object.class) {
                return method.invoke(this, args);
              }
              args = args != null ? args : emptyArgs;
              return platform.isDefaultMethod(method)
                  ? platform.invokeDefaultMethod(method, service, proxy, args)
                  : loadServiceMethod(method).invoke(args);
            }
          });
}

可以看到,在调用 Retrofit 的 create 方法之后,会创建出代理对象并返回,这样在外部调用接口方法时,invoke 内部会对方法的注解进行解析,拿到请求路径,请求头,请求方式等信息,最终拼接出一个网络请求并通过 Okhttp 实现真正的接口访问操作。

现在你知道 Retrofit 的请求都需要定义在接口里的原因了吗?

总结

代理的实现有两种方式,一是实现同一接口,二是直接继承。

代理又分为静态代理和动态代理,动态代理的动态指的是不需要提前创建代理类,只需要用面向切面的思想,单独考虑增强的逻辑即可,代理类会在运行时由系统动态创建。

代理模式是一种非常有用的设计模式,通过引入代理对象来控制对目标对象的访问,可以在不修改目标对象的前提下增加额外的功能,提高系统的灵活性和可扩展性。

相关推荐
凯子坚持 c4 小时前
深入Linux权限体系:守护系统安全的第一道防线
linux·运维·系统安全
大圣数据星球5 小时前
Fluss 写入数据湖实战
大数据·设计模式·flink
思忖小下6 小时前
梳理你的思路(从OOP到架构设计)_设计模式Template Method模式
设计模式·模板方法模式·eit
思忖小下16 小时前
梳理你的思路(从OOP到架构设计)_简介设计模式
设计模式·架构·eit
biubiubiu070616 小时前
代理模式(JDK,CGLIB动态代理,AOP切面编程)
代理模式
liyinuo201718 小时前
嵌入式(单片机方向)面试题总结
嵌入式硬件·设计模式·面试·设计规范
aaasssdddd9621 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
T1an-121 小时前
设计模式之【观察者模式】
观察者模式·设计模式
思忖小下1 天前
梳理你的思路(从OOP到架构设计)_设计模式Factory Method模式
设计模式·工厂方法模式·eit
霁月风1 天前
设计模式——工厂方法模式
c++·设计模式·工厂方法模式