1 为什么要用代理?
当用户希望和某个对象打交道的时候,但是程序可能不希望用户直接访问该对象,而是提供一个特殊的对象,这个特殊的对象就被称为当前要访问对象的代理,在程序中,让用户直接和这个代理对象打交道
2 代理的特点是啥?
代理模式必须要让代理类和目标类实现相同的接口,客户端通过代理类来调用目标方法,代理类会将所有的方法调用分派到目标对象上反射执行,还可以在分派过程中添加"前置通知"和后置处理(如在调用目标方法前校验权限,在调用完目标方法后打印日志等)等功能。特别要注意的是,代理只做效果增强,却不改变结果的类型,如果需要改变返回结果的类型,那么不推荐使用代理模式
3 静态代理
所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了,如下代码,StaticProxy类直接就是Person的实现类
public interface Person { public void eat(String food); public void sleep(String time); }
public class StaticProxy implements Person { private Person person; public StaticProxy(Person person) { this.person = person; } @Override public void eat(String food) { System.out.println("静态代理提供的 吃" + food); } @Override public void sleep(String time) { System.out.println("静态代理提供的 睡" + time+"h"); } }
public class TestStatic { public static void main(String[] args){ Person person =new PersonImpl(); StaticProxy staticProxy=new StaticProxy(person); staticProxy.eat("食物"); staticProxy.sleep("24"); } }
运行结果
4 动态代理(JDK动态代理+Cglib动态代理)
为什么要使用动态代理呢,因为静态代理已经不能满足我们业务的需要了,如果我要代理20个接口怎么办?静态代理就要建20个代理类,但是对于动态代理,只要直接new它的代理实例就可以了,这就是动态代理的好处,在下面的代码中会体现出来
4-1 JDK动态代理
public interface Person { public void eat(String food); public void sleep(String time); }
public class PersonImpl implements Person { @Override public void eat(String food) { System.out.println("JDK动态代理提供的 开始吃:" + food); } @Override public void sleep(String time) { System.out.println("JDK动态代理提供的 睡了:" + time + "h"); } }
创建代理类
a invoke方法在调用指定的具体方法时会自动调用,用于集中处理在动态代理类对象上的方法调用,比如下图中,调用到person.eat("食物")的时候,会调用到invoke(),这里可以对调用的接口做统一的处理,其实jdk的动态代理可以作为Aop的实现方式(当然Aop的实现方式有很多种)
b newProxyInstance()静态方法负责创建动态代理类的实例
public class JDKProxy implements InvocationHandler { private Person person; public JDKProxy(Person person) { this.person = person; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //插入前置通知 System.out.println("invoke method before------" + method + "------------------"); //重点就是这里,关心的并不是这个method.invoke(person,args),如果下面接口的实例做的是同一件事的话,就相当于横切 //也就是Aop的实现,前面干什么,后面干什么 Object result = method.invoke(person, args); //插入后置通知 System.out.println("invoke method after------" + method + "------------------"); return result; } /** * 创建代理 */ public void creatProxy() { //Person.class.getInterfaces(); 获取某个类下面的接口 //这里体现了动态代理的好处,可以代理Person下的所有接口 Person person = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[] {Person.class}, new JDKProxy(new PersonImpl())); //如果要在代理一个接口怎么办?直接在这里实例化一个代理实例就好了 //但是这里有个问题,新接口要做的是同一件事情,这就是Aop的一种实现方式 person.eat("食物"); person.sleep("24"); } }
参数 ClassLoader loader:类加载器 Class<?>[] interfaces:得到全部的接口 InvocationHandler h:得到InvocationHandler接口的子类实例
main方法的调用
public class TestJDK { public static void main(String[] args) { Person person = new PersonImpl(); JDKProxy jdkProxy = new JDKProxy(person); jdkProxy.creatProxy(); } }
运行结果
理解JDK动态代理
看到这里,有的小伙伴就有点不明觉厉了,小编看到这里的时候,会有以下几个问题
1 这个代理对象到底是怎么生成的呢?
http://rejoy.iteye.com/blog/1627405?page=2#comments
网上有篇写的非常好的博客,小编这篇博客里面重点给大家圈一下,大家看源码的时候,可以跟着这个顺序来看
Proxy.newProxyInstance(新增一个代理实例)---> getProxyClass0(loader, intfs)(获取代理的class字节码)---> ProxyClassFactory.apply(如果存在的话,从缓存中获取,否则从代理工厂中获取)---> ProxyGenerator.generateProxyClass(这个才是整个代理最精华的部分,获取代理类的字节码文件)---> var3.generateClassFile(将字节码文件写入内存)
2 这个InvocationHandler 的invoke方法到底是谁在调用?
我们把代理类反编译出来(主要就是用ProxyGenerator.generateProxyClass)
/*** * * 获取代理的class文件,这个方法是为了印证,代理是如何实现的,与是否生成代理无关 * 1 跟踪源码,获取代理的class------Proxy.newProxyInstance(跟进去看) * 2 用文件输出流输出,然后全看反编译之后的class * @param proxyObject */ public static void getProxyClass(Object proxyObject) { Class proxyClass = proxyObject.getClass(); String proxyName=proxyClass.getName(); // 生成代理类的字节码文件,这里是动态代理最精华的部分 // 在源码中,生成字节码之后,会根据字节码去生成对应的代理类实例 byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyClass.getName(), new Class[] {Person.class}); //生成的class String relativeFilePath = "target/classes/com/proxy/"+proxyName+".class"; FileOutputStream outputStream = null; try { outputStream = new FileOutputStream(relativeFilePath); outputStream.write(proxyClassFile); //java在使用流时,都会有一个缓冲区,按一种它认为比较高效的方法来发数据:把要发的数据先放到缓冲区, // 缓冲区放满以后再一次性发过去,而不是分开一次一次地发 //而flush()表示强制将缓冲区中的数据发送出去,不必等到缓冲区满. outputStream.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }
调用
public static void main(String[] args) { Person person=new PersonImpl(); Person2 person2=new Person2Impl(); getProxyClass(person); getProxyClass(person2); }
在指定的路径下查看反编译之后的class文件
小编的路径是
String relativeFilePath = "target/classes/com/proxy/"+proxyName+".class";
大家点开反编译的class文件
public final void eat(String var1) throws { try { //个人理解是,invoke方法是接口在调用,调用到你的接口时,接口调用invoke,如图,接口eat,调用了invoke super.h.invoke(this, m4, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } }
3 为什么jdk的动态代理只能代理接口呢?
网上看了很多的帖子,依然不是很明白,所以还是动手敲一下吧
思路:既然说jdk的动态代理不能代理类,那么我就尝试下代理类看看报什么错误
package com.base; /** * 用person4,来验证,为什么jdk的动态代理只能代理接口 * @author wb-liuhuxiang.a * @version $Id: Person4.java, v 0.1 2017年12月26日 15:05 wb-liuhuxiang.a Exp $ */ public class Person4 { public void say(String word) { System.out.println("say:" + word); } }
public class JDKProxy implements InvocationHandler { private Person4 person4; public JDKProxy(Person4 person4) { this.person4 = person4; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //插入前置通知 System.out.println("invoke method before------" + method + "------------------"); //执行相应的目标方法 Object result = method.invoke(person4, args); //插入后置通知 System.out.println("invoke method after------" + method + "------------------"); return result; } public void creatProxy() { Person4 person4 = (Person4) Proxy.newProxyInstance(Person4.class.getClassLoader(), new Class[] {Person4.class}, this); person4.say("hallo word"); } }
public class TestJDK { public static void main(String[] args) { Person4 person4=new Person4(); JDKProxy jdkProxy=new JDKProxy(person4); jdkProxy.creatProxy(); } }
然后运行结果,duang,果然错了
还是我们上面说的源码,找到
这里是生成代理类的class字节码文件,然后写入硬盘,这里清清楚楚的写明了,jdk的动态代理只能代理接口,自我理解下,在Java中,可以单个继承类,也可以实现多个接口,但是在jdk动态代理设计的时候,可能考量到单继承会有诸多的不便,所以在源码设计的时候,强行的设计成了接口
4-2 Cglib的动态代理
上文中我们可以知道,jdk的动态代理只能用于接口代理,对于类的代理就力不从心了,那么我们就可以用cglib的动态代理来实现类代理,cglib是怎么样实现动态代理的呢,首先通过字节码技术为类创建子类,子类中拦截所有父类的方法(也是AOP的实现),我们依然以上面的person为例,话不多说,直接上代码
public class CglibProxy implements MethodInterceptor { private Enhancer enhancer = new Enhancer(); public Object getProxy(Class clazz) { enhancer.setCallback(this); enhancer.setSuperclass(clazz); return enhancer.create(); } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("前置通知" + method + "----------"); //这里要比较jdk的动态代理,一个是实现InvocationHandler //一个是实现了MethodInterceptor Object result = proxy.invokeSuper(obj, args); System.out.println("后置通知" + method + "----------"); return result; } }
public class Person3 { public void eat(String food) { System.out.println("吃:" + food); } public void play(String basketball) { System.out.println("玩:" + basketball); } }
public class TestCglib { public static void main(String[] args) { CglibProxy cglibProxy=new CglibProxy(); Person3 person3=(Person3) cglibProxy.getProxy(Person3.class); person3.eat("cglibProxy提供的food"); person3.play("cglibProxy提供的basketBall"); } }
运行结果
cglib的包问题:
这两个包都是需要的,如果cglib-nodep-2.2.jar这个包如果少了,那么就会报错
5 总结:动态代理与静态代理的区别
1 静态代理的字节码文件,在程序运行前就已经生成了(代理类和委托关系在程序运行前就确定了) ,动态代理的字节码文件是在程序运行过程中,通过Java的反射机制生成的(代理类和委托关系是在程序运 行中确定的)
2 静态代理,要自己实现处理的结果,动态代理中所有的方法都被转移调用到了处理器的一个集中的地方处理(InvocationHandler.invoke),这样在接口数量较多的时候,可以灵活处理,而不需要像静态代理一样,对每一个方法进行中转(这是动态代理了最大的优点)
对于静态代理来说,如果要新代理一个接口的话,那么它就要新建一个代理类
但是对于动态代理,如果要代理一个新的接口,他只要重新new一个代理的实例就可以了,这样就大大减少的代码,避免了一些多余的代码