JDK动态代理

来源:互联网 发布:农业财政支出数据 编辑:程序博客网 时间:2024/05/21 11:09

一.用代理模式实现代理(非动态代理) 


要看清楚什么是动态代理的,首先我们来看一下静态代理的做法。无论是那种代理方式,都 
存在代理对象和目标对象两个模型,所谓目标对象就是我们要生成的代理对象所代理的那个 
对象。 
(1.) 包装的模式进行静态代理: 
接口:
Java代码  收藏代码
  1. Animal  
  2. public interface Animal {  
  3. void eat(String food);  
  4. String type();  
  5. }  
  6. 实现类:Monkey  
  7. public class Monkey implements Animal {  
  8. @Override  
  9. public String type() {  
  10. String type = "哺乳动物";  
  11. System.out.println(type);  
  12. return type;  
  13. }  
  14. @Override  
  15. public void eat(String food) {  
  16. System.out.println("The food is " + food + " !");  
  17. }  
  18. }  

包装类:
Java代码  收藏代码
  1. AnimalWrapper  
  2. public class AnimalWrapper implements Animal {  
  3. private Animal animal;  
  4. // 使用构造方法包装Animal的接口,这样所有的Animal实现类都可以被这个Wrapper  
  5. 包装。  
  6. public AnimalWrapper(Animal animal) {  
  7. this.animal = animal;  
  8. }  
  9. @Override  
  10. public void eat(String food) {  
  11. System.out.println("+++Wrapped Before!+++");  
  12. animal.eat(food);  
  13. System.out.println("+++Wrapped After!+++");  
  14. }  
  15. @Override  
  16. public String type() {  
  17. System.out.println("---Wrapped Before!---");  
  18. String type = animal.type();  
  19. System.out.println("---Wrapped After!---");  
  20. return type;  
  21. }  
  22. }  

运行程序: 
Java代码  收藏代码
  1. AnimalWrapper aw = new AnimalWrapper(new Monkey());  
  2. aw.eat("香蕉");  
  3. aw.type();  

控制台输出如下语句: 
+++Wrapped Before!+++ 
The food is 香蕉 ! 
+++Wrapped After!+++ 
---Wrapped Before!--- 
哺乳动物 
---Wrapped After!--- 
这里我们完成了对Animal 所有子类的代理,在代理方法中,你可以加入一些自己的额外的 
处理逻辑,就像上面的+++、---输出语句一样。那么Spring的前置、后置、环绕方法通知, 
通过这种方式可以有限的模拟出来,以Spring 的声明式事务为例,无非就是在调用包装的 
目标方法之前处开启事务,在之后提交事务,这样原有的业务逻辑没有受到任何事务管理代 
码的侵入。 
这种方式的静态代理,缺点就是当Animal 接口中增加了新的方法,那么包装类中也必须增 
加这些新的方法。 
(2.) 继承的模式进行静态代理: 
继承类:
Java代码  收藏代码
  1. MyMonkey  
  2. public class MyMonkey extends Monkey {  
  3. @Override  
  4. public void eat(String food) {  
  5. System.out.println("+++Wrapped Before!+++");  
  6. super.eat(food);  
  7. System.out.println("+++Wrapped After!+++");  
  8. }  
  9. @Override  
  10. public String type() {  
  11. System.out.println("---Wrapped Before!---");  
  12. String type = super.type();  
  13. System.out.println("---Wrapped After!---");  
  14. return type;  
  15. }  
  16. }  

这个例子很容易看懂,我们采用继承的方式对MyMonkey 中的方法进行代理,运行效果与 
包装的模式效果是一样的。 
但这种方式的缺点更明显,那就是不能实现对Animal 所有子类的代理,与包装的模式相比, 
大大缩小了代理范围 

二.动态代理 
   1.基于Proxy的动态代理: 
