杭州人从在杭州本地从杭州的代理商(线下商店)中买联想电脑和直接跑到北京来联想总部买电脑。最终的主体业务目标有什么区别吗?基本上是一样的。但是,从代理商那里买就省去了直接跑北京去买电脑的成本和时间了。
软件开发中也经常用到代理。程序中要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如:异常处理,日志,计算方法的运行时间,事务管理,权限管理等等。
客户端可以直接调用Target目标类,但是无法做到在程序之前处理相似的系统功能。所以客户端调用Proxy代理类,在Proxy代理类调用目标类的方法中间加入相似的系统功能就很方便。假如要统计一个类的某个方法的运行时间,这个类的源代码没有给你,你该怎么做?
编写一个与目标类具有相同的接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法的时候附加上系统功能代码。
public class Hello { public void sayHello(){ System.out.println("Hello Java!"); } } 复制代码
public class HelloProxy { private Hello hello = new Hello(); /** * 统计sayHello方法所用的时间 */ public void sayHello(){ //前置功能代码 long startTime = System.currentTimeMillis(); //调用目标方法 hello.sayHello(); //后置功能代码 long endTime = System.currentTimeMillis(); System.out.println("Hello类的sayHello方法花费了:" + (endTime - startTime) + "毫秒"); } } 复制代码
这样写的缺点显而易见,要为系统的各种接口的类增加代理功能,那将需要太多的代理类,全部采用静态代理方式 ,将是一件非常麻烦的事情,要写成百上千的代理类。使用静态代理实现的方式来增加系统功能是不可取的。
JVM虚拟机可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理类,即动态代理类。
JVM生成的动态类必须实现一个或多个接口,所以,JVM生成的动态类只能用作具有相同接口的目标代理类。问题来了,为什么JVM生成的动态类必须实现一个或多个接口呢?要想JVM生成一个代理类,那JVM必须需要知道什么呢?
下面就用程序实现创建动态类的实例对象及调用其方法:
要实现JavaJVM的动态代理技术,就要使用到java.lang.reflect.Proxy这个类。
/** * 动态生成类的字节码,并打印生成类的构造函数 */ public static void getConStructors(){ //动态生成代理类 //ClassLoader: 每一个Class就必须有一个类加载器加载进来的,比如每个人都有一个妈妈。既然需要JVM动态生成Java类,所以要为动态生成类的字节码指定类加载器 //Class Interfaces: 动态生成的字节码实现了哪些接口 Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class); //获取这个代理类的构造方法 Constructor[] constructors = clazzProxy1.getConstructors(); System.out.println("---------------------begin Construstors-----------------"); //遍历构造方法 for (Constructor constructor: constructors) { //获取每个名称 String name = constructor.getName(); StringBuilder sb = new StringBuilder(name); sb.append("("); //获取每个构造方法的参数类型 Class[] clazzTypes = constructor.getParameterTypes(); for (Class clazzType : clazzTypes) { sb.append(clazzType.getName()).append("."); } if(clazzTypes != null && clazzTypes.length != 0){ sb.deleteCharAt(sb.length() - 1); } sb.append(")"); System.out.println(sb.toString()); } } 复制代码
通过打印构造方法,得到的动态代理类只有一个带参数的构造方法,而且是一个InvocationHandler参数,这个参数放入后面的内容讲解。
要通过Proxy类来动态生成代理类,就必须要传入两个参数 - 动态生成类的字节码指定类加载器 - 动态生成的字节码实现了哪些接口
/** * 动态生成类的字节码,并打印动态类的每个方法 */ public static void getMethods(){ //动态生成代理类 Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class); //获取这个代理类的构造方法 Method[] methods = clazzProxy1.getMethods(); System.out.println("---------------------begin Construstors-----------------"); //遍历构造方法 for (Method method: methods) { //获取每个名称 String name = method.getName(); StringBuilder sb = new StringBuilder(name); sb.append("("); //获取每个构造方法的参数类型 Class[] clazzTypes = method.getParameterTypes(); for (Class clazzType : clazzTypes) { sb.append(clazzType.getName()).append("."); } if(clazzTypes != null && clazzTypes.length != 0){ sb.deleteCharAt(sb.length() - 1); } sb.append(")"); System.out.println(sb.toString()); } } 复制代码
通过打印结果可知,动态类生成的每个方法都有Collection接口的每个方法和Object类的每个方法。
上面内容通过打印构造方法,得到的动态代理类只有一个带参数的构造方法,这个构造方法就是java.lang.reflect.InvocationHandler接口,InvocationHandler是一个接口,所以我们要手动写自己的实现类来实现这个接口。先不用管实现这个接口具体是做什么的。
/** * 创建动态类的实例对象及调用其方法 * @param args * @throws Exception */ public static void main(String[] args) throws Exception{ //通过打印构造方法,得到的动态代理类有一个InvocationHandler参数 Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class); //获取Constructor类 Constructor constructor = clazzProxy1.getConstructor(InvocationHandler.class); //传递InvocationHandler参数,手动实现InvocationHander接口 //返回的结果是Collection接口的对象 Collection proxy1 = (Collection) constructor.newInstance(new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; } }); /** * 通过打印生成的对象发现结果为null 有两种种可能: * 第一种可能是对象为null * 第二种可能是对象的toString()方法为null */ System.out.println(proxy1); //对象没有报空指针异常,所以对象的toString为null,可以得出结论,代理类对象的toString()方法被代理类重写了。 System.out.println(proxy1.toString()); //调用一个方法,运行成功,所以proxy1不为null proxy1.clear(); //调用size方法出错,为什么出错呢?size方法是有返回值的。 proxy1.size(); } 复制代码
通过打印结果可知,说明动态代理类生成的对象proxy1不为null,而且proxy1对象可以调用没有返回值的方法,不能调用有返回值的方法。调用有返回值的方法会出现异常。为什么会出现异常,将会在后面讲解。
java.lang.reflect.Proxy 类还为我们直接提供创建出代理对象的方式,就是调用Proxy.newProxyInstance方法。就省去了先获取动态类的Class对象,再通过Class对象获取动态类的对象的过程了。
public static void getMyInstance(){ //Proxy.newInstance方法直接创建出代理对象 Collection proxy1 = (Collection) Proxy.newProxyInstance( Collection.class.getClassLoader(), new Class[]{Collection.class}, new InvocationHandler() { //方法外部指定目标 List target = new ArrayList<>(); @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在调用代码之前加入系统功能代码 long startTime = System.currentTimeMillis(); //睡眠1秒钟 Thread.sleep(1000); //目标方法 Object retVal = method.invoke(target, args); //在调用代码之后加入系统功能代码 long endTime = System.currentTimeMillis(); System.out.println( method.getName() + "方法花费了:" + (endTime - startTime) + "毫秒"); return retVal; } }); proxy1.add("a"); proxy1.add("b"); proxy1.add("c"); //3 System.out.println(proxy1.size()); } 复制代码
通过打印结果可知,其实每次调用代理对象的每个方法,都会调用InvocationHandler的invoke方法。那为什么会每次调用代理对象的每个方法,会调用我们实现的InvocationHandler实现类的invoke方法呢?它的内部结构是怎么样的呢。现在就模拟一下调用流程。否则知其然就不知其所以然了。构造方法接受了InvocationHandler对象是为了干什么,内部的实现方式是什么样子的。我们了解,只要构造方法只要接受参数对象,就是是为了以后要一定要用到这个参数对象。
import java.lang.reflect.Method; // 模拟MyInvocationHandler对象 interface MyInvocationHandler { public Object invoke(Object target,Method method,Object[] args); } 复制代码
/** * 模拟JVM生成的代理类的内部大致结构 * 并介绍之前使用的size()和add()方法 * 因为实现了Connection接口,所以生成的代理类的方法就是Connection接口的方法 */ public class Proxy1 { //自定义的MyInvocationHandler接口接受三个对象 MyInvocationHandler handler; //代理类的内部方法 //每调用一下,invoke方法就执行一次 Object size() throws Exception{ //内部调用InvocationHandler的invoke方法,而InvocationHandler是一个接口,所以 handler.invoke()方法是调用了我们的实现类的invoke()方法。 //proxy对象,size方法,参数 return handler.invoke(this,this.getClass().getMethod("size", null), null); } //每调用一下,invoke方法就执行一次 Object add(Object args) throws Exception{ //内部调用InvocationHandler的invoke方法 //proxy对象,size方法,参数 return handler.invoke(this,this.getClass().getMethod("size", null), new Object[]{args}); } } 复制代码
通过上面模拟的代码和调用流程图就不难知道为什么会每次调用代理对象的每个方法,会调用我们实现的InvocationHandler实现类的invoke方法。还有为什么调用有返回值的方法会出现异常也不难而知了,因为invoke方法如果不做处理直接返回null的话,就会出现异常。
1. 生成的类中有你那些方法,通过让其实现的接口告诉类加载器。 2. 产生的类中的字节码必须有一个关联类加载器对象。 3. 生成的类中的方法的代码是怎么样的,也由我们提供。把我们的代码写在一个约定好的接口对象的方法中,把对象传给它,它调用我的方法,即相当于插入了我们的代码。提供执行代码的对象就是那个InvocationHandler对象,它是在创建动态类实例对象的构造方法中传递进去的,在上面的InvocationHandler对象的Invoke方法加一点代码,就可以看到这些代码被调用运行了。 复制代码
如果有一个目标类,这个目标类本身没有实现接口。那通过什么样的方式来生成代理类呢?那JVM就无法生成代理类了。CGLIB可以动态生成一个类的子类,一个类的子类也可以用作该类的代理,所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。
通过上面代理类的介绍,代理类的各个方法中通常除了要调用目标的相应方法和对外返回目标的返回结果外,还可以在代理方法中如下四个位置加上系统功能代码: - 在调用目标方法之前 - 在调用目标方法之后 - 在调用目标方法的前后 - 在处理目标方法异常的catch块中
介绍完动态代理的原理之后,我们思考一下上面的代码设计有非常明显的不足,我们自定义拦截逻辑的系统功能和目标对象被硬编码写死在代理对象中。我们要把代码以参数对象的形式封装传递进来。以参数对象的形式传递进来的好处是,在系统运行的时候,可以临时的灵活加入系统功能,实现系统功能灵活解耦。要为InvocationHandler传递两个对象: - 目标对象target - 系统功能对象
import java.lang.reflect.Method; /** * 实现系统功能接口 * @author * */ public interface Advice { void beforeMethod(Method method); void afterMethod(Method method); } 复制代码
import java.lang.reflect.Method; /** * 自定义系统功能的实现类 * @author tianshuo * */ public class MyAdvice implements Advice { @Override public void beforeMethod(Method method) { System.out.println("在目标方法之前调用!"); } @Override public void afterMethod(Method method) { System.out.println("在目标方法之后调用!"); } } 复制代码
/** * 使用传递参数的方式灵活创建代理对象 * @param target:目标对象 * @param advice:系统功能对象 * @return Proxy Object:代理对象 */ public static Object getProxy(final Object target,final Advice advice){ //Proxy.newInstance方法直接创建出代理对象 Object proxy3 = Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { advice.beforeMethod(method); Object retVal = method.invoke(target, args); advice.afterMethod(method); return retVal; } }); return proxy3; } 复制代码
/** * 调用代理对象 * @param args * @throws Exception */ public static void main(String[] args) throws Exception{ List target = new ArrayList<>(); List proxyObject = (List) getProxy(target, new MyAdvice()); proxyObject.add("abc"); System.out.println(proxyObject.size()); } 复制代码
通过打印结果可知,说明我们实现传入参数方式来灵活实现代理功能的方式是正确的。