动态代理模式(二)

140 阅读12分钟

这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战

三. 代理模式的种类

按照代理创建的时期来进行分类的可以分为:静态代理、动态代理。

静态代理是由程序员创建或特定工具自动生成源代码,在对其编译。在程序运行之前,代理类.class文件就已经被创建了。

动态代理是在程序运行时通过反射机制动态创建的。

3.1 静态代理

先来看静态代理的实现步骤:

1)定义一个接口(Subject)
2)创建一个委托类(Real Subject)实现这个接口
3)创建一个代理类(Proxy)同样实现这个接口
4)将委托类 Real Subject 注入进代理类 Proxy,在代理类的方法中调用 Real Subject 中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

从实现和应用角度来说,静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

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

上面我们举的买房的例子就是静态代理.

源代码见上面第二点

静态代理总结: 优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点:我们得为每一个实现类都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。比如: 接口Subject增加一个方法. 所有的实现类, 代理类都要想听的增加.

3.2 动态代理

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

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

于是,在代理类和委托类之间就多了一个处理类的角色,这个角色主要是对代理类调用委托类方法的动作进行统一的调用,也就是由 InvocationHandler 来统一处理代理类调用委托类方法的操作。看下图:

从 JVM 角度来说,动态代理是在运行时动态生成 .class 字节码文件 ,并加载到 JVM 中的。 虽然动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助,Spring AOP、RPC 等框架的实现都依赖了动态代理。

就 Java 来说,动态代理的实现方式有很多种,比如:

  • JDK 动态代理
  • CGLIB 动态代理
  • Javassit 动态代理

很多知名的开源框架都使用到了动态代理, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

下面详细讲解这三种动态代理机制。

1. JDK动态代理

先来看下 JDK 动态代理机制的使用步骤:

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

下面来看看InvocationHandler接口

package java.lang.reflect;

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

在InvocationHandler里面定义了invoke方法. 该方法有三个参数:

  • proxy:代理类对象(见下一步)
  • method:还记得我们在上篇文章反射中讲到的 Method.invoke 吗?就是这个,我们可以通过它来调用委托类的方法(反射)
  • args:传给委托类方法的参数列表

第四步: 创建代理对象(Proxy):通过 Proxy.newProxyInstance() 创建委托类对象的代理对象

    @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
    {
        .....
    }

Proxy.newProxyInstance()有三个参数

  1. 类加载器 ClassLoader
  2. 委托类实现的接口数组,至少需要传入一个接口进去
  3. 调用的 InvocationHandler 实例处理接口方法(也就是第 3 步我们创建的类的实例)

下面来看看案例实现

/**
 * 抽象接口
 */
public interface ISubject {
    void operate();
}

/**
 * 委托类, 也叫被代理类
 * 真正的处理逻辑
 */
public class RealSubject implements ISubject{
    @Override
    public void operate() {
        System.out.println("实际操作");
    }
}

/**
 * 代理对象的处理类
 */
public class ProxySubject implements InvocationHandler {
    private ISubject realSubject;

    public ProxySubject(ISubject subject) {
        this.realSubject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("调用方法前---前置操作");

        //动态代理调用RealSubject中的方法
        Object result = method.invoke(realSubject, args);

        System.out.println("调用方法后---后置操作");
        return result;
    }
}


/**
 * 客户端调用类
 */
public class JdkProxyClient {
    public static void main(String[] args) {
        ISubject subject = new RealSubject();
        ISubject result = (ISubject)Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), new ProxySubject(subject));
        result.operate();
    }
}

最后的运行结果是:

调用方法前---前置操作
实际操作
调用方法后---后置操作

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

2. CGLIB动态代理

CGLIB(Code Generation Library)是一个基于 ASM 的 Java 字节码生成框架,它允许我们在运行时对字节码进行修改和动态生成。原理就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,织入额外的业务逻辑。关键词大家注意到没有,拦截!CGLIB 引入一个新的角色就是方法拦截器 MethodInterceptor。和 JDK 中的处理类 InvocationHandler 差不多,也是用来实现方法的统一调用的。

CGLIB 动态代理的使用步骤:

第一步: 首先创建一个委托类(Real Subject)
第二步: 创建一个方法拦截器实现接口 MethodInterceptor,并重写 intercept 方法。intercept 用于拦截并增强委托类的方法(和 JDK 动态代理 InvocationHandler 中的 invoke 方法类似)