JAVA 自带的动态代理是基于java.lang.reflect.Proxy、java.lang.reflect.InvocationHandler 两个 
类来完成的,使用JAVA 反射机制。 
Proxy类中的几个方法都是静态的,通常,你可以使用如下两种模式创建代理对象: 
① 
Object proxy = Proxy.newProxyInstance(定义代理对象的类加载器, 
要代理的目标对象的归属接口数组,回调接口InvocationHandler); 
② 
Class proxyClass=Proxy.getProxyClass(定义代理对象的类加载器, 
要代理的目标对象的归属接口数组); 
Object proxy = proxyClass.getConstructor( 
new Class[] { InvocationHandler.class }).newInstance( 
回调接口InvocationHandler); 
第一种方式更加直接简便,并且隐藏了代理$Proxy0 对象的结构。 
JDK 的动态代理会动态的创建一个$Proxy0的类,这个类继承了Proxy并且实现了要代理的 
目标对象的接口,但你不要试图在JDK 中查找这个类,因为它是动态生成的。$Proxy0 的结 
构大致如下所示: 
Java代码  收藏代码
  1. public final class $Proxy0 extends Proxy implements 目标对象的接口1,接口2,…{  
  2. //构造方法  
  3. Public $Proxy0(InvocationHandler h){  
  4. … …  
  5. }  
  6. }  

从上面的类结构,你就可以理解为什么第二种创建代理对象的方法为什么要那么写了。 
下面我们看一个具体的实例: 
接口1:
Java代码  收藏代码
  1. Mammal(哺乳动物)  
  2. public interface Mammal {  
  3. void eat(String food);  
  4. String type();  
  5. }  
  6. 接口2:Primate(灵长类动物)  
  7. public interface Primate {  
  8. void think();  
  9. }  
  10. 实现类:Monkey  
  11. public class Monkey implements Mammal, Primate {  
  12. @Override  
  13. public String type() {  
  14. String type = "哺乳动物";  
  15. System.out.println(type);  
  16. return type;  
  17. }  
  18. @Override  
  19. public void eat(String food) {  
  20. System.out.println("The food is " + food + " !");  
  21. }  
  22. @Override  
  23. public void think() {  
  24. System.out.println("思考!");  
  25. }  
  26. }  
  27. 回调类:MyInvocationHandler  
  28. public class MyInvocationHandler implements InvocationHandler {  
  29. private Object obj;  
  30. public MyInvocationHandler(Object obj) {  
  31. this.obj = obj;  
  32. }  
  33. @Override  
  34. public Object invoke(Object proxy, Method method, Object[] args)  
  35. throws Throwable {  
  36. System.out.println("Invoke method Before!");  
  37. Object returnObject = method.invoke(obj, args);  
  38. System.out.println("Invoke method After!");  
  39. return returnObject;  
  40. }  
  41. }  

注意:这里我们使用构造方法将要代理的目标对象传入回调接口,当然你也可以用其他的方 
式,但无论如何,一个代理对象应该是与一个回调接口对应的。 
运行程序: 
Java代码  收藏代码
  1. // 第一种创建动态代理的方法  
  2. // Object proxy = Proxy.newProxyInstance(Monkey.class.getClassLoader(),  
  3. // Monkey.class.getInterfaces(), new MyInvocationHandler(  
  4. // new Monkey()));  
  5. // 第二种创建动态代理的方法  
  6. Class<?> proxyClass = Proxy.getProxyClass(  
  7. Monkey.class.getClassLoader(),  
  8. Monkey.class.getInterfaces());  
  9. Object proxy = proxyClass.getConstructor(  
  10. new Class[] { InvocationHandler.class }).newInstance(  
  11. new MyInvocationHandler(new Monkey()));  
  12. Mammal mammal = (Mammal) proxy;  
  13. mammal.eat("香蕉");  
  14. mammal.type();  
  15. Primate primate = (Primate) proxy;  
  16. primate.think();  

