掘金 后端 ( ) • 2024-04-21 16:29

源码位置:spring_aop

上一篇文章中我们主要学习了AOP的思想和Spring AOP使用,本文讲的是Spring是如何实现AOP的,Spring AOP是基于动态代理来实现AOP的,在将动态代理之前先来了解一下什么是代理模式。

1. 代理模式

在现实中就有许多代理的例子:比如你想找一个明星帮你拍广告,明星只对他的代理人开放权限,你就需要跟他的代理人去沟通,由代理人来代替明星完成收钱+售后的操作,然而拍广告这件事还是明星去做的。

1.1 代理模式的定义

定义: 代理模式也叫委托模式,是一种设计模式,它为其他对象提供一种代理控制对这个对象的访问,它的作用就是通过一个代理类,让我们在调用目标对象方法的时候,不再是直接对该方法进行调用,而是通过代理对象间接调用

解释: 在某些情况下,调用方不太适合直接访问目标对象的方法(没开放权限),就需要使用这种设计模式来使用一个代理对象在调用方和目标对象之间起到中介作用,代理模式在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强(Advice)。这里的增强和上一篇Spring AOP中的通知(Advice)是一样的,本章节将Advice全都统称为增强。

代理模式的主要角色:

  • Subject:业务接口类,可以是抽象类或者接口(不一定有)
  • RealSubject:目标对象(被代理对象),也就是具体的业务执行
  • Proxy:代理对象,RealSubject的代理

Untitled Diagram.drawio-2.png

1.2 代理模式的实现

代理模式的实现分为静态代理动态代理

1.2.1 静态代理

定义:在程序运行前,代理类的.class文件就已经存在了,也就是提前把所有方法的代理业务全都写好在程序里。

接下来我通过代码的方式,以明星接广告为例来加深理解:

  1. 定义接口(定义拍广告的业务)
public interface Subject {
    void takeAdvertise();
}
  1. 实现接口(由明星来实现拍广告这个业务)
public class RealSubject implements Subject{
    @Override
    public void takeAdvertise() {
        System.out.println("我是明星,我来拍广告");
    }
}
  1. 实现代理(代理人,帮明星处理业务中的额外操作)
public class Proxy implements Subject{
    private final RealSubject realSubject;
    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public void takeAdvertise() {
        System.out.println("我是代理人,开始收钱");
        realSubject.takeAdvertise();
        System.out.println("我是代理人,提供售后");
    }
}

测试代码:模拟代理模式下你通过Proxy(代理对象),访问目标对象(RealSubject)

public class Application {
    public static void main(String[] args) {
        Proxy proxy = new Proxy(new RealSubject());
        proxy.takeAdvertise();
    }
}

运行结果:

我是代理人,开始收钱
我是明星,我来拍广告
我是代理人,提供售后

这就是一个静态的代理模式的实现,静态代理模式的劣势也很明显,当你需要新增一个业务的时候,比如找明星开演唱会的时候,他的代理人做的事情同样是收钱+售后,你就需要继续编写下面代码:

  1. 定义接口:
public interface Subject {
    void takeAdvertise();
    void giveConcert();
}
  1. 实现接口:
public class RealSubject implements Subject{
    @Override
    public void takeAdvertise() {
        System.out.println("我是明星,我来拍广告");
    }

    @Override
    public void giveConcert() {
        System.out.println("我是明星,我来开演唱会");
    }
}
  1. 实现代理:
public class Proxy implements Subject{
    private final RealSubject realSubject;
    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public void takeAdvertise() {
        System.out.println("我是代理人,开始收钱");
        realSubject.takeAdvertise();
        System.out.println("我是代理人,提供售后");
    }

    @Override
    public void giveConcert() {
        System.out.println("我是代理人,开始收钱");
        realSubject.giveConcert();
        System.out.println("我是代理人,提供售后");
    }
}
  1. 运行结果:
我是代理人,开始收钱
我是明星,我来开演唱会
我是代理人,提供售后

这时候我们发现代理对象出现冗余代码,既然代理对象做的都是收钱+售后两件事,我们是否有办法消除这些冗余代码呢?这时候我们就要提到动态代理了。

1.2.2 动态代理

定义:在程序运行时,运用反射机制动态创建而成

相对于静态代理来说,它更加灵活:我们不需要针对每个目标对象业务的增加而去修改代理对象中的代码,而是把创建代理对象的工作推迟到程序运行时由JVM来实现,也就是说动态代理在程序的运行时,根据需要动态创建生成。

Java对动态代理进行了实现,并提供了一些API,常见的实现方式有两种:

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

JDK动态代理

  1. 编写代理类:通过JDK动态代理的提供的接口实现一个新的代理类
public class JDKInvocationHandler implements InvocationHandler {
    private Object target;

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

    //作用是调用目标方法并对方法进行增强
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK动态代理开始");
        Object result = method.invoke(target, args);
        System.out.println("JDK动态代理结束");
        return result;
    }
}

