设计模式——代理模式

48 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第32天,点击查看活动详情

6 代理模式⭐️

介绍

  • 在代理模式下有两种角色,一种是被代理者,一种是代理(Proxy)。
    • 被代理者需要做一项工作时,不用自己做,而是交给代理做。
  • 使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
    • 代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
  • 举个例子:你找了小红来帮你问话,小红就可以看作是代理你的代理对象,代理的行为(方法)是问话。

  • 分类

    • 静态代理
    • JDK / CGLIB 动态代理
  • 动态代理

    • 相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。

    • 从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

    • 动态代理在我们日常开发中使用的相对较少,但是在框架中几乎是必用的一门技术。

      • 学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。
      • 说到动态代理,Spring AOP、RPC 框架应该是两个不得不提的,它们的实现都依赖了动态代理。

静态代理

JDK 动态代理

  • 介绍
    • InvocationHandler 接口和 Proxy 类是核心。
 // Proxy 类中使用频率最高的方法是:`newProxyInstance()` ,这个方法主要用来生成一个代理对象。
 ​
 // loader:类加载器,用于加载代理对象
 // interfaces: 被代理类实现的一些接口
 // h: 实现了 `InvocationHandler` 接口的对象。
 public static Object newProxyInstance(ClassLoader loader, 
                                       Class<?>[] interfaces,
                                       InvocationHandler h)
     throws IllegalArgumentException{
     //......
 }

要实现动态代理的话,还必须需要实现InvocationHandler 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。

 public interface InvocationHandler {
 ​
     /**
      * 当你使用代理对象调用方法的时候实际会调用到这个方法
      */
     // proxy:动态生成的代理类
     // method:与代理类对象调用的方法相对应
     // args:当前 method 方法的参数
     public Object invoke(Object proxy, Method method, Object[] args)
         throws Throwable;
 }

也就是说:你通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

  • JDK 动态代理类使用步骤
    • 定义一个接口及其实现类;
    • 自定义InvocationHandler并重写invoke方法,在invoke方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
    • 通过Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)方法创建代理对象;
  • 代码示例

    • 定义发送短信的接口
     public interface SmsService {
         String send(String message);
     }
    
    • 实现发送短信的接口
     public class SmsServiceImpl implements SmsService {
         public String send(String message) {
             System.out.println("send message:" + message);
             return message;
         }
     }
    
    • 定义一个 JDK 动态代理类
     import java.lang.reflect.InvocationHandler;
     import java.lang.reflect.InvocationTargetException;
     import java.lang.reflect.Method;
     ​
     public class DebugInvocationHandler implements InvocationHandler {
         // 代理类中的真实对象
         private final Object target;
     ​
         public DebugInvocationHandler(Object target) {
             this.target = target;
         }
       
       /**当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke() 方法,然后 invoke() 方法代替我们去调用了被代理对象的原生方法。
       */
         public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
             //调用方法之前,我们可以添加自己的操作
             System.out.println("before method " + method.getName());
             
             Object result = method.invoke(target, args);
             
             //调用方法之后,我们同样可以添加自己的操作
             System.out.println("after method " + method.getName());
             return result;
         }
     }
    
    • 获取代理对象的工厂类
     public class JdkProxyFactory {
         // 主要通过`Proxy.newProxyInstance()`方法获取某个类的代理对象
         public static Object getProxy(Object target) {
             return Proxy.newProxyInstance(
                     target.getClass().getClassLoader(), // 目标类的类加载
                     target.getClass().getInterfaces(),  // 代理需要实现的接口,可指定多个
                     new DebugInvocationHandler(target)   // 代理对象对应的自定义 InvocationHandler
             );
         }
     }
    
    • 实际使用
     SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
     smsService.send("java");
    
    • 运行上述代码之后,控制台打印出:
     before method send
     send message:java
     after method send
    

CGLIB 动态代理

  • 介绍

    • JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类
    • MethodInterceptor 接口和 Enhancer 类是核心。
    • 你需要自定义 MethodInterceptor 并重写 intercept() 方法,intercept() 用于拦截增强被代理类的方法。
    • 你可以通过 Enhancer 类来动态获取被代理类,当代理类调用方法的时候,实际调用的是MethodInterceptor 中的 intercept() 方法。
     public interface MethodInterceptor extends Callback{
         // 拦截增强被代理类中的方法
         
         // obj:被代理的对象(需要增强的对象)
       // method:被拦截的方法(需要增强的方法)
       // args:方法入参
       // proxy:用于调用原始方法
         public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                                    MethodProxy proxy) throws Throwable;
     }
    
    • 不需要额外的依赖(不同于 JDK 动态代理)。CGLIB(Code Generation Library) 属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖。
     <dependency>
       <groupId>cglib</groupId>
       <artifactId>cglib</artifactId>
       <version>3.3.0</version>
     </dependency>
    
  • CGLIB 动态代理类使用步骤

    • 定义一个类;
    • 自定义 MethodInterceptor 并重写 intercept() 方法,intercept() 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke() 方法类似;
    • 通过 Enhancer 类的 create()创建代理类;
  • 代码示例

    • 实现一个使用阿里云发送短信的类
     package github.javaguide.dynamicProxy.cglibDynamicProxy;
     ​
     public class AliSmsService {
         public String send(String message) {
             System.out.println("send message:" + message);
             return message;
         }
     }
    
    • 自定义 MethodInterceptor(方法拦截器)
     import net.sf.cglib.proxy.MethodInterceptor;
     import net.sf.cglib.proxy.MethodProxy;
    
     import java.lang.reflect.Method;
    
     /**
      * 自定义MethodInterceptor
      */
     public class DebugMethodInterceptor implements MethodInterceptor {
         /**
          * @param o           代理对象(增强的对象)
          * @param method      被拦截的方法(需要增强的方法)
          * @param args        方法入参
          * @param methodProxy 用于调用原始方法
          */
         @Override
         public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
             //调用方法之前,我们可以添加自己的操作
             System.out.println("before method " + method.getName());
             
             Object object = methodProxy.invokeSuper(o, args);
             
             //调用方法之后,我们同样可以添加自己的操作
             System.out.println("after method " + method.getName());
             return object;
         }
     }
    

对比

  • JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
  • 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。