代理模式-Proxy
一、定义
代理模式给某一个对象提供了一个代理对象,并由代理对象控制对原对象的引用。在一些情况下,一个客户端不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。
二、类图
代理模式中的角色:
●抽象对象角色:声明了目标对象和代理对象的共同接口,这样一来在任何可以使用目标对象的地方都可以使用代理对象。
●目标对象角色:定义了代理对象所代表的目标对象。
●代理对象角色:代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象。代理对象通常在客户端调用传递给目标对象之前或之后执行某个操作,而不是单纯地将调用传递给目标对象。
将上面的结构图转换成代码如下:
/** * 真实角色与代理角色的共同接口 */ interface Subject { void request(); } class RealSubject implements Subject { @Override public void request() { System.out.println("操作"); } } class ProxySubject implements Subject {
//引入被代理的对象 private Subject realSubject;
//通过构造器将被代理的角色传入进来 public ProxySubject(Subject realSubject){ this.realSubject=realSubject; } private void preRequest(){ System.out.println("增强操作:before操作"); } @Override public void request() { //前置增强 preRequest(); realSubject.request();//原操作 //后置增强 postRequest(); } private void postRequest(){ System.out.println("增强操作:after操作"); } } public class Client { public static void main(String[] args) { //真实角色 Subject realSubject = new RealSubject(); //代理角色 Subject proxy = new ProxySubject(realSubject); //真实角色的行为 realSubject.request(); System.out.println("========================"); //代理角色代替真实角色进行操作 proxy.request(); } }
用一个简单的demo来演示一下代理模式
/** * 抽象主题(房东和中介的共同接口) * */ public interface Person { void sellHouse(); } /** * 真实主题(房东) * */ public class HouseOwner implements Person{ @Override public void sellHouse() { System.out.println("sell House"); } } /** * 代理对象(中介) * */ public class Agency implements Person{ private Person houseOwner;//被代理的对象(房东) Agency(Person houseOwner){ this.houseOwner = houseOwner; } @Override public void sellHouse() { System.out.println("中介代理卖房开始"); houseOwner.sellHouse(); System.out.println("中介代理卖房结束"); } } //调用 public static void test(){
//房东自己卖方
HouseOwner houseOwner = new HouseOwner();
houseOwner.sellHouse(); //中介代理房东卖房 HouseOwner houseOwner = new HouseOwner(); Person agency = new Agency(houseOwner); agency.sellHouse(); }
三、静态代理和动态代理
上面的例子实际上是静态代理,静态代理指在编译期就生成了代理对象。显然ProxySubject类是由我们手动实现的,最终会被编译成class文件,这是编译期间完成的,所以就是静态代理。相反,动态代理指的就是在运行时生成代理对象,也就是代理类不是我们在编码时就把该类实现了,而是由程序动态生成,程序运行时自然属于运行期,所以就是动态代理。
静态代理和动态代理的区别可以理解为一个被事先植入流氓程序的app在运行过程中弹广告,和一个正常app在运行过程中被流氓程序拦截而弹出广告。一个是事先增强,一个是运行时动态增强。
四、动态代理的几种实现
动态代理的实现方式主要有:JDK动态代理、ASM底层实现和CGLIB与javassist等高级实现。
1.JDK动态代理
其实JDK中已经提供了对动态代理内置的支持,Proxy官方文档
JDK动态代理只需要用到3个类:
- Proxy
- CLassLoader
- InvocationHandler
public class Client { public static void main(String[] args) { // 真实角色 Subject realSubject = new RealSubject(); ClassLoader loader = RealSubject.class.getClassLoader();// 与真实角色具有相同的classloader Class<?>[] interfaces = { Subject.class };// 共同的接口 InvocationHandler h = new MyInvocationHandler(realSubject);// 实现增强操作 // 代理角色(动态创建) Subject proxy = (Subject) Proxy.newProxyInstance(loader, interfaces, h); // 真实角色的操作 realSubject.request(); // 代理角色代替真实角色进行操作 proxy.request(); } } class MyInvocationHandler implements InvocationHandler { private Subject realSubject; public MyInvocationHandler(Subject realSubject) { this.realSubject = realSubject; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 前置增强 preRequest(); Object rs = method.invoke(realSubject, args); // 后置增强 postRequest(); return rs; } private void preRequest() { System.out.println("增强操作:before操作"); } private void postRequest() { System.out.println("增强操作:after操作"); } }
JDK的动态代理有局限性,就是只能对实现了接口的类进行代理。
2.ASM
ASM是一个Java字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM可以直接产生二进制class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。因此,不鼓励直接使用ASM。
3.CGLIB(基于ASM)
CGLIB在ASM的基础上,做了一些高级的API抽象,从而使得我们能够很容易实现对一个类的增强。由于使用简单,因此有很多的框架都使用它,比如Hibernate,Spring等。(Hibernate3.3把默认使用的javassist更换为cglib)
CGLIB原理:动态生成要代理类的子类,子类重写要代理的类的所有非final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
CGLIB缺点:因为final方法无法被继承,所以无法对final方法进行代理
CGLIB核心类
- net.sf.cglib.proxy.Enhancer 主要的增强类
- net.sf.cglib.proxy.MethodInterceptor 主要的方法拦截类,它是Callback接口的子接口,需要用户实现。
- net.sf.cglib.proxy.MethodProxy JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用。
下面使用CGLIB来实现动态代理。
(说明:需引入cglib的jar包。这里我的jdk环境为jdk7,引入的包为cglib-full-2.0.2.jar)
import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class Client { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(RealSubject.class); enhancer.setCallback(new TargetInterceptor()); // 生成代理类,该代理类为RealSubject的子类 RealSubject proxy = (RealSubject) enhancer.create();// 向上转型 proxy.request(); System.out.println(proxy); } } /** * 目标对象拦截器 */ class TargetInterceptor implements MethodInterceptor { /** * obj为目标对象 ,method为目标方法,params 为参数, */ @Override public Object intercept(Object obj, Method method, Object[] params, MethodProxy proxy) throws Throwable { // 前置增强 preRequest(); Object result = proxy.invokeSuper(obj, params); // 后置增强 postRequest(); return result; } private void preRequest() { System.out.println("增强操作:before操作"); } private void postRequest() { System.out.println("增强操作:after操作"); } } /** * 目标类(不能实现接口) */ class RealSubject { public void request() { System.out.println("操作"); } }
4.Javassist
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
import java.io.UnsupportedEncodingException; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtField; import javassist.CtNewConstructor; import javassist.CtNewMethod; import javassist.NotFoundException; /** * 该例子中使用Javassist的API成功组织出代理类的一个子类,可以看出添加构造函数,增加属性,增加方法, * 内容都是使用字符串类型即可完成。通过Javassist强大的字节生成能力可以达到动态增加类和实现动态代理的功能*/ public class Client { public static void main(String[] args) throws CannotCompileException, InstantiationException, IllegalAccessException, NotFoundException, UnsupportedEncodingException { // 创建类池,true为使用默认路径 ClassPool classPool = new ClassPool(true); String className = RealSubject.class.getName();//类名 CtClass ctClass = classPool.makeClass(className + "$$JavassistProxy");//代理类类名 // 添加接口,可选 // ctClass.addInterface(classPool.get(RayTestInterface.class.getName())); // 添加超类 ctClass.setSuperclass(classPool.get(RealSubject.class.getName())); // 添加默认构造函数 ctClass.addConstructor(CtNewConstructor.defaultConstructor(ctClass)); // 添加属性 public RealSubject real = new RealSubject(); ctClass.addField(CtField.make("public " + className + " real = new " + className + "();", ctClass)); // 添加方法,里面进行动态代理(增强) public void request(){ real.request();} ctClass.addMethod(CtNewMethod .make("public void request(){" + "System.out.println(123);" + " real.request();" + "System.out.println(456);}",ctClass)); //生成动态类 Class<RealSubject> testClass = ctClass.toClass(); //生成动态类的实例 RealSubject proxy = testClass.newInstance(); //调用代理类方法 proxy.request(); System.out.println(proxy); } }
我们开发中用的较多的Spring框架就同时使用了Jdk动态代理和Cglib动态代理两种方式。
五、代理模式和装饰者模式的区别
代理模式和装饰者模式确实挺像的,但是他们的区别是关注点或者说意图不同。
装饰者模式的意图是对对象进行功能上的增强。
代理模式的意图是给对象提供一个代理对象,让代理对象来控制对原对象的访问。
……更多设计模式的内容,可以访问Refactoring.Guru
总结
1.代理的作用
代理模式给某一个对象提供了一个代理对象,并由代理对象控制对原对象的引用
2.静态代理和动态代理的区别
3.动态代理的实现方式
JDK动态代理、asm/cglib、javassist
1.实现原理区别
JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
CGLIB使用底层字节码技术(asm开源库),把目标类的class文件加载进来,为目标类创建一个子类,并在父类方法中采用方法拦截的技术拦截对父类方法的调用,并顺势织入横切逻辑。
2.各自缺陷
JDK动态代理只能对实现了接口的类进行代理。
CGLIB不能对final类和final方法生成动态代理类。
最后要搞清楚的问题:
1.为什么JDK动态代理只能对实现类了接口的类进行代理,而不能对继承了其它类的类进行代理?
2.通过实验可以发现JDK动态代理生成的代理类会继承自Proxy类,为什么非要继承Proxy类呢?
3.既然CGLIB没有JDK的局限性,为什么就没有取代JDK动态代理?例如Spring AOP的实现中就两者并存。