一、定义
AOP即面向切面编程,是一种设计思想,旨在把多个互不关系的类的非核心公共行为和核心行为进行分离。AOP是面向对象编程(OOP)的补充,传统的面向对象编程,若多个互不关系的类存在相同的行为,比如日志记录、权限校验等,开发者需要在这些类中编写许多重复的代码,造成大量的代码冗余,可复用性和可维护性较差,而AOP对OOP的补充,通过切点、通知、织入这些功能把这些公共行为和核心行为进行分离,从而大大提供代码的可复用性和可维护性。
Spring AOP是Spring的AOP实现,它是Spring的核心特性之一,它借用了AspectJ的一组注解,自己基于这些注解通过动态代理实现了AOP,使开发者能够轻松地将关注点集成到他们的应用程序中。
二、核心概念
1、AOP通用概念
(1)切面(Aspect)
切面是封装横切关注点的代码模块,由切入点和通知组成。在Spring AOP中,一个切面可以定义在什么时候、什么地方以及如何应用某种特定的行为到目标对象上。
-
切点(Pointcut)
-
连接点(Joinpoint)
-
切点用于定义横切关注点应用的时机和范围。它是一组连接点的集合,这些连接点共享相同的特性。切点的切点表达式execution()用于匹配连接点,从而确定哪些连接点应该接收通知。
切点表达式的认识
a、表达式的格式
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
具体解释:
- modifiers-pattern: 方法的访问修饰符,如 public、protected 等,可以省略。
- ret-type-pattern: 方法的返回类型,如 void、int 等。
- declaring-type-pattern: 方法所属的类的类型模式,可以使用通配符 * 匹配任意字符。
- name-pattern: 方法的名称模式,可以使用通配符 * 匹配任意字符。
- param-pattern: 方法的参数模式,包括参数类型和个数。
- throws-pattern: 方法抛出的异常类型。
b、举例
- 所有公共方法的执行
execution(public * *(..))
- 名称以 set 开头的所有方法的执行
execution(* set*(..))
- AccountService 接口定义的任何方法的执行
execution(* com.xyz.service.AccountService.*(..))
- service 包中定义的任何方法的执行
execution(* com.xyz.service.*.*(..))
连接点是代表应用程序执行过程中可以插入切面(Aspect)的地方,例如方法的调用、字段的访问等。在Spring AOP中,一个连接点总是代表一个方法的执行。连接点是AOP框架可以在其上 “织入” 切面的点。
-
通知(Advice)
通知定义了在切入点匹配时要执行的代码,它是增强应用到连接点上的行为。通知有多种类型,包括前置通知(Before Advice)、后置通知(After Advice) 、环绕通知(Around Advice) 、异常通知(After Throwing Advice) 和 返回通知(After Returning Advice) 。这些通知类型决定了增强在连接点上的执行顺序和方式。
通知的执行顺序:
a、无异常存在的执行顺序
环绕前置--> 普通前置--> 目标方法执行--> 普通返回 --> 普通后置--> 环绕返回 -->环绕后置
b、有异常存在的执行顺序
环绕前置--> 普通前置 --> 目标方法执行 -->普通异常 --> 普通后置
注意:异常通知与返回通知是互斥的,有异常无返回,有返回无异常
(2)目标对象
被一个或多个切面所通知的对象。也被称为被通知(advised)对象。由于Spring AOP是通过代理模式实现的,因此在运行时,目标对象总是用代理对象所包裹,所以目标对象也是被代理对象。
(3)织入(Weaving)
织入是将切面应用到目标对象并创建代理对象的过程。这是AOP框架在运行时或编译时完成的核心任务。
2、SpringAOP额外新增的概念
Advisor
Advisor包含一个切入点和通知,而Aspect虽然也可以包含切入点和通知,但是可以包含多个切入点和通知。
三、使用
下面两种方式都需要添加@EnableAspectJAutoProxy注解
1、通过AspectJ注解
(1)切面代码
@Component
@EnableAspectJAutoProxy
@Aspect
public class AspectTest {
@Pointcut("execution(* org.example.aop.*.*(..))")
public void pointcut(){}
@Before("pointcut()")
public void before(){
System.out.println("方法执行前");
}
@After("pointcut()")
public void after(){
System.out.println("方法执行后");
}
@AfterReturning("pointcut()")
public void afterReturning(){
System.out.println("方法返回后通知");
}
@AfterThrowing("pointcut()")
public void afterThrowing(){
// 异常通知与返回通知是互斥的,有异常无返回,有返回无异常
System.out.println("方法异常后通知");
}
@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("方法环绕通知前");
// 必须下面这行代码,不然除了环绕通知外的通知无法生效
joinPoint.proceed();
System.out.println("方法环绕通知后");
}
}
(2)目标对象代码
@Component
public class TestAopMethod {
public void test(){
System.out.println("I AM TEST METHOD");
}
}
(3)测试代码
@Autowired
TestAopMethod testAopMethod;
@Override
public void run(String... args) throws Exception {
testAopMethod.test();
}
执行结果:
2、通过Advisor
(1)编写切入点Pointcut
public class MyPointCut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
// 判断类是否匹配
return new MyClassFilter();
}
@Override
public MethodMatcher getMethodMatcher() {
// 判断方法是否匹配
return new MyMethodMatcher();
}
private class MyMethodMatcher implements MethodMatcher{
@Override
public boolean matches(Method method, Class<?> targetClass) {
Annotation[] annotations=method.getDeclaredAnnotations();
if(annotations==null||annotations.length==0)return false;
for(Annotation annotation:annotations){
if(annotation.annotationType()== MyAnnotation.class){
return true;
}
}
return false;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
private class MyClassFilter implements ClassFilter{
@Override
public boolean matches(Class<?> clazz) {
return AnnotationUtils.isCandidateClass(clazz,MyAnnotation.class);
}
}
}
(2)编写通知Advice
public class MyInterceptAdvice implements MethodInterceptor {
@Nullable
@Override
public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
System.out.println("MyInterceptAdvice invoke before");
return invocation.proceed();
}
}
(3)编写Advisor
@EnableAspectJAutoProxy
@Component
public class MyAdvisor implements PointcutAdvisor {
@Override
public Advice getAdvice() {
// 通知
return new MyInterceptAdvice();
}
@Override
public boolean isPerInstance() {
return false;
}
@Override
public Pointcut getPointcut() {
// 切入点
return new MyPointCut();
}
}
(4)编写目标类
public class TestAopMethod {
@MyAnnotation
public void test(){
System.out.println("I AM TEST METHOD");
}
}
(5)编写测试代码
public static void main(String[] args) {
AnnotationConfigApplicationContext context1=new AnnotationConfigApplicationContext(SysConfig.class);
TestAopMethod obj= (TestAopMethod) context1.getBean("testAopMethod");
obj.test();
}
执行结果:
四、原理
1、认识
2、详细过程调用
(1)前置环境:
有一个SysConfig类,添加了@ComponentScan(basePackages = "org.example.aop")和@Configuration注解。该配置类的作用是通过@ComponentScan注解扫描basePackages 包下所有声明了@Component注解的bean信息,比如切面类AspectTest
的bean信息,以及也通过@Bean配置目标类TestAopMethod的bean信息。而切面类AspectTest添加了@EnableAspectJAutoProxy注解和@Component注解。有一个main方法,方法内容是创建一个AnnotationConfigApplicationContext容器,并通过该容器的getBean方法获取TestAopMethod实例,最后调用该实例的test方法(该test方法就是切点,程序会在该方法前后进行增强)
(2)大体流程
启动程序,创建容器AnnotationConfigApplicationContext并初始化。在这个过程中,会先添加ConfigurationClassPostProcessor和入口配置类SysConfig的bean信息到容器的beanDefinitionMap中。接着会进行容器刷新。在这个刷新过程,有两个主要的方法,一个是invokeBeanFactoryPostProcessors方法,该方法会先找到ConfigurationClassPostProcessor的bean信息,然后调用该BeanFactoryPostProcessor的processConfigBeanDefinitions方法对入口配置类SysConfig进行解析,包括对@ComponentScan、@Import等常见注解解析,而启动Spring AOP功能的注解@EnableAspectJAutoProxy就是在此处解析,该注解真正解析的是@Import注解,里面配置了一个AspectJAutoProxyRegistrar类,会在解析之后,调用loadBeanDefinitionsFromRegistrars方法来执行AspectJAutoProxyRegistrar类的registerBeanDefinitions方法,会在这个registerBeanDefinitions里面向容器添加AnnotationAwareAspectJAutoProxyCreator的bean信息。另一个方法是finishBeanFactoryInitialization,该方法会对BeanDefinitionMap中非懒加载的bean信息通过调用容器的getBean方法进行实例化。这里主要介绍TestAopMethod的实例化,在getBean方法里面会对创建并已初始化好的TestAopMethod实例,调用初始化后applyBeanPostProcessorsAfterInitialization方法来为该实例创建代理对象,applyBeanPostProcessorsAfterInitialization方法会去调用AnnotationAwareAspectJAutoProxyCreator这个BeanProcessor的postProcessAfterInitialization方法,但是AnnotationAwareAspectJAutoProxyCreator未重写该方法,所以真正调用的是它的祖父类AbstractAutoProxyCreator的postProcessAfterInitialization方法,该方法的核心是使用wrapIfNecessary对目标进行包装。wrapIfNecessary方法会先通过getAdvicesAndAdvisorsForBean找到符合条件的切面类(声明了@Aspect注解)和Advisor接口实现类,该getAdvicesAndAdvisorsForBean的具体过程是先通过findCandidateAdvisors找到所有声明了@Aspect注解的切面类和Advisor接口实现类,然后通过findAdvisorsThatCanApply根据TestAopMethod的类名找到符合条件的切面类和Advisor接口实现类。接着,基于这些符合条件的类利用createProxy创建代理对象。这里会把TestAopMethod实例作为TargetSource参数传入createProxy方法,接着会在该方法内根据TargetSource创建ProxyFactory对象,调用ProxyFactory的getProxy方法获取代理对象,该getProxy方法会先根据TargetSource是否有实现接口来判断是使用JDK动态代理或者CGLIB动态代理方式创建对应的AopProxyFactory,之后使用AopProxyFactory来创建代理对象。最后把这个TestAopMethod实例放入一级缓存。
生成的的代理对象
五、问题
1、在某个项目中,有个包aop,在这个包下有3个文件,一个是切面类AspectTest,一个是配置类SysConfig,一个是目标类TestAopMethod。该配置类里通过@ComponentScan扫描AspectTest,以及通过@Bean注解声明TestAopMethod的bean信息。之后利用收到创建容器AnnotationConfigApplicationContext的方法获取TestAopMethod实例,并调用它的方法test,会出现TestAopMethod获取不到(即为空)的错误,若把SysConfig重新移入另一个包config,会发现不会报错,原因是为什么呢?