【设计模式】代理模式
概述:
1. 什么是代理
2. 代理的分类
3. Spring AOP对动态代理的应用
一、什么是代理
你需要乘飞机,但是去不了机场,机票代理点就能让你实现买机票的需求。
你需要办理车过户,但是你不知道流程,在门口找一个专门代你办理的人,他都给你办了,这就是代理。
可见代理是个中间商,他代替原来的事务部门,满足你的需求,这就是代理模式的意义。
想象一下,你想修改某个类以实现特殊的功能,但是这个类在SDK包里,或者在远程机器上,怎么办?
这时候你可以找个代理,不就是想实现自定义功能吗?不用去改原始类了,你在我这随便改,我把原始类集成进来,这样我既有原始类的功能,又有你自定义的功能,不就完美了。
这就是代理模式。
二、代理的分类
1. 静态代理
这个 不好类比说明,因为java程序中有运行中的概念,静态代理就相当于运行前,你就已经写好了代理类,然后编译直接调用。
比如有如下场景,目前有个生产玩具的类,在不改变这个类的前提下,增加统计这个类生产玩具方法用时的功能,这个怎么实现?
1 /** 2 * 委托者,原始类,一个生产玩偶的工厂 3 */ 4 public class ToyFactory implements Produce { 5 @Override 6 public void produce_cat() { 7 System.out.println("生产了一只小猫"); 8 try { 9 Thread.sleep(new Random().nextInt(1000)); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 } 14 15 @Override 16 public void produce_deer() { 17 System.out.println("生产了一只小鹿"); 18 try { 19 Thread.sleep(new Random().nextInt(1000)); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 } 24 }
1 /** 2 * 生产方法统计时间的代理类 3 */ 4 public class ToyFactoryTimeProxy implements Produce{ 5 private ToyFactory toyFactory; 6 7 8 public ToyFactoryTimeProxy(ToyFactory toyFactory) { 9 this.toyFactory = toyFactory; 10 } 11 12 @Override 13 public void produce_cat() { 14 long startTime = System.currentTimeMillis(); 15 this.toyFactory.produce_cat(); 16 long endTime = System.currentTimeMillis(); 17 long takeTime = endTime - startTime; 18 System.out.println("log-----cat take time="+takeTime); 19 } 20 21 @Override 22 public void produce_deer() { 23 long startTime = System.currentTimeMillis(); 24 this.toyFactory.produce_deer(); 25 long endTime = System.currentTimeMillis(); 26 long takeTime = endTime - startTime; 27 System.out.println("log-----deer take time="+takeTime); 28 29 } 30 }
1 public static void main(String[] args) { 2 ToyFactory toyFactory = new ToyFactory(); 3 ToyFactoryTimeProxy toyFactoryTimeProxy = new ToyFactoryTimeProxy(toyFactory); 4 toyFactoryTimeProxy.produce_cat(); 5 }
执行结果:
生产了一只小猫
log-----cat take time=226
这就是静态代理的实现,这种方式属于聚合的方式,其实还有一种方式能实现类似的效果,就是继承。
我们可以继承工厂类,然后重写造小猫的方法,在这方法中写统计时间的逻辑,但是继承方式有弊端,如果我们再要一个功能,就是在统计完时间后,还打印日志,这无非就是再写一个子类,继承时间代理类,但是如果新的需求是先打印日志,再统计时间,对于继承来说,之前写的就要不了了,得再写一个工厂类的子类,作为日志代理类,再写一个日志代理类的子类,作为时间代理类。
然而通过聚合的方式,可以利用java多态的特性,既然所有的代理类和委托类都需要实现同一个接口,那么我们就直接都聚合接口,而不是具体的委托类,这样就可以实现代理类之间也可以互相代理了。
首先把时间代理类中的ToyFactory改成Produce。
1 /** 2 * 生产方法统计时间的代理类 3 */ 4 public class ToyFactoryTimeProxy implements Produce{ 5 private Produce produce; 6 7 8 public ToyFactoryTimeProxy(Produce produce) { 9 this.produce = produce; 10 } 11 12 @Override 13 public void produce_cat() { 14 long startTime = System.currentTimeMillis(); 15 this.produce.produce_cat(); 16 long endTime = System.currentTimeMillis(); 17 long takeTime = endTime - startTime; 18 System.out.println("log-----cat take time="+takeTime); 19 } 20 21 @Override 22 public void produce_deer() { 23 long startTime = System.currentTimeMillis(); 24 this.produce.produce_deer(); 25 long endTime = System.currentTimeMillis(); 26 long takeTime = endTime - startTime; 27 System.out.println("log-----deer take time="+takeTime); 28 29 } 30 }
1 /** 2 * 这是个生产方法打日志的代理类 3 */ 4 public class ToyFactoryLogProxy implements Produce{ 5 private Produce Produce; 6 7 8 public ToyFactoryLogProxy(Produce Produce) { 9 this.Produce = Produce; 10 } 11 12 @Override 13 public void produce_cat() { 14 this.Produce.produce_cat(); 15 System.out.println("log-----cat is produced"); 16 } 17 18 @Override 19 public void produce_deer() { 20 this.Produce.produce_deer(); 21 System.out.println("log-----deer is produced"); 22 } 23 }
1 public static void main(String[] args) { 2 ToyFactory toyFactory = new ToyFactory(); 3 ToyFactoryTimeProxy toyFactoryTimeProxy = new ToyFactoryTimeProxy(toyFactory); 4 ToyFactoryLogProxy toyFactoryLogProxy = new ToyFactoryLogProxy(toyFactoryTimeProxy); 5 toyFactoryLogProxy.produce_cat(); 6 }
执行结果:
生产了一只小猫 log-----cat take time=914 log-----cat is produced
如果想反过来,只需要把main方法中的聚合顺序调整一下就可以了。
这里跑题一下,积累一下多态的知识:
面向接口编程的概念,用电脑主板和显卡来举例。 如果你主板上链接的是具体的某个内存条,那么会造成一种什么情况: 在组装电脑之处,你对内存的要求就是2G就能满足,你new了一个2G的内存条,随着使用2G不够了,这个时候你已经没法切换了。 《面向对象软件构造(Object Oriented Software Construction)》中提出了开闭原则,它的原文是这样:“Software entities should be open for extension,but closed for modification”。
翻译过来就是:“软件实体应当对扩展开放,对修改关闭”。这句话说得略微有点专业,我们把它讲得更通俗一点,也就是:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,引入新功能。
开闭原则中“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于原有代码的修改是封闭的,即修改原有的代码对外部的使用是透明的。 然而要解决这个问题,答案其实就在接口上了,你约定好了一个规范接口,所有显卡对象要想接入主板,必须实现这个接口,那么你在设计功能的时候,压根不用考虑具体实现,
2G不够直接拔了换4G,ArrayList不行就直接换LinkList,具体实现与主体类就实现了解耦,而面向接口编程,本质上就是运用了java多态的特性。
静态代理虽然也实现了功能,但是存在两个问题:
1. 如果SDK包里有100个委托类需要代理,那么就得写100个代理类,这个在现实工作中并不稀奇,最常用到的就是AOP,你需要拦截符合条件的所有类的方法,给他们附加上功能,这个要用静态代理实现就是把每个类都加上代码。
2. 就算委托类很少,但是里面的方法很多,也会造成很大的工作量,而且同样的代码会重复写很多次,100个方法就得写一百次统计时间的那段代码,极其繁琐。
如果我们自己去解决这两个问题,会怎么写,首先,需要根据委托类灵活的去生成对应的代理类,这个必须是一个自动的过程,如问题1,可能巨量的类需要代理,必须全自动才能解决量的问题。
再有就是对于委托类中方法的解决方案,如果你动态生成的代理类里,还是一个一个的去实现方法,问题2就解决不掉,最好是有一个通用的方法,这个方法能代表委托类中的所有方法(或者符合条件的方法),然后在这个类中加上你想加的代码,就等于所有方法都有了,实现了这两种解决方案的,就是动态代理。
2. 动态代理
继续上面的思路,我们的问题转移到怎么生成一个动态代理上面来了。
继续思考,你要生成这个样的一个代理,首先你要获取到委托类实现了哪些接口,因为我们将要生成的代理类也要实现接口,其次是咱们要这个代理类干啥活,打日志也好,筛选返回值也好,你得告诉它。
我们看看JDK的是否跟我们说的一样:
public class Proxy implements java.io.Serializable {
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException 6 { 9 final Class<?>[] intfs = interfaces.clone(); 15 /* 16 * Look up or generate the designated proxy class. 17 */ 18 Class<?> cl = getProxyClass0(loader, intfs); 20 /* 21 * Invoke its constructor with the designated invocation handler. 22 */ 23 try { 28 final Constructor<?> cons = cl.getConstructor(constructorParams); 29 final InvocationHandler ih = h; 30 if (!Modifier.isPublic(cl.getModifiers())) { 31 AccessController.doPrivileged(new PrivilegedAction<Void>() { 32 public Void run() { 33 cons.setAccessible(true); 34 return null; 35 } 36 }); 37 } 38 return cons.newInstance(new Object[]{h});
48 } catch (NoSuchMethodException e) { 49 throw new InternalError(e.toString(), e); 50 } 51 }
}
可以看到,在生成动态代理类的方法中,跟我们预想的只多了一个ClassLoader,委托类实现的一些接口(Class<?>[] interfaces),和我们需要的委托类做的事(InvocationHandler h),这里都有。
我们需要重点关注Class<?> cl = getProxyClass0(loader, intfs)这句代码,这里产生了代理类,这个类就是动态代理的关键。
可以通过java自带的类方法ProxyGenerator.generateProxyClass,看看jdk给我生成的代理文件是什么样子的:
1 byte[] Proxy0s = ProxyGenerator.generateProxyClass("12345", ToyFactory.class.getInterfaces()); 2 String path = "C:\\Users\\panda_zhu\\Desktop\\12345.class"; 3 try{ 4 FileOutputStream fos = new FileOutputStream(path); 5 fos.write(Proxy0s); 6 fos.flush(); 7 System.out.println("编译文件生成完毕!"); 8 } catch (Exception e) { 9 e.printStackTrace(); 10 }
委托类:
/** * 委托者,原始类,一个生产玩偶的工厂 */ public class ToyFactory implements Produce { @Override public void produce_cat() { System.out.println("生产了一只小猫"); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void produce_deer() { System.out.println("生产了一只小鹿"); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } }
生成的class文件反编译后如下(部分):
public final class 12345 extends Proxy implements Produce { private static Method m1; private static Method m4; private static Method m2; private static Method m3; private static Method m0; public 12345(InvocationHandler paramInvocationHandler)throws { super(paramInvocationHandler); }
//从这里可以很清晰的看到,利用反射,把委托类中的方法取出,聚合到代理类中,然后通过父类的属性InvocationHandler中的invoke方法执行,这个方法后续说。从而实现了代理委托类的功能。 static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); m4 = Class.forName("com.example.design.proxy.Produce").getMethod("produce_deer", new Class[0]); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); m3 = Class.forName("com.example.design.proxy.Produce").getMethod("produce_cat", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); return; } } public final boolean equals(Object paramObject) throws { try { return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); } } public final void produce_deer() throws { try { this.h.invoke(this, m4, null); return; } } } public final void produce_cat() throws { try { this.h.invoke(this, m3, null); return; } } }
到此为止,我们至少解决了一个问题,那就是动态生成一个代理文件。
但是,这里其实有一个疑问点,就是这个生成的代理类,是怎么知道我的委托类是谁的,这里也就依据和委托类实现同一个接口而写了方法的空壳子而已,真正实现都是人家InvocationHandler的invoke方法去实现的,不论是生产鹿也好生产猫也好,就这一个方法,当然这也是咱们最初的设想,即上一节通用方法的解决方案,但是是怎么实现的呢?又是怎么精准定位委托类的呢?
我们看看这个类的源码:
public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
就一个方法,第一个参数传入代理类,咱们自动生成的代理类,传入的是他自己。
第二个是需要执行的方法,这个代理类传的是他反射出来接口的方法。
第三个是这些方法的参数,这里为了方便看咱们没有参数。
通过这个方法也得不出这个答案,因为代理类本来也没法说清楚委托类是谁,第二个顶多告诉这个通用的方法,我要执行的是哪个方法,所以可以推断,这些都应该落在自己定义的Invocation上。
/** * 自定义的invoaction类, */ public class MyInvocationHandler implements InvocationHandler { private ToyFactory toyFactory; public MyInvocationHandler(ToyFactory toyFactory){ this.toyFactory = toyFactory; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("开始执行自定义方法。。"); long startTime = System.currentTimeMillis(); method.invoke(toyFactory,new Object[]{}); long endTime = System.currentTimeMillis(); System.out.println("执行"+method.getName()+"方法共耗时:"+(endTime-startTime)); return null; } }
答案在这里,跟预想的一样,是在自定义invacation类里聚合了委托类,并且通过method.invoke()方法,实现传入哪个方法,调用托管类哪个方法这种灵活性的。
这里有一个疑问,就是传进来的proxy对象好像没有用上,这个是干啥的,其实这个参数是为了返回值,jdk文档中表示,这个invoke方法的返回值必须跟传入的proxy返回值对应。
1 ToyFactory toyFactory = new ToyFactory(); 2 //使用动态代理 3 Produce o = (Produce)Proxy.newProxyInstance(toyFactory.getClass().getClassLoader(), toyFactory.getClass().getInterfaces(), new MyInvocationHandler(toyFactory)); 4 o.produce_deer();
开始执行自定义方法。。
生产了一只小鹿
执行produce_deer方法共耗时:974
至此,咱们静态变量遇到的问题就算是彻底解决了。
动态生成代理类解决了需要一直自己写代理类的事,method.invoke方法解决了每个方法都需要写重复代码的问题。
3. Spring AOP 对动态代理的应用
Spring AOP是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。
在web开发中,我们通常将项目分为controller、service、dao层,这种分层是一种纵向的,我们为了好理解,可以把它想象层一个竖状的圆柱形,数据从中川流不息。
而AOP则是从这个圆柱截面中插入一个滤网,也就是我们说的面向切面。
在日常开发中,日志拦截、权限处理、异常拦截、事务等,都是基于这种切面完成的。
那spring在哪调用了动态代理呢?
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); } return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this); } }
最后那个返回值是不是很眼熟了。
题外知识点练习:
如何使用spring aop。
POM中引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.2.6.RELEASE</version> </dependency>
由于springboot默认配置启动aop,所以不用另外配置了。
1 @Aspect 2 @Component 3 public class WebAspect { 4 /* 5 * 切点,用来匹配需要切入的并增强的目标方法 6 * 下面的表示com.example.design.proxy.aop包下所有类的所有方法 7 * 匹配规则很灵活可以自行百度 8 */ 9 @Pointcut("execution(* com.example.design.proxy.aop.*.*(..))") 10 public void pointCut(){ 11 12 } 13 14 /* 15 * 在方法执行前开始执行 16 * */ 17 @Before("pointCut()") 18 public void beforeAdvice(JoinPoint joinPoint){ 19 System.out.println("前置通知开始。。"); 20 Signature signature = joinPoint.getSignature(); 21 System.out.println("目前代理的是哪一个方法:"+ signature.getName()); 22 } 23 24 /* 25 * 在方法执行后开始执行 26 * */ 27 @After("pointCut()") 28 public void afterAdvice(){ 29 System.out.println("后置通知开始。。"); 30 } 31 32 33 @AfterReturning(value = "execution(* com.example.design.proxy.aop.*.*(..))",returning = "args") 34 public void afterReturningAdvice(JoinPoint joinPoint,String args){ 35 System.out.println("后置返回通知开始。。"); 36 System.out.println("返回值是:"+args); 37 } 38 39 40 41 }
写一个切面类,确定拦截那些目标类,如果是使用动态代理的话,这一步就是挑选委托类和组装自定义invocation的地方,这里只是挑选了几个基本的通知方式,其实还有环绕通知,异常通知等等,对应的是咱们在自定义invocation中方法执行不同位置写入的增强代码。
@RestController @RequestMapping("/aop") public class AopController { @RequestMapping("before") public String testBeforeAdvice(){ return "testBeforeAdvice方法开始执行!"; } }
http://localhost:8080/aop/before
前置通知开始。。
目前代理的是哪一个方法:testBeforeAdvice
后置返回通知开始。。
返回值是:testBeforeAdvice方法开始执行!
后置通知开始。。
全文涉及知识点:
1. 代理模式,包括动态代理,静态代理。
2. java多态。
3. spring aop对于动态代理的使用。
4. aop在springboot中使用示例。
练习源码:https://github.com/panda-zhu/design
全文借鉴:
Spring AOP实现原理: https://blog.csdn.net/moreevan/article/details/11977115/
10分钟看懂动态代理模式: https://www.cnblogs.com/faster/p/10874371.html