Skip to content

Spring AOP是如何实现的

动态代理

Spring AOP的实现原理核心是动态代理 (Dynamic Proxy)。 当我们为一个Spring Bean配置切面(Aspect)时,Spring IoC容器在创建这个Bean时,并不会返回原始的对象实例,而是返回一个该对象的代理对象。 后续所有对该Bean方法的调用,实际上都是通过这个代理对象来完成的。代理对象在调用原始方法的前后,有机会织入(Weave)我们在切面中定义的额外逻辑(即“通知”,Advice),从而实现了面向切面的编程。

Spring AOP主要采用两种动态代理技术来实现:

1. JDK动态代理

实现原理 JDK动态代理是Java原生支持的技术,它基于接口和反射机制。 当Spring决定为一个对象创建JDK动态代理时,它会通过java.lang.reflect.Proxy这个类,动态地在内存中生成一个代理类。这个代理类会同时实现目标对象所实现的所有接口,并且继承自Proxy类。 当我们调用代理对象上属于接口中的方法时,这个调用会被分派到我们所实现的InvocationHandler接口的invoke方法中。在这个invoke方法里,Spring就可以将切面逻辑(如@Before, @After等通知)织入,并在适当的时候通过反射调用目标对象的原始方法。

触发条件 这是Spring的默认策略。如果一个目标Bean实现了至少一个接口,Spring AOP就会默认使用JDK动态代理。

优点 * 原生支持:作为JDK的一部分,不需要引入任何第三方依赖。 * 创建速度快:通过反射生成代理类的速度通常比CGLib操作字节码要快。

缺点 * 必须基于接口:目标类必须实现接口,否则JDK动态代理无法工作。 * 只能代理接口方法:只能代理接口中声明的方法。如果一个类实现了接口,但我们在它的一个非接口方法上配置了切面,这个切面是不会生效的。同时,因为代理对象只暴露了接口类型,我们也无法直接调用到目标类中独有的方法。 * 执行效率较低:执行代理方法时,底层需要通过反射机制来回调,这在性能上相比直接调用要慢。

2. CGLIB动态代理

实现原理 CGLIB (Code Generation Library) 是一个强大的、高性能的代码生成库。它通过操作字节码的方式,为目标类创建一个子类来作为代理类。 它会重写目标类中所有可以被重写的方法(非final、非private的方法),并在重写的方法中织入切面逻辑,从而实现AOP的功能。因为它是通过生成子类的方式实现,所以不要求目标类必须实现接口。

触发条件 当需要被代理的Bean没有实现任何接口时,Spring AOP会自动切换到CGLIB动态代理。 当然,我们也可以强制Spring对所有Bean都使用CGLIB代理。

优点 * 不依赖接口:因为是基于继承,所以目标类无需实现接口。 * 可代理范围更广:可以代理目标类中所有可被重写的方法,不仅限于接口方法。 * 执行效率高:生成的代理类和我们自己写的普通类差别不大,方法的调用是直接的,不涉及反射,因此执行效率高于JDK动态代理。

缺点 * 无法代理final类或方法:由于CGLIB的原理是继承,所以无法代理被final修饰的类(无法继承)或方法(无法重写)。 对于private方法,同样因为子类无法访问和重写,所以也无法代理。 * 创建速度慢:通过操作字节码来生成代理类的过程,比JDK的反射方式要慢。 但由于在Spring中,大部分Bean都是单例的,代理类只需创建一次,所以这个缺点通常可以接受。

代理机制的一个重要提醒:自调用问题

由于Spring AOP是基于代理的,因此有一个非常重要的限制需要理解:只有通过代理对象的外部方法调用,才会触发切面逻辑

思考以下场景:

public class MyService {
    public void methodA() {
        System.out.println("Executing method A");
        this.methodB(); // 内部调用
    }

    public void methodB() {
        System.out.println("Executing method B");
    }
}

假设我们为methodB配置了一个切面。当外部代码通过代理对象调用myService.methodA()时,methodA的切面(如果配置了)会生效。但是,在methodA内部,通过this.methodB()来调用methodB时,这个调用是直接发生在原始对象内部的,它绕过了代理对象。因此,methodB上配置的切面不会被触发。

这就是所谓的“自调用失效”问题。要解决这个问题,通常需要重构代码,或者通过AopContext.currentProxy()获取当前代理对象来发起调用,但后者会使代码与Spring AOP框架产生耦合。