加载中...

动态代理


代理模式

代理模式,就是使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能

举例来说,生活中有很多演员艺人之类的人,一般他们去跟外界沟通都不会直接是本人去,都是经纪人或者工作室去联络通知,这个经纪人、工作室就相当于这里的代理

代理模式大致有三种角色:

  • Real Subject:真实类,也就是被代理类、委托类。用来真正完成业务服务功能;
  • Proxy:代理类。将自身的请求用 Real Subject 对应的功能来实现,代理类对象并不真正的去实现其业务功能;
  • Subject:定义 Real Subject 和 Proxy 角色都应该实现的接口。

一个基本的 UML 图如下所示

代理主要分为两种类型:静态代理和动态代理,动态代理又有 JDK 代理和 CGLib 代理两种

要理解静态和动态这两个含义,我们首先需要理解一下 Java 程序的运行机制:

首先 Java 源代码经过编译生成字节码,然后再由 JVM 经过类加载,连接,初始化成 Java 类型,可以看到字节码是关键,静态和动态的区别就在于字节码生成的时机。

静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在编译时已经将接口,被代理类(委托类),代理类等确定下来,在程序运行前代理类的.class文件就已经存在了。

动态代理:在程序运行后通过反射创建生成字节码再由 JVM 加载而成。

静态代理

 //定义业务接口
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;
     }
 }
//创建代理类
public class SmsProxy implements SmsService {
 
     // 将委托类注入进代理类
     private final SmsService smsService;
 
     public SmsProxy(SmsService smsService) {
         this.smsService = smsService;
     }
 
     @Override
     public String send(String message) {
         // 调用委托类方法之前,我们可以添加自己的操作
         System.out.println("before method send()");
         // 调用委托类方法
         smsService.send(message);
         // 调用委托类方法之后,我们同样可以添加自己的操作
         System.out.println("after method send()");
         return null;
     }
 }
//具体使用
public static void main(String[] args) {
    SmsService smsService = new SmsServiceImpl();
    SmsProxy smsProxy = new SmsProxy(smsService);
    smsProxy.send("Java");
}

静态代理的不足之处:

  1. 代理类只代理一个委托类(其实可以代理多个,但不符合单一职责原则),也就意味着如果要代理多个委托类,就要写多个代理(静态代理在编译前必须确定)
  2. 如果每个委托类的每个方法都要被织入同样的逻辑,比如说要计算每个委托类每个方法的耗时,就要在方法开始前,开始后分别织入计算时间的代码,那就算用代理类,它的方法也有无数这种重复的计算时间的代码

JDK动态代理

回顾静态代理,我们可以讲其执行过程抽象为:

显而易见,代理类无非是在调用委托类方法的前后增加了一些操作。委托类的不同,也就导致代理类的不同。

那么为了做一个通用性的代理类出来,我们把调用委托类方法的这个动作抽取出来,把它封装成一个通用性的处理类,于是就有了动态代理中的 InvocationHandler 角色(处理类)。这个角色主要是对代理类调用委托类方法的这个动作进行统一的调用

动态代理的具体使用步骤:

1)定义一个接口(Subject)

2)创建一个委托类(Real Subject)实现这个接口

3)创建一个处理类并实现 InvocationHandler 接口,重写其 invoke 方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些处理逻辑),并将委托类注入处理类

4)创建代理对象(Proxy):通过 Proxy.newProxyInstance() 创建委托类对象的代理对象。

注意到这个方法有三个参数:

public static Object newProxyInstance(ClassLoader loader,
                                         Class<?>[] interfaces,
                                         InvocationHandler h);
  1. loader: 代理类的ClassLoader,最终读取动态生成的字节码,并转成 java.lang.Class 类的一个实例,通过此实例的 newInstance() 方法就可以创建出代理的对象
  2. interfaces: 委托类实现的接口,JDK 动态代理要实现所有的委托类的接口
  3. InvocationHandler: 委托对象所有接口方法调用都会转发到 InvocationHandler.invoke(),在 invoke() 方法里我们可以加入任何需要增强的逻辑 主要是根据委托类的接口等通过反射生成的

动态代理避免了静态代理那样的硬编码,另外所有委托类实现接口的方法都会在 Proxy 的 InvocationHandler.invoke() 中执行,这样如果要统计所有方法执行时间这样相同的逻辑,可以统一在 InvocationHandler 里写, 也就避免了静态代理那样需要在所有的方法中插入同样代码的问题

public interface Subject {
   public void request();
}
// 委托类
public class RealSubject implements Subject {
   @Override
   public void request() {
       // 卖房
       System.out.println("卖房");
   }
}

//代理类
public class ProxyFactory {

   private Object target;// 维护一个目标对象

   public ProxyFactory(Object target) {
       this.target = target;
   }

   // 为目标对象生成代理对象
   public Object getProxyInstance() {
       return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
               new InvocationHandler() {

                   @Override
                   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                       System.out.println("计算开始时间");
                       // 执行目标对象方法
                       method.invoke(target, args);
                       System.out.println("计算结束时间");
                       return null;
                   }
               });
   }

   public static void main(String[] args) {
       RealSubject realSubject = new RealSubject();
       System.out.println(realSubject.getClass());
       Subject subject = (Subject) new ProxyFactory(realSubject).getProxyInstance();
       System.out.println(subject.getClass());
       subject.request();
   }
}