package org.springframework.cglib.proxy;

import java.lang.reflect.Method;

public interface MethodInterceptor extends Callback {
    Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;
}

该方法拥有四个参数:

  1. Object var1:委托类对象
  2. Method var2:被拦截的方法(委托类中需要增强的方法)
  3. Object[] var3:方法入参
  4. MethodProxy var4:用于调用委托类的原始方法(底层也是通过反射机制,不过不是 Method.invoke 了,而是使用 MethodProxy.invokeSuper 方法)

第三步: 创建代理对象(Proxy):通过 Enhancer.create() 创建委托类对象的代理对象.
也就是说:我们在通过 Enhancer 类的 create() 创建的代理对象在调用方法的时候,实际会调用到实现了 MethodInterceptor 接口的处理类的 intercept()方法,可以在 intercept() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

可以发现,CGLIB 动态代理机制和 JDK 动态代理机制的步骤差不多,CGLIB 动态代理的核心是方法拦截器 MethodInterceptor 和 Enhancer,而 JDK 动态代理的核心是处理类 InvocationHandler 和 Proxy。

代码示例

不同于 JDK的是, JDK 动态代理不需要添加额外的依赖,CGLIB 是一个开源项目,如果你要使用它的话,需要手动添加相关依赖。

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

第一步: 创建委托类

public class RealSubject {
    public void operate() {
        System.out.println("实际操作的动作");
    }
}

第二步: 创建拦截器类, 实现MethodInterceptor 接口. 在这里面可以对方法进行增强处理

public class ProxyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        System.out.println("调用真实操作之前---操作前处理");

       // 调用真实用户需要处理的业务逻辑
        Object object = methodProxy.invokeSuper(o, args);


        System.out.println("调用真实操作之后---操作后处理");
        return object;
    }
}

第三步: 创建代理对象Proxy:通过 Enhancer.create() 创建委托类对象的代理对象

public class CglibProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        // 创建cglib动态代理的增强类
        Enhancer enhancer = new Enhancer();

        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());

        // 设置委托类
        enhancer.setSuperclass(clazz);

        // 设置方法拦截器
        enhancer.setCallback(new ProxyMethodInterceptor());

        // 创建代理类
        return enhancer.create();
    }
}

从 setSuperclass 我们就能看出,为什么说 CGLIB 是基于继承的。

第四步: 客户端调用

public class CglibClient {
    public static void main(String[] args) {
        RealSubject proxy = (RealSubject)CglibProxyFactory.getProxy(RealSubject.class);
        proxy.operate();
    }
}

最后的运行结果:

调用真实操作之前---操作前处理 实际操作的动作 调用真实操作之后---操作后处理

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

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

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

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

4. 什么情况下使用动态代理?

1)我们知道, 设计模式的开闭原则,对修改关闭,对扩展开放,在工作中, 经常会接手前人写的代码,有时里面的代码逻辑很复杂不容易修改,那么这时我们就可以使用代理模式对原来的类进行增强。

2)在使用 RPC 框架的时候,框架本身并不能提前知道各个业务方要调用哪些接口的哪些方法 。那么这个时候,就可用通过动态代理的方式来建立一个中间人给客户端使用,也方便框架进行搭建逻辑,某种程度上也是客户端代码和框架松耦合的一种表现。

3)Spring AOP 采用了动态代理模式

5. 静态代理和动态代理对比

1)灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的

2)JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 .class 字节码文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

四. 代理模式的优缺点

优点:
1、职责清晰。
2、高扩展性。
3、智能化。

缺点
1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

五. 代理模式使用了哪几种设计原则?

  1. 单一职责原则: 一个接口只做一件事
  2. 里式替换原则: 任何使用了基类的地方,都可以使用子类替换. 不重写父类方法
  3. 依赖倒置原则: 依赖于抽象, 而不是依赖与具体
  4. 接口隔离原则: 类和类之间应该建立在最小的接口上
  5. 迪米特法则: 一个对象应该尽可能少的和对其他对象产生关联, 对象之间解耦
  6. 开闭原则: 对修改封闭, 对扩展开放(体现的最好的一点) 代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类

六. 代理模式和其他模式的区别

1. 代理模式和装饰器模式的区别

装饰器模式为了增强功能,而代理模式是为了加以控制。 来看看代理模式和装饰器模式的UML图

2. 代理模式和适配器模式的区别

适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口 来看看代理模式和适配器模式的UML图