
浅析Spring中AOP的实现原理——动态代理
浅析Spring中AOP的实现原理——动态代理
一、前言
今天上课的时候,我想尝试一下在Spring
里面的大名顶顶的CGLIB
代理方式,但是当时并没有成功,发现需要加配置才行,于是,我打算在这篇博客就来简单地聊一聊Spring
的AOP
是如何实现的,并通过一个简单的测试用例来验证一下。废话不多说,直接开始。
二、正文
2.1 Spring AOP的实现原理
Spring
的AOP
实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring
的某个bean
配置了切面,那么Spring
在创建这个bean
的时候,实际上创建的是这个bean
的一个代理对象,我们后续对bean
中方法的调用,实际上调用的是代理类重写的代理方法。而Spring
的AOP
使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
JDK动态代理
Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java
语言的应该会对JDK
动态代理有所了解。JDK
实现动态代理需要两个组件,首先第一个就是InvocationHandler
接口。我们在使用JDK
的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke
方法,这个方法其实就是我们提供的代理方法。然后JDK
动态代理需要使用的第二个组件就是Proxy
这个类,我们可以通过这个类的newProxyInstance
方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke
方法。
CGLIB动态代理
JDK
的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK
的动态代理将没有办法使用,于是Spring
会使用CGLib
的动态代理来生成代理对象。CGLib
直接操作字节码,生成类的子类,重写类的方法完成代理。
以上就是Spring
实现动态的两种方式,下面我们具体来谈一谈这两种生成动态代理的方式。
JDK动态代理
实现原理
JDK
的动态代理是基于反射实现。JDK
通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler
接口的invoke
方法。并且这个代理类是Proxy类的子类(记住这个结论,后面测试要用)。这就是JDK
动态代理大致的实现方式。
优点
JDK
动态代理是JDK
原生的,不需要任何依赖即可使用;- 通过反射机制生成代理类的速度要比
CGLib
操作字节码生成代理类的速度更快(我估计这就是为啥是Spring默认方式的主要原因);
缺点
- 如果要使用
JDK
动态代理,被代理的类必须实现了接口,否则无法代理(我猜这也是后面为啥后面Spring Boot要换成CGLIB的原因); JDK
动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring
仍然会使用JDK
的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。JDK
动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;
CGLIB动态代理
原理
CGLib
实现动态代理的原理是,底层采用了ASM
字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring
中的切面)植入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。
优点
- 使用
CGLib
代理的类,不需要实现接口,因为CGLib
生成的代理类是直接继承自需要被代理的类; CGLib
生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;CGLib
生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib
执行代理方法的效率要高于JDK
的动态代理;
缺点
- 由于
CGLib
的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final
类,则无法使用CGLib
代理; - 由于
CGLib
实现代理方法的方式是重写父类的方法,所以无法对final
方法,或者private
方法进行代理,因为子类无法重写这些方法; CGLib
生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK
通过反射生成代理类的速度更慢;
代码测试
测试JDK动态代理
下面我们通过一个简单的例子,来验证上面的说法。首先我们需要一个接口和它的一个实现类,然后再为这个实现类的方法配置切面,看看Spring
是否真的使用的是JDK
的动态代理。假设接口的名称为Human
,而实现类为Student
:
package com.hanserwei.entity;
public interface Human {
void display();
}
package com.hanserwei.entity;
import com.hanserwei.annotation.DynamicProxy;
import org.springframework.stereotype.Component;
@Component
public class Student implements Human {
@Override
@DynamicProxy
public void display() {
System.out.println("我是Hanserwei!");
}
}
定义自定义注解,待会用来标记切入点
package com.hanserwei.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicProxy {
String value() default "";
}
定义AOP切面类
package com.hanserwei.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class HumanAspect {
@Pointcut("@annotation(com.hanserwei.annotation.DynamicProxy)")
public void pointcut() {}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("before");
joinPoint.proceed();
System.out.println("after");
return null;
}
}
这里我用配置类来代替配置文件(配置文件确实太抽象了,用不惯)
package com.hanserwei.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.hanserwei")
public class SpringApplicationConfig {
}
APP启动类暨测试类
package com.hanserwei;
import com.hanserwei.config.SpringApplicationConfig;
import com.hanserwei.entity.Human;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringApplicationConfig.class);
// 注意,这里只能通过Human.class获取,而无法通过Student.class,因为在Spirng容器中,
// 因为使用JDK动态代理,Ioc容器中,存储的是一个类型为Human的代理对象
Human student = applicationContext.getBean(Human.class);
student.display();
// 输出代理类的父类,以此判断是JDK还是CGLib
System.out.println(student.getClass().getSuperclass());
}
}
注意看上面代码中,最长的那一句注释。由于我们需要代理的类实现了接口,则Spring
会使用JDK
的动态代理,生成的代理类会实现相同的接口,然后创建一个代理对象存储在Spring
容器中。这也就是说,在Spring
容器中,这个代理bean
的类型不是Student
类型,而是Human
类型,所以我们不能通过Student.class
获取,只能通过Human.class
(或者通过它的名称获取)。这也证明了我们上面说过的另一个问题,JDK
动态代理无法代理没有定义在接口中的方法。假设Student
这个类有另外一个方法,它不是Human
接口定义的方法,此时就算我们为它配置了切面,也无法将切面织入。而且由于在Spring
容器中保存的代理对象并不是Student
类型,而是Human
类型,这就导致我们连那个不属于Human
的方法都无法调用。这也说明了JDK
动态代理的局限性。
我们前面说过,JDK
动态代理生成的代理类继承了Proxy
这个类,而CGLib
生成的代理类,则继承了需要进行代理的那个类,于是我们可以通过输出代理对象所属类的父类,来判断Spring
使用了何种代理。下面是输出结果:
before
我是Hanserwei!
after
class java.lang.reflect.Proxy
通过上面的输出结果,我们发现,代理类的父类是Proxy
,也就意味着果然使用的是JDK
的动态代理。
测试CGLIB动态代理
好,测试完JDK
动态代理,我们开始测试CGLib
动态代理。我们前面说过,只有当需要代理的类没有实现接口时,Spring
才会使用CGLib
动态代理,于是我们定义一个GirlFriend
这个类的,不让它实现接口(她非人哉):
package com.hanserwei.entity;
import com.hanserwei.annotation.DynamicProxy;
import org.springframework.stereotype.Component;
@Component
public class GirlFriend {
@DynamicProxy
public void display() {
System.out.println("I love Hanserwei");
}
}
由于GirlFriend
没有实现接口,所以我们的测试方法也需要做一些修改。之前我们是通过Human.class
这个类型从Spring
容器中获取代理对象,但是现在,由于没有实现接口,所以我们不能再这么写了,而是要写成GirlFriend.class
,如下:
package com.hanserwei;
import com.hanserwei.config.SpringApplicationConfig;
import com.hanserwei.entity.GirlFriend;
import com.hanserwei.entity.Human;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringApplicationConfig.class);
// 注意,这里只能通过Human.class获取,而无法通过Student.class,因为在Spirng容器中,
// 因为使用JDK动态代理,Ioc容器中,存储的是一个类型为Human的代理对象
Human student = applicationContext.getBean(Human.class);
student.display();
// 输出代理类的父类,以此判断是JDK还是CGLib
System.out.println(student.getClass().getSuperclass());
System.out.println("=================================");
// 修改为GirlFriend类
GirlFriend girlFriend = applicationContext.getBean(GirlFriend.class);
girlFriend.display();
System.out.println(girlFriend.getClass().getSuperclass());
}
}
因为CGLib
动态代理是生成了GirlFriend
的一个子类,所以这个代理对象也是GirlFriend
类型(子类也是父类类型),所以可以通过GirlFriend.class
获取。输出如下:
before
我是Hanserwei!
after
class java.lang.reflect.Proxy
=================================
before
I love Hanserwei
after
class com.hanserwei.entity.GirlFriend
可以看到,AOP
成功生效,并且代理对象所属类的父类是GirlFriend
,验证了我们之前的说法。下面我们修改一下GirlFriend
类的定义,将display
方法加上final
修饰符,再看看效果:
package com.hanserwei.entity;
import com.hanserwei.annotation.DynamicProxy;
import org.springframework.stereotype.Component;
@Component
public class GirlFriend {
@DynamicProxy
// 加上final修饰,即无法被重写!
public final void display() {
System.out.println("I love Hanserwei");
}
}
此时的输出:
before
我是Hanserwei!
after
class java.lang.reflect.Proxy
=================================
I love Hanserwei
class com.hanserwei.entity.GirlFriend
可以看到,输出的父类仍然是GirlFriend
,也就是说Spring
依然使用了CGLib
生成代理。但是我们发现,我们为display
方法配置的环绕通知并没有执行,也就是代理类并没有为display
方法进行代理。这也验证了我们之前的说法,CGLib
无法代理final
方法,因为子类无法重写父类的final
方法。下面我们可以试着为GirlFriend
类加上final
修饰符,让他无法被继承,此时看看结果。
我这里尝试把GirlFriend
类直接变成final,这样就会抛异常,我大概猜一下,估计是底层代理它的时候,先生成它的子类,然后重写所有可以重写的方法,然后再看哪些方法被切入点标记,标记了的会被advice
增强。
强制Spring使用CGLIB
过上面的测试我们会发现,CGLib
的动态代理好像更加强大,而JDK
的动态代理却限制颇多。而且前面也提过,CGLib
的代理对象,执行代理方法的速度更快,只是生成代理类的效率较低。但是我们使用到的bean
大部分都是单例的,并不需要频繁创建代理类,也就是说CGLib
应该会更合适。但是为什么Spring
默认使用JDK
呢?这我也不太清楚,网上也没有找到相关的描述(如果有人知道,麻烦告诉我)。但是据说SpringBoot
现在已经默认使用CGLib
作为AOP
的实现了。
那我们可以强制Spring
使用CGLib
,而不使用JDK
的动态代理吗?答案当然是可以的。我们知道,如果要使用注解(@Aspect
)方式配置切面,则需要在xml
文件中配置下面一行开启AOP
:
<aop:aspectj-autoproxy />
如果我们希望只使用CGLib
实现AOP
,则可以在上面的这一行加点东西:
<!-- 将proxy-target-class配置设置为true -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
当然,如果我们是使用Java
类进行配置,比如说我们上面用到的SpringApplicationConfig
这个类,如果是通过这种方式配置,则强制使用CGLib
的方式如下:
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan(basePackages = "com.hanserwei")
public class SpringApplicationConfig {
}
如果我们是在xml文件中配置切面,则可以通过以下方式来强制使用CGLib
:
<!-- aop:config用来在xml中配置切面,指定proxy-target-class="true" -->
<aop:config proxy-target-class="true">
<!-- 在其中配置AOP -->
</aop:config>
总结
上面我们就对Spring
中AOP
的实现原理做了一个大致的介绍。归根到底,Spring AOP
的实现是通过动态代理,并且有两种实现方式,分别是JDK
动态代理和CGLib
动态代理。Spring
默认使用JDK
动态代理,只有在类没有实现接口时,才会使用CGLib
。
上面的内容若存在错误或者不足,欢迎指正或补充。也希望这篇博客对需要了解Spring AOP
的人有所帮助。