跳至主要內容

设计模式学习-代理模式

chenxi编程设计模式大约 7 分钟

为什么要有代理模式

假设我们要对学生的信息进行更新,在对学生信息进行更新前,以及更新后,我们都需要进行日志的打印,如果不借助代理模式,我们的实现方式:

public class Student {
    public void update() {
        System.out.println("学生信息更新前进行日志打印!"); // 使用sout模拟日志打印
        System.out.println("学生信息已完成更新~"); // 模拟学生信息更新操作
        System.out.println("学生信息更新后进行日志打印!");
    }
}

从以上代码可以发现,我们业务最核心的代码,就只有学生信息更新这一操作,而日志打印并非业务的核心代码,我们将非核心代码与核心代码杂糅在一块,就造成了代码的冗余。
此时,我可以借助代理模式来解决该问题。

什么是代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对目标对象的引用。
代理模式可以理解为是一种思想,对于代理模式的具体实现又可分为:

  • 静态代理
  • 动态代理

静态代理

静态代理的实现方式较为简单,将目标对象要被代理的方法提取出来,定义一个接口。
代理类与目标类都去实现该接口,核心目的就是让代理类知道,它要代理目标类的哪些方法。
并将目标类中的非核心代码部分转移到代理类中,然后通过代理对象调用目标对象的方法的方式来执行核心代码部分。
静态代理代码实现:

public interface Student {
    void update();
}

public class StudentImpl implements Student {
    @Override
    public void update() {
        System.out.println("学生信息已完成更新~");
    }
}

public class StudentProxt implements Student {
    private Student student;

    public StudentProxt(Student student) {
        this.student = student;
    }

    @Override
    public void update() {
        System.out.println("学生信息更新前进行日志打印!"); // 使用sout模拟日志打印
        student.update(); // 目标类的目标方法中只写核心业务逻辑,不再进行日志打印
        System.out.println("学生信息更新后进行日志打印!");
    }
}

测试:

Student student = new StudentImpl();
Student studentProxt = new StudentProxt(student);
studentProxt.update();

可以发现静态代理这种方式是存在很大缺陷的,因为我们每要代理一个对象,都需要去创建一个代理类,并在方法中进行硬编码,代码的复用性很低,所以,引入了动态代理的方式。

动态代理

说起动态两字,就会让人想到反射技术,同时,动态代理又可分为:

  • JDK 动态代理
  • Cglib 动态代理

JDK 动态代理

JDK 动态代理即是通过 JDK 提供的 API 来进行动态代理的实现。
同静态代理一样,我们需要先定义一个公共的接口:

public interface Student {
    void update();
}

然后,定义一个代理工厂类ProxyFactory,用于生成代理对象。
其中最核心的操作就是通过 JDK 中提供的Proxy类中的newProxyInstance()方法来生成代理对象。
newProxyInstance()方法需要传入 3 个参数:

public static Object newProxyInstance(ClassLoader loader, // 用于指定一个类加载器
                                          Class<?>[] interfaces, // 指定生成的代理对象有哪些方法,这也是定义公共接口的目的所在
                                          InvocationHandler h // 指定代理对象要做什么事情
                                     ) throws IllegalArgumentException 

其中第 3 个参数,一般通过匿名内部类的方式进行传入,并重写invoke()方法,而invoke()方法,也是对目标对象的方法进行增强,以及调用目标方法的地方。
具体实现

public class ProxyFactory<T> {
    // 要代理的目标对象
    private T targetObject;

    // 代理工厂初始化时传入目标对象
    public ProxyFactory(T targetObject) {
        this.targetObject = targetObject;
    }

    // 得到代理对象
    public Object getProxyInstance() {
        return Proxy.newProxyInstance(ProxyFactory.class.getClassLoader(),
                targetObject.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("前置日志打印!");
                        Object res =  method.invoke(targetObject, args);
                        System.out.println("后置日志打印!");
                        return res;
                    }
                });
    }
}

