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

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

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

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

静态代理(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 的请求都需要定义在接口里的原因了吗?

总结

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

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

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

相关推荐
哪 吒4 小时前
最简单的设计模式,抽象工厂模式,是否属于过度设计?
设计模式·抽象工厂模式
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
OkeyProxy4 小时前
什麼是ISP提供的公共IP地址?
代理模式·proxy模式·ip地址·isp·海外ip代理
转世成为计算机大神7 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
小乖兽技术8 小时前
23种设计模式速记法
设计模式
.Ayang9 小时前
SSRF漏洞利用
网络·安全·web安全·网络安全·系统安全·网络攻击模型·安全架构
.Ayang9 小时前
SSRF 漏洞全解析(概述、攻击流程、危害、挖掘与相关函数)
安全·web安全·网络安全·系统安全·网络攻击模型·安全威胁分析·安全架构
小白不太白95010 小时前
设计模式之 外观模式
microsoft·设计模式·外观模式
小白不太白95010 小时前
设计模式之 原型模式
设计模式·原型模式
澄澈i10 小时前
设计模式学习[8]---原型模式
学习·设计模式·原型模式