控制台输出: 
Invoke method Before! 
The food is 香蕉 ! 
Invoke method After! 
Invoke method Before! 
哺乳动物 
Invoke method After! 
Invoke method Before! 
思考! 
Invoke method After! 
你可以看到动态代理成功了,在目标对象的方法调用前后都输出了我们打印的语句。其实 
Spring 中对接口的动态代理,进而做诸如声明式事务的AOP 操作也是如此,只不过代码会 
更加复杂。 
我们用下面的图说明上面的执行过程: 
我们看到目标对象的方法调用被Proxy拦截,在InvocationHandler 中的回调方法中通过反射 
调用。这种动态代理的方式实现了对类的方法的运行时修改。 
JDK 的动态代理有个缺点,那就是不能对类进行代理,只能对接口进行代理,想象一下我 
们的Monkey如果没有实现任何接口,那么将无法使用这种方式进行动态代理(实际上是因 
为$Proxy0 这个类继承了Proxy,JAVA 的继承不允许出现多个父类)。但准确的说这个问题 
不应该是缺点,因为良好的系统,每一个类都是应该有一个接口的。 
从上面知道$Proxy0 是动态代理对象的所属类型,但由于这个类型根本不存在,我们如何鉴 
别一个对象是一个普通的对象还是动态代理对象呢?Proxy类中提供了isProxyClass(Class c) 
方法鉴别与此。 
下面我们介绍一下InvocationHandler 这个接口,它只有一个方法invoke()需要实现,这个方 
法会在目标对象的方法调用的时候被激活,你可以在这里控制目标对象的方法的调用,在调 
用前后插入一些其他操作(譬如:鉴权、日志、事务管理等)。Invoke()方法的后两个参数很 
好理解,一个是调用的方法的Method对象,另一个是方法的参数,第一个参数有些需要注 
意的地方,这个proxy 参数就是我们使用Proxy 的静态方法创建的动态代理对象,也就是 
$Proxy0的实例(这点你可以在Eclipse的断点调试中看到proxy的所属类型确实是$Proxy0)。 
由于$Proxy0 在JDK 中不是静态存在的,因此你不可以把第一个参数Object proxy强制转换 
为$Proxy0 类型,因为你根本就无法从Classpath 中导入$Proxy0。那么我们可以把proxy 转 
为目标对象的接口吗?因为$Proxy0 是实现了目标对象的所有的接口的,答案是可以的。但 
实际上这样做的意义不大,因为你会发现转换为目标对象的接口之后,你调用接口中的任何 
一个方法,都会导致invoke()的调用陷入死循环而导致堆栈溢出。如下所示: 
Java代码  收藏代码
  1. public Object invoke(Object proxy, Method method, Object[] args)  
  2. throws Throwable {  
  3. Mammal mammal=(Mammal)proxy;  
  4. mammal.type();  
  5. … …  
  6. }  

这是因为目标对象的大部分的方法都被代理了,你在invoke()通过代理对象转换之后的接口 
调用目标对象的方法,依然是走的代理对象,也就是说当mammal.type()方法被激活时会立 
即导致invoke()的调用,然后再次调用mammal.type()方法,… …从而使方法调用进入死循 
环,就像无尽的递归调用。 
那么invoke()方法的第一个参数到底干什么用的呢?其实一般情况下这个参数都用不到,除 
请求 代理对象 
Proxy 
InvocationHandler 目标对象 
非你想获得代理对象的类信息描述,因为它的getClass()方法的调用不会陷入死循环。如下 
所示: 
Java代码  收藏代码
  1. Class<?> c = proxy.getClass();  
  2. Method[] methods = c.getDeclaredMethods();  
  3. for (Method m : methods) {  
  4. System.out.println(m.getName());  
  5. }  

这里我们可以获得代理对象的所有的方法的名字,你会看到控制台输出如下信息: 
eat 
think 
type 
equals 
toString 
hashCode 
我们看到proxy确实动态的把目标对象的所有的接口中的方法都集中到了自己的身上。 
这里还要注意一个问题,那就是从Object身上继承的方法hashCode()等的调用也会导致陷入 
死循环,为什么getClass()不会呢?因为getClass()方法是final的,不可以被覆盖,所以也就 
不会被Proxy代理。但不要认为Proxy不可以对final的方法进行动态代理,因为Proxy面向 
的是Monkey的接口,而不是Monkey本身,所以即便是Monkey在实现Mammal、Primate 
接口的时候,把方法都变为final的,也不会影响到Proxy的动态代理。 