打印结果如下:

计算开始时间
卖房
计算结束时间

CGLIB 动态代理

从newProxyInstance 的方法签名可以看出,JDK动态代理的一个问题是,委托类必须实现了某个代理接口,并且代理类也只能代理接口中实现的方法,为了解决这个问题,我们可以用 CGLIB 动态代理机制。

CGLIB 动态代理也提供了类似的 Enhance 类,,原理就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,织入额外的业务逻辑,具体实现的增强逻辑写在 MethodInterceptor.intercept() 中,也就是说所有委托类的非 final 方法都会被方法拦截器拦截

使用示例如下:

public class MyMethodInterceptor implements MethodInterceptor {
   @Override
   public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
       System.out.println("目标类增强前!!!");
       //注意这里的方法调用,不是用反射!!!
       Object object = proxy.invokeSuper(obj, args);
       System.out.println("目标类增强后!!!");
       return object;
   }
}

public class CGlibProxy {
   public static void main(String[] args) {
       //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
       Enhancer enhancer = new Enhancer();
       //设置目标类的字节码文件
       enhancer.setSuperclass(RealSubject.class);
       //设置回调函数
       enhancer.setCallback(new MyMethodInterceptor());

       //这里的create方法就是正式创建代理类
       RealSubject proxyDog = (RealSubject) enhancer.create();
       //调用代理类的方法
       proxyDog.request();
   }
}

打印结果如下:

目标类增强前!!!
卖房
目标类增强后!!!

JDK 动态代理和 CGLIB 动态代理对比

1)JDK 动态代理是基于实现了接口的委托类,通过接口实现代理;而 CGLIB 动态代理是基于继承了委托类的子类,通过子类实现代理。

2)JDK 动态代理只能代理实现了接口的类,且只能增强接口中现有的方法;而 CGLIB 可以代理未实现任何接口的类,但不能是final的方法。

3)就二者的效率来说,大部分情况都是 JDK 动态代理的效率更高,随着 JDK 版本的升级,这个优势更加明显。

4)JDK 动态代理的拦截对象是通过反射的机制来调用被拦截方法的,CGLIB 采用了FastClass 的机制来实现对被拦截方法的调用

FastClass 机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法

总结

静态代理就是,对于你想要增强的委托类,我们需要新建一个代理类,这两个类实现一个同样的接口,然后将委托类注入进代理类中,在代理类的方法中调用委托类中的对应方法。这样,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

从 JVM 层面来说, 静态代理就是在编译时就将接口、委托类、代理类这些都变成了一个个实际的 .class 文件。

静态代理的弊端很明显,一个委托类对应一个代理类,多个委托类就需要新建多个代理类。

JDK 动态代理需要委托类实现一个接口,不过代理类就不需要也实现同样的接口了,但是,JDK 动态代理机制中添加了一个新的角色,那就是处理类。具体来说,我们需要新建一个处理类,然后将委托类注入处理类,另外,这个处理类需要实现 InvocationHandler 接口,并重写其 invoke 方法,在 invoke 方法中可以利用反射机制调用委托类的方法,并可以在其前后添加一些额外的处理逻辑。最后,我们定义一个代理类,通过 Proxy.newProxyInstance() 创建委托类对象的代理对象。

JDK 动态代理有一个最致命的问题是它只能代理实现了某个接口的实现类,并且代理类也只能代理接口中实现的方法,要是实现类中有自己私有的方法,而接口中没有的话,该方法就不能进行代理调用。

为了解决这个问题,我们可以用 CGLIB 动态代理机制,CGLIB(Code Generation Library)其实就是一个基于 ASM 的 Java 字节码生成框架。

解释一下什么是字节码生成框架:

一个 Class 类对应一个 .class 字节码文件,也就是说字节码文件中存储了一个类的全部信息。字节码其实是二进制文件,内容是只有 JVM 能够识别的机器码。

JVM 解析字节码文件也就是加载类的过程是这样的:JVM 读取 .class 字节码文件,取出二进制数据,加载到内存中,解析字节码文件内的信息,然后生成对应的 Class 类对象。

显然,这个过程是在编译期就发生的。

那如果我们在运行期遵循 Java 编译系统组织 .class 字节码文件的格式和结构,生成相应的二进制数据(这就是字节码工具做的事情),然后再把这个二进制数据加载转换成对应的类。这样,我们不就完成了在运行时动态的创建一个类吗。这个思想其实也就是动态代理的思想。

简单来说,动态代理就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用(这也就是为什么说 CGLIB 是基于继承的了),织入额外的业务逻辑。关键词就是拦截,CGLIB 引入一个新的角色方法拦截器,让其实现接口 MethodInterceptor,并重写 intercept 方法,这里的 intercept 用于拦截并增强委托类的方法(和 JDK 动态代理 InvocationHandler 中的 invoke 方法类似),最后,通过 Enhancer.create() 创建委托类对象的代理对象。

总之,三种代理的角色分配为:

静态代理:

  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类

JDK 动态代理:

  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类
  • InvocationHandler:处理类,统一调用方法

CGLIB 动态代理:

  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类
  • MethodInterceptor:方法拦截器,统一调用方法

文章作者: DestiNation
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 DestiNation !
  目录