CONTENT
代理模式
定义: 代理模式(Proxy Pattern),设计模式的一种,为其他对象提供一种代理以控制对这个对象的访问。
用途:
-
在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
-
使用代理对象可以在不修改目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。比如说在目标对象的某个方法执行前后可以增加一些自定义的操作。
比喻: 类似与明星(目标对象)和经纪人(代理对象)的关系,客户想谈合作不直接找明星(目标对象),而是找他的经纪人(代理对象)谈合作,然后明星(目标对象)来做客户要求的工作。
优点: 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。符合代码设计的开闭原则(对扩展开放,对修改关闭)。远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
缺点: 由于在客户端和目标对象之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
分类:
-
远程代理:为不同地理的对象提供局域网代表对象。
-
虚拟代理:根据需要将资源消耗很大的对象进行延迟,真正需要的时候再创建。
-
安全代理:控制用户的访问权限。
-
智能代理:提供对目标对象额外的服务「使用最多的」。(静态代理和动态代理)
静态代理
静态代理中的 静态 指的是 代理类 在编译期间就创建好了,是手动创建的类,不是运行时动态生成的。也就是说 在编译时就已经将 接口,目标类,代理类等确定下来。
其实 实现某一接口或继承某一类,都可以实现静态代理,但是平时说的静态代理指的是使用接口的静态代理,因为它更灵活。
为什么需要接口?
代理类与目标类之间通常会存在关联关系,一个代理类的对象与一个目标类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用目标类的对象的相关方法,来提供特定的服务。使用接口,当换一个实现相同接口的目标类时,不需要写新的代理类,静态代理的情况下,用接口更灵活。
from:优快云:夢_殤:JAVA静态代理为什么用聚合用接口
优点:
- 在符合开闭原则的情况下,对目标对象功能进行扩展和拦截。
- 访问无法访问的资源,职责清晰。
缺点:
- 每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。一个 代理类 只能服务于 一种 目标类(实现某一接口)。也就是说 需要对实现不同接口的目标类都单独写一个代理类,多个代理类代码可能冗余。
- 代理类和目标类必须实现相同的接口。假如接口中新增了一个方法,那么目标类和代理类都要实现新方法,同时代理类可能还要对新方法写和之前一样的操作。这会很繁琐,代码也需要经常修改,维护代价高。
- 代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。
以 明星(目标对象)和 经纪人(代理对象)举例:
// 工作者的接口
interface Workable {
void work();
}
// 目标类 明星
class Star implements Workable {
@Override
public void work() {
System.out.println("Star sing and dance.");
}
}
// 代理类 经纪人
class Broker implements Workable {
private final Workable worker;
public Broker(Workable worker) {
this.worker = worker;
}
@Override
public void work() {
System.out.println("Broker do some prep work.");
worker.work();
System.out.println("Broker do some finishing work.");
}
}
public class StaticProxy {
public static void main(String[] args) {
// 目标对象 明星
Star star = new Star();
// 代理对象 经纪人
Broker broker = new Broker(star);
// 通过 代理对象(经纪人) 调用 目标对象(明星) 完成客户的工作要求
broker.work();
}
}
OutPut 程序输出
Broker do some prep work.
Star sing and dance.
Broker do some finishing work.
动态代理
那么有没有这样的代理,不管接口是否新增了方法,代理类都不需要做任何修改呢?对于相同的代理类代码只写一次呢?有,那就是 JDK 动态代理。
那么有没有这样的代理,不管是否实现了接口,都能实现代理呢?有,那就是 CGLIB 动态代理。
相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理目标类。
动态代理是指客户通过代理类来调用其它目标对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。动态代理是在运行时动态生成类字节码,并加载到 JVM 中。
JDK 动态代理(基于接口)
※ JDK 是通过接口实现的动态代理,目标类必须实现至少一个接口 ※
JDK 中生成代理对象主要涉及的类是:java.lang.reflect.Proxy,接口是 java.lang.reflect.InvocationHandler。
Proxy 类中用到的方法,方法返回一个 代理类的对象
public static Object newProxyInstance(ClassLoader loader, // 指定当前目标对象使用的类加载器
Class<?>[] interfaces, // 目标对象实现的一些接口的类型
InvocationHandler h) // 事件处理器,就是实现了下面接口的对象
InvocationHandler 接口中要实现的方法,实际 代理部分 的处理逻辑,当动态代理对象调用一个方法时,这个方法的实际调用就会被转发到实现 InvocationHandler 接口类中的 invoke 方法。
public Object invoke(Object proxy, Method method, Object[] args)
// proxy : 动态生成的 代理对象
// method : 与代理类对象调用的方法相对应
// args : 当前 method 方法的参数
以 明星(目标对象)和 经纪人(代理对象)举例:
这里如果 Workable 接口新增了方法 workFast(),如果代理不用 workFast(),完全不用改,如果代理用 workFast(),但在 workFast() 方法前后做的事情和 work() 方法是一样的,那也不用改。或者说如果以后不代理 Star 类了,比如说 代理 Child,Child 实现 Playable 接口,如果在 play() 方法前后做的事情是一样的,BrokerInvocationHandler 和 JdkProxyFactory 都不用改。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 工作者的接口
interface Workable {
void work();
}
// 目标类 明星
class Star implements Workable {
@Override
public void work() {
System.out.println("Star sing and dance.");
}
}
// 经纪人 实际工作,如果通过 经纪人代理对象 调用 明星目标对象,实际会转到执行这里的 invoke 方法
class BrokerInvocationHandler implements InvocationHandler {
// target :目标对象
private final Object target;
public BrokerInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
// 为了证明代理类是动态生成的
System.out.println("proxy.getClass().getName() : " + proxy.getClass().getName());
System.out.println("Broker do some prep work.");
result = method.invoke(target, args);
System.out.println("Broker do some finishing work.");
return result;
}
}
// 代理对象 的工厂类,生产代理类的代理对象,这里只生产 BrokerInvocationHandler 经纪人
class JdkProxyFactory {
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载器
target.getClass().getInterfaces(), // 代理需要实现的接口(可指定多个)
new BrokerInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler
);
}
}
public class DynamicProxyJDK {
public static void main(String[] args) {
// 目标对象 明星
Star star = new Star();
// 代理对象 经纪人
Workable broker = (Workable) JdkProxyFactory.getProxy(star);
// 通过 代理对象(经纪人) 调用 目标对象(明星) 完成客户的工作要求
broker.work();
}
}
OutPut 程序输出
proxy.getClass().getName() : $Proxy0 // 为了证明代理类是动态生成的
Broker do some prep work.
Star sing and dance.
Broker do some finishing work.
CGLIB 动态代理(基于类继承)
※ CGLIB 是通过继承实现的动态代理,目标类不能声明为 final,调用的方法不能声明为 final ※
CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。
很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
CGLIB(Code Generation Library) 实际是属于一个开源项目,使用 cglib 需要引入 cglib 的 jar 包。
pom.xml
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
CGLIB 中生成代理对象主要涉及的类是:net.sf.cglib.proxy.Enhancer,接口是:net.sf.cglib.proxy.MethodInterceptor。
Enhancer 类中用到的方法,方法返回一个 代理类的对象
public Object create()
在调用 create() 创建代理对象之前,需要设置几个量,调用的是 Enhancer 类中 或者是 Enhancer 继承的父类 AbstractClassGenerator 中的 set 方法
public void setClassLoader(ClassLoader classLoader) // 设置当前目标对象使用的类加载器,父类中
public void setSuperclass(Class superclass) // 设置目标类,Enhancer 类中
public void setCallback(Callback callback) // 设置方法拦截器,父类中
MethodInterceptor 接口中要实现的方法,实际 代理部分 的处理逻辑,当动态代理对象调用一个方法时,这个方法的实际调用就会被拦截到实现 MethodInterceptor 接口类中的 intercept 方法。
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
// o: 动态生成的 代理对象
// method : 与代理对象调用的方法相对应,目标类被拦截的方法
// args : 当前 method 方法的参数
// methodProxy :代理对象调用的方法,因为不需要目标对象,用它调用父类(目标类)的方法
以 明星(目标对象)和 经纪人(代理对象)举例:
可以看到 明星(目标类)不再需要实现一个 Workable 接口。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
// 目标类 明星
class Star {
public void work() {
System.out.println("Star sing and dance.");
}
}
// 经纪人 实际工作,如果通过 经纪人代理对象 调用 明星目标对象,实际会拦截到执行这里的 intercept 方法
class BrokerMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object result = null;
// 为了证明代理类是动态生成的
System.out.println("o.getClass().getName() : " + o.getClass().getName());
System.out.println("Broker do some prep work.");
result = methodProxy.invokeSuper(o, args);
System.out.println("Broker do some finishing work.");
return result;
}
}
// 代理对象 的工厂类,生产代理类的代理对象,这里只生产 BrokerMethodInterceptor 经纪人
class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
Enhancer enhancer = new Enhancer(); // 创建动态代理字节码增强器
enhancer.setClassLoader(clazz.getClassLoader()); // 设置目标类的类加载器
enhancer.setSuperclass(clazz); // 设置目标类
enhancer.setCallback(new BrokerMethodInterceptor()); // 设置方法拦截器,代理对象自定义的 MethodInterceptor
return enhancer.create(); // 创建代理类对象
}
}
public class DynamicProxyCGLIB {
public static void main(String[] args) {
// 目标对象 明星
// 不需要目标对象,因为是 通过继承 目标对象 实现的动态代理,代理类继承了 目标类中的方法
// 代理对象 经纪人
Star broker = (Star) CglibProxyFactory.getProxy(Star.class);
// 通过 代理对象(经纪人) 调用 目标类(明星)的方法 完成客户的工作要求
broker.work();
}
}
OutPut 程序输出
o.getClass().getName() : Star$$EnhancerByCGLIB$$e55eba8a // 为了证明代理类是动态生成的
Broker do some prep work.
Star sing and dance.
Broker do some finishing work.
JDK 动态代理 v.s. CGLIB 动态代理
- JDK 是通过接口实现的动态代理,只能代理实现了接口的类或者直接代理接口。CGLIB 是通过继承实现的动态代理,可以代理未实现任何接口的类,但不能代理声明为 final 类型的类,声明为 final、private、static 的方法(final,private 方法不能继承,static 方法不能重写)。
- 就二者的效率来说,JDK 动态代理生成类速度快,调用慢(因为不能直接调用被代理对象的方法,而是要通过反射),CGLIB 生成类速度慢,但后续调用快(因为可以通过计算索引,直接定位到具体的方法)。但是实际上 JDK 的速度随着版本的升级越来越优秀,而 CGLIB 却止步不前。在 JDK8 下 JDK 动态代理性能比 CGLIB 稍好一些。
运行 1 0 5 10^5 105 次
JDK8 | CGLIB | |
---|---|---|
耗时 | 990ms | 1041ms |
JDK 动态代理为什么必须基于接口
-
直接原因:动态生成的代理类(eg:$Proxy0)会实现目标类实现的所有接口 并 继承
java.lang.reflect.Proxy
类,Java 是单继承多实现,所以 JDK 无法利用继承来为目标对象生产代理对象。 -
根本原因:(个人看法,思考很浅)
- 代理接口比代理类更好更灵活些,调接口可以灵活的代理实现接口的多个类。面向接口编程?
- 为了通用,所以牺牲了一些特性,继承目标类的话需要重写方法,就不能代理声明为 final 类型的类,声明为 final、private、static 的方法。
- 。。。
Reference
- 百度百科:代理模式
- 菜鸟教程:代理模式
- JavaGuide:Java 代理模式详解
- thinkkeep.github.io :静态代理模式
- 优快云:夢_殤:JAVA静态代理为什么用聚合用接口
- 简书:Android开发_Hua:静态代理与动态代理详解
- segmentfault:Soarkey:Java三种代理模式:静态代理、动态代理和cglib代理
- B站:尚硅谷Java入门视频教程
- 优快云:风云叶易:实战CGLib系列文章 MethodInterceptor和Enhancer
- 简书:myf008:JDK 动态代理和Cglib性能对比
- 简书:小可怜求放过:JDK动态代理为什么不能代理类–详解动态代理
- segmentfault:MinGRn:关于代理:为什么 JDK 动态代理只能为接口生成代理?
- 知乎:luoxn28:JDK动态代理为什么不能代理类:评论Jeromememory
- 优快云:大火yzs:【AOP系列】自己动手模仿一个可以代理普通类的Proxy类(四)