2.基于CGLIB的动态代理: 
CGLIB 是一个开源的动态代理框架,它的出现补充了JDK 自带的Proxy 不能对类实现动态 
代理的问题。CGLIB是如何突破限制,对类也能动态代理的呢?这是因为CGLIB内部使用 
了另一个字节码框架ASM,类似的字节码框架还有Javassist、BCEL等,但ASM被认为是 
性能最好的一个。但这类字节码框架要求你对JAVA 的Class 文件的结构、指令集都比较了 
解,CGLIB 对外屏蔽了这些细节问题。由于CGLIB 使用ASM 直接操作字节码,因此效率 
要比Proxy高,但这里所说的效率是指代理对象的性能,在创建代理对象时,Proxy是要比 
CGLIB效率高的。 
下面我们简单看一个CGLIB完成动态代理的例子。 
目标类:
Java代码  收藏代码
  1. Monkey  
  2. public class Monkey {  
  3. public String type() {  
  4. String type = "哺乳动物";  
  5. System.out.println(type);  
  6. return type;  
  7. }  
  8. public final void eat(String food) {  
  9. System.out.println("The food is " + food + " !");  
  10. }  
  11. public void think() {  
  12. System.out.println("思考!");  
  13. }  
  14. }  

我们看到这个Monkey 类有两点变化,第一点是没有实现任何接口,第二点是eat()方法是 
final的。 
回调接口: 
Java代码  收藏代码
  1. import java.lang.reflect.Method;  
  2. import net.sf.cglib.proxy.MethodInterceptor;  
  3. import net.sf.cglib.proxy.MethodProxy;  
  4. public class MyMethodInterceptor implements MethodInterceptor {  
  5. @Override  
  6. public Object intercept(Object obj, Method method, Object[] args,  
  7. MethodProxy proxy) throws Throwable {  
  8. System.out.println("******************");  
  9. Object o = proxy.invokeSuper(obj, args);  
  10. System.out.println("++++++++++++++++++");  
  11. return o;  
  12. }  
  13. }  

运行程序: 
Java代码  收藏代码
  1. import net.sf.cglib.proxy.Enhancer;  
  2. public class Cglib {  
  3. public static void main(String[] args) {  
  4. Monkey monkey = (Monkey) Enhancer.create(Monkey.class,  
  5. new MyMethodInterceptor());  
  6. monkey.eat("香蕉");  
  7. monkey.type();  
  8. monkey.think();  
  9. }  
  10. }  

控制台输出: 
The food is 香蕉 ! 
****************** 
哺乳动物 
++++++++++++++++++ 
****************** 
思考! 
++++++++++++++++++ 
你会发现eat()方法没有被代理,因为在它的前后没有输出MethodInterceptor 中的打印语句。 
这是因为CGLIB 动态代理的原理是使用ASM 动态生成目标对象的子类,final 方法不能被 
子类覆盖,自然也就不能被动态代理,这也是CGLIB的一个缺点。 
我们看到CGLIB进行动态代理的编写过程与Proxy没什么太大的不同,Enhancer 是CGLIB 
的入口,通过它创建代理对象,同时为代理对象分配一个net.sf.cglib.proxy.Callback 回调接 
口,用于执行回调。我们常用的是MethodInterceptor 接口,这个接口继承自Callback接口, 
用于执行方法拦截。 
MethodInterceptor 接口中的intercept()方法中的参数分别为代理对象、被调用的方法的 
Method对象,方法的参数、CGLIB提供的方法代理对象,一般来说调用目标方法时我们使 
用最后一个参数,而不是JAVA 反射的第二个参数,因为CGLIB使用ASM的字节码操作, 
代理对象的执行效率比反射机制更高。
原创粉丝点击