【设计模式】代理模式

概述:

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

 

posted @ 2021-10-17 15:27  胖达利亚  阅读(206)  评论(0编辑  收藏  举报