• 【设计模式】代理模式


    概述:

    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

  • 相关阅读:
    使用envoy在k8s中作grpc的负载均衡
    操作系统中锁的原理(转)
    Linux shell利用sed如何批量更改文件名详解(转)
    Http 连接复用
    记一次Redis错误排查经历(redis cluster 节点重启后无限同步问题)
    nginx重启几种方法(转)
    k8s基础知识-1、基础组件
    Eclipse的预设的Include的路径
    转:音频与采样的计算
    转: wireshark过滤语法总结
  • 原文地址:https://www.cnblogs.com/pandaNHF/p/15417060.html
Copyright © 2020-2023  润新知