代理模式
代理模式是很常见的一种设计模式,代理一词拆开来看就是代为受理,那显然是要涉及到请求被代理的委托方,提供代理的代理方,以及想要通过代理来实际联系委托方的客户三个角色。举个生活中常见的例子,房东都是通过中介来处置自己的房屋,并不与租客直接接触,这种场景下,房东本身是委托方,中介是代理方,房东把自己的房屋委托给中介进行房屋出租,这样当租客想要租房的时候,只能通过中介来咨询办理。这样房东自身不用暴露身份,不用和租客费时沟通,带领租客看房等一系列问题,这样都转由中介来解决。当然,中介也可以给多个房东提供服务,这样租客只接触一个中介,就可以看到不同的房源,最终找到符合心意的房间。
通过上面的例子,代理模式有以下两个优点:
- 可以隐藏委托类的实现;
- 可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。
字节码处理
Java 程序员都应该知道,Java 通过 Javac 编译器将.java 源文件编译成能被 JVM 虚拟机识别的.class 字节码文件, 当 .class字节码文件通过 JVM 转为机器可以执行的二进制机器码时,JVM 类加载器首先加载字节码文件,然后通过解释器逐行进行解释执行 ,生成对应的 Class 对象,进而使 Class 对象创建类的具体实例来进行调用实现具体的功能。
上图说明了 Java 加载字节码的流程,但是 Java 的强大在于不仅仅可以加载在编译器生成好的字节码,还可以在运行期系统中,遵循 Java 编译系统组织.class 文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,这样就完成了在代码动态创建一个类的能力,如下图流程。
关于动态生成类的技术目前有两种,一种是自己动手,从零开始创建字节码,理论上可行,实际上很难;第二种是,使用已有的一些能操作字节码的库,帮助我们创建 class。目前,能够操作字节码的常用的工具/库有:
Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库, 接下来我们使用 Javasisst 工具在运行时动态创建字节码并加载类,如下代码:
package com.msdn.classLoad; public class JavasisstLearn { /** * 动态生成一个新类 * @return */ public static Class<?> createNewClass(){ try { //获取ClassPool ClassPool pool = ClassPool.getDefault(); //创建User类 CtClass ctClass = pool.makeClass("com.msdn.bean.User"); //创建User类成员变量name CtField name = new CtField(pool.get("java.lang.String"),"name",ctClass); //设置name为私有 name.setModifiers(Modifier.PRIVATE); //将name写入class,并初始化为空 ctClass.addField(name,CtField.Initializer.constant("")); //增加set方法,名字为“setName” ctClass.addMethod(CtNewMethod.setter("setName",name)); //增加get方法,名称为“getName” ctClass.addMethod(CtNewMethod.getter("getName",name)); //添加无参构造函数 CtConstructor constructor = new CtConstructor(new CtClass[] {},ctClass); constructor.setBody("{name = \"hresh\";}");//相当于public Sclass(){this.name = "hresh";} ctClass.addConstructor(constructor); //添加有参构造函数 constructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")},ctClass); constructor.setBody("{$0.name = $1;}");//第一个传入的形参$1,第二个传入的形参$2,相当于public Sclass(String s){this.name = s;} ctClass.addConstructor(constructor); System.out.println(ctClass.getDeclaredField("name")); //反射调用新创建的类 Class<?> uClass = ctClass.toClass(); Object user = uClass.newInstance(); Method getter = null; getter = user.getClass().getMethod("getName"); System.out.println(getter.invoke(user)); }catch (Exception ex){ ex.printStackTrace(); } return null; } public static void main(String[] args) { createNewClass(); } } 复制代码
这里只是介绍了 Javasisst 工具的部分功能,感兴趣的朋友可以参考这两篇文章:Java动态编程初探——Javassist 和 Javassist中文技术文档
在 Java 学习的过程中,我们多是默认使用静态加载字节码,对于动态加载字节码没有过多接触,但是在某些技术场景下显得尤为必要。关于动态生成类的场景的介绍大家可以参考 Java动态生成类的场景的考察
介绍静态和动态加载字节码的两种方式,是为了引出下面关于两种代理方式的介绍,代理机制通过代理类创建时间的不同分为了静态代理和动态代理:
- 静态代理:代理类在编译阶段生成,程序运行前就已经存在,那么这种代理方式被称为静态代理,这种情况下的代理类通常都是我们在 Java 代码中定义的。
- 动态代理:代理类在程序运行时创建,也就是说,这种情况下,代理类并不是在 Java 代码中定义的,而是在运行时根据我们在 Java 代码中的“提示”动态生成的。
目前,静态代理主要有 AspectJ 静态代理、JDK 静态代理技术 ,而动态 代理有 JDK 动态代理、Cglib 动态代理技术,而 Spring AOP 是整合使用了 JDK 动态代理和 Cglib 动态代理两种技术。
关于 Java 编程的动态性有一系列的文章,有兴趣的朋友可以去阅读一番:Java动态化
静态代理
AspectJ 静态代理
关于 AspectJ 在 IDEA 中的配置,以及简单的案例学习,可以参看AspectJ入门及在IDEA中的配置一文。
将租房抽象为一个 Rent 接口,如下:
public interface Rent { public void rent(); } 复制代码
房东实现了 Rent 接口:
public class Host implements Rent { public void rent() { System.out.println("房屋出租"); } } 复制代码
用 AspectJ 语法实现一个代理 IntermediaryAspectJ:
public aspect IntermediaryAspectJ { //定义切点 pointcut rentPointCut():call(void com.msdn.bean.Host.rent()); /** * 定义前置通知 * befor(参数):连接点函数{ * 函数体 * } */ before():rentPointCut(){ seeHouse(); } /** * 定义后置通知 * after(参数):连接点函数{ * 函数体 * } */ after():rentPointCut(){ fare(); } private void seeHouse(){ System.out.println("带租客看房"); } private void fare(){ System.out.println("收中介费"); } } 复制代码
测试代码如下:
public class AspectJTest { public static void main(String[] args) { Host host = new Host(); host.rent(); } } 复制代码
执行结果为:
带租客看房 房屋出租 收中介费 复制代码
可以看到 Host 的 rent()方法前后输出了我们在 IntermediaryAspectJ 中定义的前置和后置通知,该类充当代理的功能。具体的 AspectJ 语法我们不深究,只需要知道 pointcut 是定义要代理的切入点,这里是定义了一个 pointcut,代理 Host 类中的 rent()方法。而 before()和 after()分别可以定义具体在切入点前后需要的额外操作。
总结一下,AspctJ 就是用特定的编译器和语法,对类实现编译期增强,实现静态代理技术,下面我们看 JDK 静态代理。
JDK静态代理
JDK 静态代理更多的是一种设计模式,JDK 静态代理的代理类和委托类会实现同一接口或是派生自相同的父类,代理类对客户对象是可见的,其结果图如下:
继续套用上面租房的例子,改写代码实现一个 JDK 静态代理模式。
中介也实现 Rent 接口,持有一个房东对象来提供租房服务,包括带领租客看房,解答相关疑问等:
public class Intermediary implements Rent { private Host host; public Intermediary() { } public Intermediary(Host host) { this.host = host; } @Override public void rent() { seeHouse(); host.rent(); fare(); } public void seeHouse(){ System.out.println("带租客看房"); } public void fare(){ System.out.println("收中介费"); } } 复制代码
通过中介来替房东完成租房行为:
public class JdkTest { public static void main(String[] args) { Host host = new Host(); Intermediary proxy = new Intermediary(host); proxy.rent(); } } 复制代码
执行结果同上。
以上就是一个典型的静态代理的实例,很简单但是也能说明问题,我们可以分析一下静态代理的优缺点:
优点:静态代理在编译时产生 class 文件,运行时无需产生,可直接使用,效率好。业务类可以只关注自身逻辑,可以重用,通过代理类来增加通用的逻辑处理;
缺点:静态代理要为每个目标类创建一个代理类,当需要代理的对象太多,那么代理类也变得很多。同时代理类违背了可重复代理只写一次的原则。 另外当接口中增加方法时,所有实现类都需要实现该方法,增加代码维护的复杂度。
关于静态代理存在的缺陷,可以通过动态代理来解决。
动态代理
动态代理中,代理类并不是在 Java 代码中实现,而是在运行时期生成,相比静态代理,动态代理可以很方便的对委托类的方法进行统一处理,如添加方法调用次数、添加日志功能等等,动态代理分为 JDK动态代理和 cglib 动态代理。
JDK动态代理
JDK 提供了动态代理,其原理图如下:
还以中介和房东的模型为例,将租房抽象为一个接口:
public interface Rent { public void rent(); } 复制代码
房东实现了 Rent 接口:
public class Host implements Rent { public void rent() { System.out.println("房屋出租"); } } 复制代码
实现一个代理类的请求处理器,处理对具体类的所有方法的调用:
public class InvocationHandlerImpl implements InvocationHandler { Rent rent; public InvocationHandlerImpl(Rent rent) { this.rent = rent; } @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { seeHouse(); //租房 Object object = method.invoke(rent,objects); fare(); return object; } public void seeHouse(){ System.out.println("带租客看房"); } public void fare(){ System.out.println("收中介费"); } } 复制代码
通过 JDK 动态代理机制实现一个动态代理:
public class JdkProxy { public static void main(String[] args) { //创建被代理的具体类 Host host = new Host(); //获取对应的ClassLoader ClassLoader classLoader = host.getClass().getClassLoader(); //获取被代理对象实现的所有接口 Class[] interfaces = host.getClass().getInterfaces(); //设置请求处理器,处理所有方法调用 InvocationHandler invocationHandler = new InvocationHandlerImpl(host); /** * 5.根据上面提供的信息,创建代理对象 在这个过程中, * a.JDK会通过根据传入的参数信息动态地在内存中创建和.class文件等同的字节码 * b.然后根据相应的字节码转换成对应的class, * c.然后调用newInstance()创建实例 */ Object o = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler); Rent rent = (Rent) o; rent.rent(); } } 复制代码
我们从代理的创建入手,看看 JDK 的动态代理都做了什么。 在 Jdk 的 java.lang.reflect 包下有个 Proxy 类,它正是构造代理类的入口。这个类的结构入下:
从上图发现最后面四个是公有方法。而最后一个方法 newProxyInstance 就是创建代理对象的方法。这个方法的源码如下:
public static Object newProxyInstance(ClassLoader var0, Class<?>[] var1, InvocationHandler var2) throws IllegalArgumentException { Objects.requireNonNull(var2); Class[] var3 = (Class[])var1.clone(); SecurityManager var4 = System.getSecurityManager(); if (var4 != null) { checkProxyAccess(Reflection.getCallerClass(), var0, var3); } Class var5 = getProxyClass0(var0, var3); try { if (var4 != null) { checkNewProxyPermission(Reflection.getCallerClass(), var5); } final Constructor var6 = var5.getConstructor(constructorParams); if (!Modifier.isPublic(var5.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { var6.setAccessible(true); return null; } }); } return var6.newInstance(var2); } catch (InstantiationException | IllegalAccessException var8) { throw new InternalError(var8.toString(), var8); } catch (InvocationTargetException var9) { Throwable var7 = var9.getCause(); if (var7 instanceof RuntimeException) { throw (RuntimeException)var7; } else { throw new InternalError(var7.toString(), var7); } } catch (NoSuchMethodException var10) { throw new InternalError(var10.toString(), var10); } } 复制代码
这个方法需要三个参数:ClassLoader,用于加载代理类的 Loader 类,通常这个 Loader 和被代理的类是同一个 Loader 类。Interfaces,是要被代理的那些那些接口。
InvocationHandler,就是用于执行除了被代理接口中方法之外的用户自定义的操作,它也是用户需要代理的最终目的。用户调用目标方法都被代理到 InvocationHandler 类中定义的唯一方法 invoke 中。
下面还是看看 Proxy 如何产生代理类的过程,它构造出来的代理类到底是什么样子?下面揭晓啦。
具体步骤如下:
- Proxy.newProxyInstance()获取 Host 类的所有接口列表(第二个参数:interfaces);
- 确定要生成的代理类的类名,默认为:com.sun.proxy.$ProxyXXXX;
- 根据需要实现的接口信息,在代码中动态创建该 Proxy 类的字节码;
- 将对应的字节码转换为对应的 class 对象;
- 创建 InvocationHandler 实例 handler,用来处理 Proxy 所有方法调用;
- Proxy 的 class 对象以创建的 handler 对象为参数(第三个参数: invocationHandler ),实例化一个 Proxy 对象。
而对于 InvocationHandler,我们需要重写其 invoke 方法:
public Object invoke(Object proxy, Method method, Object[] args) 复制代码
在调用代理对象中的每一个方法时,在代码内部,都是直接调用了 InvocationHandler 的 invoke 方法,而 invoke 方法根据代理类传递给自己的 method 参数来区分是什么方法。
可以看出,Proxy.newProxyInstance()
方法生成的对象也是实现了 Rent 接口的,所以可以在代码中将其强制转换为 Rent 来使用,和静态代理达到了同样的效果。我们可以用下面代码把生成的代理类的字节码保存到磁盘里,然后反编译看看 JDK 生成的动态代理类的结构。
public class ProxyUtils { public static void main(String[] args) { Host host = new Host(); genereateClassFile(host.getClass(),"HostProxy"); } public static void genereateClassFile(Class clazz,String proxyName){ //根据类信息和提供的代理类名称,生成字节码 byte[] classFile = ProxyGenerator.generateProxyClass(proxyName,clazz.getInterfaces()); String paths = clazz.getResource(".").getPath(); System.out.println(paths); FileOutputStream out = null; try { //保存到硬盘中 out = new FileOutputStream(paths + proxyName + ".class"); out.write(classFile); out.flush(); } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } 复制代码
编译后的 HostProrxy.class 文件结果如下:
//动态代理类HostProrxy实现了Rent接口 public final class HostProxy extends Proxy implements Rent { //加载接口中定义的所有方法 private static Method m1; private static Method m3; private static Method m2; private static Method m0; //构造函数接入InvocationHandler,也就是持有了InvocationHandler对象 public HostProxy(InvocationHandler var1) throws { super(var1); } public final boolean equals(Object var1) throws { try { return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } //自动生成的rent方法,实际调用InvocationHandler对象的invoke方法,传入m3参数对象代表rent()方法 public final void rent() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode() throws { try { return (Integer)super.h.invoke(this, m0, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } //加载接口中定义的所有方法 static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("com.msdn.bean.Rent").getMethod("rent"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } } 复制代码
通过查看编译后的代码可以看出,JDK 生成的动态代理类实现和具体类相同的接口,并持有 InvocationHandler 对象(InvocationHandler对象又持有具体类),调用动态代理类中的方法,会触发传入 InvocationHandler 的 invoke()方法 ,通过 method 参数,来区分调用的是什么具体的方法。
注意:JDK 动态代理会根据被代理对象生成一个继承了 Proxy 类,并实现了该业务接口的 JDK 代理类,该类的字节码会被传进去的 ClassLoader 加载,创建了 JDK 代理对象实例。
JDK 代理对象实例创建时,业务代理对象实例会被赋值给 Proxy 类,JDK 代理对象实例也就有了业务代理对象实例,同时 JDK 代理对象实例通过反射根据被代理类的业务方法创建了相应的 Method 对象m(可能有多个)。当 JDK 代理对象实例调用业务方法,如 proxy.rent();此时会先把对应的m对象作为参数传给 invoke()方法(就是 invoke 方法的第二个参数),调用了 JDK 代理对象实例的 invoke()回调方法,在 invoke 方法里面再通过反射调用被代理对象的方法,即 Object object = method.invoke(rent,objects);
CGLIB动态代理
JDK 中提供的生成动态代理类的机制有个鲜明的特点是:
某个类必须有实现的接口,而生成的代理类也只能代理某个类接口定义的方法,比如:如果上述例子的 Host 类实现了继承自 Rent 接口的方法外,自身另外实现了方法 buyHouse(),则在产生的动态代理类中不会有这个方法。更极端的情况是:如果某个类没有实现接口,那么这个类就不能用 JDK 产生动态代理。
那么怎么解决上述存在的问题呢?这里介绍一种新的代理方法——CGLIB,“CGLIB(Code Generation Library),是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展 Java 类与实现J ava接口。
CGLIB 创建某个类A的动态代理类的模式是:
- 查找A上的所有非 final 的 public 类型的方法定义;
- 将上述步骤中的方法的定义转换成字节码;
- 将组成的字节码转换成相应的代理的 Class 对象;
- 实现 MethodInterceptor 接口,用来处理对代理类上的所有方法的请求(该接口和 JDK 动态代理 InvocationHandler 的功能和角色是一样的)
有了上述 JDK 动态代理的例子,CGLIB 就比较容易理解了,接下里用案例进行分析,复用上述的 Rent 接口和 Host 类。
Maven 导入 CGLIB 相关包,格式如下:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.6</version> </dependency> 复制代码
实现 MethodInterceptor 接口:
public class MethodInterceptorImpl implements MethodInterceptor { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { seeHouse(); Object object = methodProxy.invokeSuper(o,objects); fare(); return object; } public void seeHouse(){ System.out.println("带租客看房"); } public void fare(){ System.out.println("收中介费"); } } 复制代码
创建动态代理:
public class CglibProxyDemo { public static void main(String[] args) { Host host = new Host(); MethodInterceptor methodInterceptor = new MethodInterceptorImpl(); //cglib中加强器,用来创建动态代理 Enhancer enhancer = new Enhancer(); //设置要创建动态代理的类 enhancer.setSuperclass(host.getClass()); //设置回调,这里相当于是对于代理类上所有方法的调用,都会调用Callback,而Callback则需要实行intercept()方法进行拦截 enhancer.setCallback(methodInterceptor); Rent rent = (Rent) enhancer.create(); rent.rent(); } } 复制代码
通过以上实例可以看出,Cglib 通过继承实现动态代理,具体类不需要实现特定的接口,而且代理类可以调用具体类的非接口方法,更加灵活。
关于 JDK 动态代理和 CGLIB动态代理适用范围的不同,需要修改一下 Host 类,然后查看动态代理执行的结果。
Host.java
public class Host implements Rent { public void rent() { System.out.println("房屋出租"); } public void buyHouse(){ System.out.println("买房后续出租"); } public final void travel(){ System.out.println("旅游!!!"); } } 复制代码
JdkProxy.java
public class JdkProxy { public static void main(String[] args) { //创建被代理的具体类 Host host = new Host(); //获取对应的ClassLoader ClassLoader classLoader = host.getClass().getClassLoader(); //获取被代理对象实现的所有接口 Class[] interfaces = host.getClass().getInterfaces(); //设置请求处理器,处理所有方法调用 InvocationHandler invocationHandler = new InvocationHandlerImpl(host); /** * 5.根据上面提供的信息,创建代理对象 在这个过程中, * a.JDK会通过根据传入的参数信息动态地在内存中创建和.class文件等同的字节码 * b.然后根据相应的字节码转换成对应的class, * c.然后调用newInstance()创建实例 */ Object o = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler); Rent rent = (Rent) o; // Rent rent = (Rent) new InvocationHandlerImpl().newInstance(host); rent.rent(); Host host1 = (Host) o; host1.buyHouse(); host1.travel(); } } 复制代码
执行结果为:
带租客看房 房屋出租 收中介费 Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to com.msdn.bean.Host at com.msdn.dynamic.JdkProxy.main(JdkProxy.java:36) 复制代码
CglibProxyDemo.java
public class CglibProxyDemo { public static void main(String[] args) { Host host = new Host(); MethodInterceptor methodInterceptor = new MethodInterceptorImpl(); //cglib中加强器,用来创建动态代理 Enhancer enhancer = new Enhancer(); //设置要创建动态代理的类 enhancer.setSuperclass(host.getClass()); //设置回调,这里相当于是对于代理类上所有方法的调用,都会调用Callback,而Callback则需要实行intercept()方法进行拦截 enhancer.setCallback(methodInterceptor); Rent rent = (Rent) enhancer.create(); rent.rent(); Host host1 = (Host) enhancer.create(); host1.buyHouse(); host1.travel(); } } 复制代码
执行结果为:
带租客看房 房屋出租 收中介费 带租客看房 买房后续出租 收中介费 旅游!!! 复制代码
总结:JDK 动态代理必须实现接口,通过反射来动态生成代理方法,消耗系统性能。Cglib 动态代理无需实现接口,通过生成子类字节码来实现,比反射快一点,没有性能问题。但是由于 Cglib 会继承被代理类,需要重写被代理方法,所以被代理类不能是 final 类,被代理方法不能是 final 标识。因此,Cglib 应用更加广泛一些。