解释一下代理类:

  • 代理类中同样需要一个目标对象target,这里我们使用Object得以接收不同的目标对象
  • 实现 java.lang.reflect.InvocationHandler 接口,并实现invoke方法
  • invoke()方法的作用是调用目标方法并对方法进行增强,代理对象增强的内容和目标方法的逻辑在该方法的代码块中得以体现
  • invoke()方法中有三个参数Object proxy, Method method, Object[] args
    • proxy: 表示生成的代理对象,通常不直接使用
    • method: 是反射机制里的对象,该对象可以通过类的class对象+方法名来获取,通过该对象可以调用目标方法
    • args: 参数列表,用于传入目标方法中需要的参数
  1. 获取代理对象并使用:
public class Application {
    public static void main(String[] args) {
        //动态代理
        
        //目标类
        RealSubject target = new RealSubject();
        //根据代理类获取代理对象
        Subject proxy = (Subject) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[] {Subject.class},
                new JDKInvocationHandler(target));

        proxy.giveConcert();
        proxy.takeAdvertise();
    }
}

解释下上述过程如何获取代理对象的:

获取代理对象需要使用java.lang.reflect.Proxy类中的newProxyInstance()方法,在该方法中需要传入三个参数:

  • 类加载器: 在使用动态代理时,通常建议传入目标对象的类加载器作为参数,以确保代理对象能够正常访问目标对象的方法。
  • Class数组: 这是一个包含目标对象所实现的接口的数组,代理对象将实现这些接口,并将方法调用委托给 JDKInvocationHandler(下一个参数)
  • InvocationHandler: 这是一个实现了 InvocationHandler 接口的对象,用于处理代理对象的方法调用。这里传入了一个JDKInvocationHandler 对象,它将拦截代理对象的方法调用,并在方法执行前后执行额外的逻辑。

JDK动态代理的局限性:代理类和目标类都必须实现相同的接口,因此只能代理接口类,如果把目标类为普通类的话就会报错,而CGLIB动态代理恰巧可以弥补这方面的不足,接下来我们就讲讲GCLIB。

CGLIB动态代理

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,允许我们在运行时对字节码进行修改和动态生成,CGLIB通过继承方式实现代理。

  1. 导入GCLIB的依赖:
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>
  1. 实现接口类:实现MethodInterceptor接口,使用起来和JDK动态代理差不多,这里就不赘述了。
public class CGLibMethodInterceptor implements MethodInterceptor {
    private Object target;

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

    /**
     *
     * @param o 代理类
     * @param method 目标方法
     * @param objects 参数列表
     * @param methodProxy
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLib动态代理开始");
        Object result = method.invoke(target, objects);
        System.out.println("CGLib动态代理结束");
        return result;
    }
}
  1. 这里我新创建一个不实现接口的普通目标对象
public class RealSubject2 {
    public void takeAdvertise() {
        System.out.println("我是明星,我来拍广告");
    }

    public void giveConcert() {
        System.out.println("我是明星,我来开演唱会");
    }
}
  1. 获取代理对象并使用,与JDK获取代理对象的差别如下:
  • JDK中必须使用一个接口类来接收代理对象,GCLib直接使用目标类来接收
  • 创建的时候调用net.sf.cglib.proxy.Enhancer类的creat()方法
  • create()方法中少传了一个class[]接口列表
public class Application {
    public static void main(String[] args) {
        //CGLIB动态代理
        RealSubject2 target = new RealSubject2();
        RealSubject2 proxy = (RealSubject2) Enhancer.create(
                target.getClass(),
                new CGLibMethodInterceptor(target));

        proxy.takeAdvertise();
        proxy.giveConcert();
    }
}
  1. 成功对普通目标对象的目标方法进行增强:
CGLib动态代理开始
我是明星,我来拍广告
CGLib动态代理结束
CGLib动态代理开始
我是明星,我来开演唱会
CGLib动态代理结束

2. Spring AOP 的实现

Spring AOP是通过动态代理实现的,上面我们提到的JDK动态代理和CGLIB动态代理,Spring AOP都用到了。

【问题】Spring AOP 什么时候使用JDK,什么时候又使用GCLIB呢?

Spring AOP通过代理工厂创建代理对象,代理工厂中有一个重要的属性:proxyTargetClass,它的值可以为true|false

proxyTargetClass 目标对象 代理方式 false 实现了接口 jdk代理 false 未实现接口(只有实现类) cglib代理 true 实现了接口 cglib代理 true 未实现接口(只有实现类) cglib代理

该属性是基于程序员配置的,Spring Framework中默认属性值为false,可以通过@EnableAspectJAutoProxy(proxyTargetClass = true)来修改为true。

Spring Boot项目中,@EnableAspectJAutoProxy注解无效,程序员可在application.yml中修改属性值,并且从Spring Boot 2.x 开始,该属性值默认为true,程序员可以在配置文件中将其修改为false

spring:
  aop:
    proxy-target-class: false

3. 总结

本篇文章首先介绍了一种新的设计模式 —— 代理模式,然后围绕代理模式介绍了静态代理和动态代理分别是什么。

Spring AOP是基于动态代理实现的,本文着重介绍了两种动态代理方式的差别:JDK动态代理和CGLIB动态代理;并且介绍了Spring AOP什么时候使用JDK代理和CGLIB动态代理。