重修设计模式-结构型-代理模式
在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
代理模式通过创建一个代理对象,使得客户端对目标对象的访问都通过代理对象间接进行,从而可以在不修改目标对象的前提下,增加额外的功能操作,如权限控制、日志记录、事务处理等。代理模式又分为静态代理和动态代理。
静态代理(Static Proxy):
在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了,即静态代理是硬编码在程序中的。
静态代理有两种实现方式,一种是代理类和原始类实现同一接口,一种是代理类继承原始类实现。
比较常用的是通过接口实现方式,因为这种方式更符合接口隔离原则,面向接口而非实现编程,代码可读性更高,且接口会起到一种规范的作用。如果原始类没有定义接口,也无法修改原始类,就可以通过继承实现。
举个例子,应用在做性能监控,需要埋点每个接口的请求时间并记录,上报逻辑已封装成 ReportUtil
,按需求直接实现代码如下:
//请求封装,忽略接口请求逻辑
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"
}
}
代码实现非常简单,但有很明显的两个问题。
- 接口业务请求和埋点上报逻辑耦合在一起,不符合单一职责,接口时长上报是单独的逻辑,业务类应该只聚焦业务的处理。
- 需求增改困难,如果修改埋点上报方法参数,需要改到每个接口中的代码,不符合开闭原则。
接口实现方式:
下面用接口实现的静态代理将代码改造,首先需要定义出通用接口:
interface IService {
fun login(phone: String, pwd: String): Any?
fun register(phone: String, pwd: String): Any?
}
原始类继承该接口,并实现请求逻辑:
//请求封装,忽略接口请求逻辑
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"
}
}
代理类继承同一接口,并持有原始类,在接口现实时,增加额外代码并调用原始类方法:
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
}
}
调用处:
val requestProxy = ServiceRequestProxy(ServiceRequestImpl())
requestProxy.login("name", "123")
由于实现了同一接口,将原始类替换为代理类也只需要改动很少的代码。
继承方式实现:
如果原始类并没有定义接口,且代码也无法改动(比如三方库中代码),那么就需要代理类去继承原始类,以扩展附加功能。
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)
}
}
}
调用处:
val requestImpl = ServiceRequestProxy()
requestImpl.login("name", "123")
继承的方式对 final 类和 final 方法是无效的,这种方式缺点是子类和父类耦合严重,代码可读性也会变差,且 Java 不支持多继承,这可能影响一些场景;优点是不需要原始类继承接口,适合更灵活的场景。
动态代理(Dynamic Proxy):
不事先为每个原始类编写代理类,而是在运行的时候动态创建,然后在系统中用代理类替换掉原始类。
静态代理虽然解决了一些问题,但也带来了新的问题:
- 如果原始类中新增接口方法,又要改动代理类中代码,新增代理逻辑。
- 在代理类中,需要将原始类的所有方法都实现一遍,并为每个方法都增加相似逻辑,有很多重复的模板代码。
- 如果要增加附加功能的类有很多,就需要为每个原始类都创建一个代理类,导致类的数量成倍增加。
这时就可以用动态代理解决上面问题,不提前创建代理类,而是在运行时动态创建,并在使用时替换掉原始类
。
在 Java 开发中,代理技术有 JDK 动态代理和 CGLib 动态代理两种方式,JDK 动态代理是 Java 自带的,需要被代理对象必须实现一个或多个接口,底层是利用Java的反射机制,在运行时动态地创建代理类,因此在生成代理对象时会消耗一定的时间。但在执行方法时,因为是接口代理,所以调用速度相对较快。CGLib 动态代理需要引用 cglib 和 asm 库,通过继承实现,可以代理没有实现接口的类,但无法代理 final 类和方法,底层通过ASM框架生成字节码,创建被代理类的子类,重写非final方法,在调用时通过MethodInterceptor
拦截,执行增强逻辑,适用于被代理类没有实现任何接口且无法更改的场景。
最常用的是 JDK 动态代理,主要依赖于java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口。以上面的接口埋点上报为例,利用 JDK 动态代理实现如下:
-
定义接口,也就是上面的
IService
-
实现
InvocationHandler
接口,该接口中的invoke
方法会在代理对象的方法被调用时自动执行,在此实现附加逻辑。这里增加了接口时长的埋点逻辑。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 } }
-
创建代理实例并使用,通过
Proxy
类的newProxyInstance
静态方法创建代理实例。参数1:类加载器(通常使用目标对象的类加载器)参数2:目标对象实现的接口数组 参数三:实现了
InvocationHandler
接口的处理器对象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
的核心就是基于动态代理实现的,它的核心源码如下:
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 的请求都需要定义在接口里的原因了吗?
总结
代理的实现有两种方式,一是实现同一接口,二是直接继承。
代理又分为静态代理和动态代理,动态代理的动态指的是不需要提前创建代理类,只需要用面向切面的思想,单独考虑增强的逻辑即可,代理类会在运行时由系统动态创建。
代理模式是一种非常有用的设计模式,通过引入代理对象来控制对目标对象的访问,可以在不修改目标对象的前提下增加额外的功能,提高系统的灵活性和可扩展性。