Proxy(代理模式)

在23种设计模式中,个人觉得最难理解的就是代理模式了,其中的动态代理更是难理解,不过不要担心,我会用很简单的描述语句和动态代理的原理来解释代理模式。

一、静态代理

坦克大战的小游戏应该都玩过,我想在坦克移动的时候做点事情,比如记录日志,还要记录坦克的移动的时间,此时坦克类就是目标类,记录坦克移动的日志类和记录坦克的移动的时间类都是代理类。下面看代码。

public class Tank implements Movable { ​ /** * 模拟坦克移动了一段时间 */ @Override public void move() { System.out.println("Tank moving claclacla..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } } ​ public static void main(String[] args) { new TankLogProxy( new TankTimeProxy( new Tank() ) ).move(); } } ​ class TankTimeProxy implements Movable { ​ private Movable movable; ​ TankTimeProxy(Movable movable) { this.movable = movable; } ​ @Override public void move() { long start = System.currentTimeMillis(); movable.move(); long end = System.currentTimeMillis(); System.out.println(end - start); } } ​ class TankLogProxy implements Movable { ​ private Movable movable; ​ TankLogProxy(Movable movable) { this.movable = movable; } ​ @Override public void move() { System.out.println("tank start"); movable.move(); System.out.println("tank stop"); } } ​ interface Movable { void move(); }

上面这种写法虽然可以做到给坦克可以一直扩展代理类,但是只能做到给坦克一个类扩展,比如这时候我又来了个飞机类,飞机类也需要记录日志和移动的时间,此时静态代理就不行了,需要用到动态代理。

UML图:

二、动态代理

动态代理分为jdk的动态代理和cglib的动态代理,不过他们两个的底层用的都是asm字节码操作框架实现的,动态代理意思就是在运行时动态的在内存中生成一份字节码并将其对应的实例返回,通过asm来生成的。

jdk的动态代理

jdk的动态代理,学过jdk的动态代理api使用就应该都能写出下面的代码,Proxy.newInstance(...)会返回一个代理对象,调用代理对象的目标方法时就会调用InvocationHandler的invoke(),为什么呢?下面会说到。public class Tank implements Movable { ​ /** * 模拟坦克移动了一段时间 */ @Override public void move() { System.out.println("Tank moving claclacla..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } } ​ public static void main(String[] args) { //在运行的时候动态的通过asm加载字节码并创建代理对象 //param1:哪个类加载器把代理类load到内存 //param2:代理类要实现的接口 //param3:代理行为:被代理对象调用方法时需要做什么处理 Movable movable = (Movable) Proxy.newProxyInstance(Tank.class.getClassLoader(), //相当于前面的TankLogProxy代理类要实现Movable接口 Tank.class.getInterfaces(), new TankLogHandlder(new Tank())); movable.move(); } } ​ class TankLogHandlder implements InvocationHandler { ​ Movable movable; ​ TankLogHandlder(Movable movable) { this.movable = movable; } ​ public void before() { System.out.println("Tank move"); } ​ public void after() { System.out.println("Tank stop"); } ​ //param1:代理对象自己 //param2:目标对象的目标方法 //param3:目标方法的参数 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object o = method.invoke(movable, args); after(); return o; } } ​ interface Movable { void move(); }动态代理是在运行的时候通过asm动态的在内存中生成一份字节码,既然在内存中,我们就无法看到其源码,可以通过下面的代码将动态产生的字节码保存在项目根路径下,通过反编译可以看到其源码,也就是代理对象对应的类源码System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");在实例化代理类时,会调用父类(Proxy)构造,并将InvocationHandler的实例(之前程序传递进去的InvocationHandler的实例)传递过去,在move()中,调用了父类的成员变量h,也就是InvocationHandler的实例的invoke方法,并将自身(代理对象)、m3(通过反射来获取的目标方法)、参数(目标参数没有方法所以为null)。所以在我们之前动态代理的程序中,调用代理对象的move()时,为什么会调用InvocationHandler的invoke(),这里的源码一下就能看清楚final class $Proxy0 extends Proxy implements Movable { private static Method m1; private static Method m3; private static Method m2; private static Method m0; ​ public $Proxy0(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); } } ​ public final void move() 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 { //m0、m1、m2都是Object类的方法 m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); //通过反射获取对应的方法对象 m3 = Class.forName("com.ahead.proxy.v9.Movable").getMethod("move"); 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()); } } }

cglib动态代理

jdk的动态代理是通过接口来实现的动态代理,cglib是通过继承来实现动态代理的,调用代理类的move(),会去调用MethodInterceptor的intercept(),实际上底层都是基于asm来实现的使用cglib时要引入对应的pom依赖<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.12</version> </dependency>

public class Main { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Tank.class); //TimeMethodInterceptor作用相当于jdk动态代理里面的InvocationHandler enhancer.setCallback(new TimeMethodInterceptor()); //返回代理类 Tank tank = (Tank) enhancer.create(); tank.move(); } } ​ class TimeMethodInterceptor implements MethodInterceptor { ​ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { ​ //父类是Tank System.out.println(o.getClass().getSuperclass().getName()); System.out.println("before"); Object result = null; //执行父类的方法 result = methodProxy.invokeSuper(o, objects); System.out.println("after"); return result; } } ​ class Tank { public void move() { System.out.println("Tank moving claclacla..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } } }

ASM

前面一直在说不管是jdk的动态代理或者cglib的动态代理都是基于ASM来实现的,这里就写个Demo模拟下ASM是怎么动态生成字节码的,这里被访问者的代码是不会变的,被访问者拥有Tank.class字节码,然后自定义的访问者,访问者的visitMethod()的里面可以通过ASM修改被访问者中的Tank.class字节码,这里的我写的就是给Tank.class字节码中的所有方法执行之前加上TimeProxy.before(),最后我将访问者修改后的Tank.class字节码保存了下来在Spring的AOP中,使用了jdk的动态代理和cglib动态代理,如果有接口,Spring的AOP就会使用jdk的动态代理,如果没有接口,就会使用cglib的动态代理,不管哪个动态代理底层都是使用ASM来操作的。通知类不就相当于这里的TimeProxy吗,目标类就是Tank类,最终通过ASM给目标类动态的把通知类中的代码切入到目标类中形成一个代理类(这里保存到磁盘上的那个字节码反编译后的类),将代理类的实例返回给调用者,Spring底层也是相当于下面这样做的

注:这里需要掌握访问者模式(visitor)才能看懂,访问者模式简单来说就是被访问者的代码不变,访问者访问被访问者时,怎么处理由访问者来处理,被访问者只要传递自己所拥有的东西给访问者就行

import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; ​ import java.io.File; import java.io.FileOutputStream; ​ import static org.objectweb.asm.Opcodes.ASM4; import static org.objectweb.asm.Opcodes.INVOKESTATIC; ​ public class ClassTransformerTest { public static void main(String[] args) throws Exception { //被访问者(将Tank的字节码文件读取进来) ClassReader cr = new ClassReader(ClassPrinter.class.getClassLoader(). getResourceAsStream("com/ahead/ASM/Tank.class")); //访问者:里面会有关于字节码所有的visitXXX方法 ClassWriter cw = new ClassWriter(0); ​ /** * 自定义访问者 * jdk的动态代理就是这样做的 * Spring的AOP通过cglib和jdk的动态代理实现的,而这两者底层都是通过asm来操作的 * 因此Spring的AOP原理就是这样 */ ClassVisitor cv = new ClassVisitor(ASM4, cw) { ​ //运行时动态的给Tank.class字节码文件中添加了方法 @Override public MethodVisitor visitMethod(int access, String name, String descriptor,String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature,exceptions); //return mv; return new MethodVisitor(ASM4, mv) { @Override public void visitCode() { //给Tank.class字节码中的所有方法执行之前调用TimeProxy.before() //通过特殊的代码写,每个代码对应字节码,相当于汇编对应机器码 visitMethodInsn(INVOKESTATIC, "TimeProxy","before", "()V", false); super.visitCode(); } }; } }; //被访问者接收访问者 cr.accept(cv, 0); //将动态生成的字节码保存到字节数组中 byte[] b2 = cw.toByteArray(); ​ String path = (String)System.getProperties().get("user.dir"); File f = new File(path + "/DesignPatterns/com/ahead/ASM/"); f.mkdirs(); ​ //指定动态生成的Tank.class文件保存的位置 FileOutputStream fos = new FileOutputStream(new File(path + "/DesignPatterns/com/ahead/ASM/Tank_0.class")); fos.write(b2); fos.flush(); fos.close(); } } ​ class TimeProxy { public static void before() { System.out.println("before ..."); } }

通过ASM修改后的Tank.class字节码反编译后

public class Tank { public Tank() { TimeProxy.before(); super(); } ​ public void move() { TimeProxy.before(); System.out.println("Tank Moving ClaClaCla ..."); } }