测试:

Student student = new StudentImpl();
ProxyFactory<Student> proxyFactory = new ProxyFactory<>(student);
Student proxyStudent = (Student)proxyFactory.getProxyInstance();
proxyStudent.update();

Cglib 动态代理

既然都有了 JDK 动态代理,为什么还要有 Cglib 动态代理呢?

从静态代理与 JDK 动态代理的实现中,我们可以发现二者的一个共通点,那就是都需要依赖一个公共接口,让目标类去实现它,以此来确定我们的代理对象要代理目标对象的哪些方法。
而要想不定义公共接口,而对目标对象进行代理的话,就可以使用 Cglib 动态代理。

什么是 Cglib 动态代理?

Cglib 动态代理也叫作子类代理,它主要是在内存中构建了一个目标对象的子类对象从而实现对目标对象的功能拓展,它的底层是通过字节码处理框架 ASM 来转换字节码并生成新的类。
因为 Cglib 动态代理走得是继承路线,所以就要求目标类不能被final关键字修饰,这也比较容易理解,被final修饰的类为最终类,是无法被任何类继承的。
因为 Cglib 并非是 JDK 内置类,所以要想使用 Cglib,需要引入其依赖:

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

Cglib 动态代理的代码实现:

依然是先定义目标类,但不再需要再定义公共接口了。

public class Student {
    public void update() {
        System.out.println("学生信息已完成更新~");
    }
}

定义代理工厂类CglibProxyFactory,用于生成代理对象。
该代理工厂类需要实现MethodInterceptor接口,并重写intercept()方法,这个方法同 JDK 代理的invoke()方法一样,当调用代理对象的目标方法时,这个时候会触发intercept()方法执行,并且将目标类的目标方法以及方法实参当作参数传入过来,我们就可以基于此来对目标方法进行增强
在 JDK 代理中,代理对象的生成是使用 JDK 提供的内置类,也就是Proxy类的newProxyInstance()方法帮助在内存中构建的代理对象,而在添加的 Cglib 依赖中,同样提供了Enhancer工具类,可以使用该类的create()方法帮助构建代理类。
值得注意的是,前面说 Cglib 代理类是通过继承的方式在内存中构建的子类,所以需要setSuperclass()方法指定代理的父类为当前目标对象,同时也需要通过setCallback()方法指定回调方法。
具体实现

public class CglibProxyFactory<T> implements MethodInterceptor {
    private T targetObject; // 要代理的目标对象

    // 代理工厂初始化时传入目标对象
    public CglibProxyFactory(T targetObject) {
        this.targetObject = targetObject;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("前置日志打印!");
        method.invoke(targetObject, objects);
        System.out.println("后置日志打印!");
        return null;
    }

    // 得到代理对象
    public Object getProxyInstance() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetObject.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }
}

无论是使用 JDK 动态代理还是 Cglib 动态代理,都需要感知到目标对象的目标方法
对此,JDK 动态代理采用让目标类去实现公共接口的方式,以此来指定要代理的对象有哪些方法。
而 Cglib 动态代理则是借助继承关系,它通过在内存中构建了一个目标对象的子类对象,以此来感知要代理的对象有哪些方法。

动态代理的简化-Spring AOP

从上述动态代理的代码实现来看,代码量还是不小的。
所以平时也几乎不会自己实现动态代理的代码,而是借助 Spring AOP 框架来进行切面编程。
对于 Spring AOP 框架,可以理解为基于 AOP 编程思维,封装动态代理技术,简化动态代理技术实现的框架。

Spring AOP 的底层技术:

  • JDK 动态代理:代理的目标类必须实现接口。
  • Cglib 动态代理:通过继承被代理的目标类实现代理,无需目标类实现接口。
  • AspectJ:早期的 AOP 实现框架,Spring AOP 借用了 AspectJ 中的 AOP 注解。
上次编辑于:
贡献者: chenxi