1. AOP 切面 1.1. 切面的定义 Spring AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架提供的一种对OOP(Object-Oriented Programming,面向对象编程)的补充,是一种通过预编译方式和运行期动态代理实现程序功能的技术。它可以让我们将横切关注点(如日志记录、性能统计等)从纵向代码中解耦出来,以提高代码的模块化、可重用性和可维护性。
在Spring AOP中,切面(Aspect)是一个模块化的、跨越多个类的关注点的定义。比如一个日志切面可以定义日志记录的行为,在应用程序的各个模块中进行调用。切面由切点(Pointcut)和增强(Advice)组成。
切点 切点是一个表达式,用于匹配需要织入增强的目标方法。常用的表达式语言是AspectJ表达式,它可以匹配方法的访问修饰符、返回值类型、方法名等。
增强 定义了切面在切点匹配时所执行的具体行为,有以下几种类型:
前置增强(Before Advice):在目标方法执行之前执行。
后置增强(After Advice):在目标方法执行之后执行,无论是否产生异常。
返回增强(After Returning Advice):在目标方法正常返回之后执行。
异常增强(After Throwing Advice):在目标方法抛出异常时执行。
环绕增强(Around Advice):在目标方法执行前后执行。
织入(Weaving)是将切面应用到目标对象并创建代理对象的过程。Spring提供了三种织入方式:
编译时织入(Compile-time Weaving):在编译阶段,通过特定的编译器在编译期织入切面代码。
类加载时织入(Load-time Weaving):在类加载阶段,通过特定的类加载器在加载类时织入切面代码。
运行时织入(Runtime Weaving):在应用程序运行时,通过动态代理技术在运行期织入切面代码。
Spring AOP支持多种织入方式,其中最常用的是运行时织入。它通过使用动态代理技术,在目标对象和切面之间创建一个代理对象,对目标方法进行增强。
需要注意的是,Spring AOP仅支持对Spring管理的Bean进行切面增强,对于不属于Spring容器管理的Bean,增强是无效的。
1.2. 切面简单实现 首先需要引入aspect包
1 2 3 4 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > </dependency >
然后创建切面类,并加入容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component @Aspect public class MyAspect { @Pointcut("execution(* com.kewen.learning.spring.tool.aspect.AspectService.*(..))") public void aspect () {} @Around("aspect()") public Object around (ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("around 前置" ); Object proceed = joinPoint.proceed(); System.out.println("around 后置" ); return proceed; } }
1.3. 切面详解 1.3.1. 增强注解 包括前置增强(@Before
)、返回增强(@AfterReturning
)、异常增强(@AfterThrowing
)、后置增强(@After
)和环绕增强(@Around
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Aspect @Component public class LogAspect { @Pointcut("execution(public * com.example.demo.service.*.*(..))") public void pointcut () { } @Before("pointcut()") public void before (JoinPoint joinPoint) { System.out.println("Before Advice: " + joinPoint.getSignature().getName()); } @AfterReturning(pointcut = "pointcut()", returning = "result") public void afterReturning (JoinPoint joinPoint, Object result) { System.out.println("After Returning Advice: " + joinPoint.getSignature().getName() + ", result: " + result); } @AfterThrowing(pointcut = "pointcut()", throwing = "exception") public void afterThrowing (JoinPoint joinPoint, Throwable exception) { System.out.println("After Throwing Advice: " + joinPoint.getSignature().getName() + ", exception: " + exception.getMessage()); } @After("pointcut()") public void after (JoinPoint joinPoint) { System.out.println("After Advice: " + joinPoint.getSignature().getName()); } @Around("pointcut()") public Object around (ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("Around Before: " + joinPoint.getSignature().getName()); Object result = joinPoint.proceed(); System.out.println("Around After: " + joinPoint.getSignature().getName() + ", result: " + result); return result; } }
1.3.2. 切面表达式解析 切面表达式有两种通配符,一种是.位于路径中或方法中,一种是*用以表示任意类型
com.example.HelloService.hello(..)
用以匹配hello方法,不区分重载(..匹配任意参数)
com.example.*.hello(..)
用以匹配example包下所有类的hello方法(*匹配任意单个类或单个包名)
com.example..*.hello(..)
用以匹配example及子包下所有类的hello方法(..匹配多层路径)
com.example..*(..)
用以匹配example及子包下所有类的所有方法(..匹配多层路径和任意参数,*匹配单个方法名)
com.example..(..)
此种是错误的,..
不能匹配方法名
1.3.2.1. execution
表达式 execution
是用来匹配方法执行的切点表达式,它是最主要的切点匹配器。例如:
execution(* set*(..))
:这个表达式将会匹配所有以 “set” 开头的方法。”“ 表示任何返回类型,”set “ 表示所有以 “set” 开头的方法,”..” 表示任何参数。
execution(* com.example.ClassName.methodName(..))
:这个表达式将会匹配 com.example.ClassName
类的 methodName
方法,无论这个方法接受什么参数。
execution(* com.example.*.*(..))
:这个表达式将会匹配 com.example
包下的所有类的所有方法。
execution(* *(..))
:这个表达式将会匹配所有的方法,无论是哪个类的方法。
1 2 3 4 5 6 @Component @Aspect public class MyAspect { @Pointcut("execution(* com.kewen.learning.spring.tool.aspect.AspectService.*(..))") public void aspect () {} }
1.3.2.2. within
表达式 within
是用来匹配特定的路径,用法与execution
类似,只是within
不关注返回值和方法,只关注到包或类。
within(com.example.ClassName)
:这个表达式将会匹配 com.example.ClassName
类内的所有方法。
within(com.example.*)
:这个表达式将会匹配 com.example
包内的所有方法。
within(com.example..*)
:这个表达式将会匹配 com.example
包及其子包内的所有方法。
1.3.2.3. this
和 target
表达式 this
和 target
主要用来在切面中引入被代理的对象,以便拿到其中的属性或方法进一步处理。 使用 this
匹配的是当前实例化的对象(即代理之后了的对象),而 target
匹配的是被代理的目标对象(原对象)。
例:
1 2 3 4 5 6 7 8 9 10 11 12 import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;@Aspect @Component public class UserLoggingAspect { @Before("execution(* com.kewen.learning.spring.tool.aspect.AspectService.*(..)) && this(thisAspectService) &&target(targetAspectService)") public void before1 (JoinPoint joinPoint,AspectService thisAspectService,AspectService targetAspectService) { System.out.println(thisAspectService.getClass().getName()); System.out.println(thisAspectService.getClass().getName()); } }
如果没有this(thisAspectService)
,则thisAspectService
对象无法引入,会报错
1.3.2.4. args
表达式 args
是用来匹配方法参数为指定类型的执行方法的。
通配符(*):可以使用通配符作为参数类型的占位符,例如args(*)
将匹配任意参数类型的方法。
单个参数类型:可以指定单个参数类型,例如args(java.lang.String)
将匹配具有String类型参数的方法。
多个参数类型:可以指定多个参数类型,使用逗号分隔,例如args(java.lang.String, java.lang.Integer)
将匹配具有String类型和Integer类型参数的方法。
包含子类型:可以使用”+”前缀来指示匹配参数类型及其子类型,例如args(+java.lang.Number)
将匹配具有Number类型及其子类型参数的方法。
排除特定类型:可以使用”!”前缀来排除特定类型,例如args(!java.lang.Boolean)
将排除具有Boolean类型参数的方法。
请注意,args表达式只匹配方法调用的参数类型,不考虑方法的返回类型、方法的目标对象等因素。因此,它通常与其他切点表达式组合使用,以更准确地选择切面建议的目标方法。
例:
1 2 3 4 5 6 7 8 @Aspect @Component public class MyAspect { @Before("execution(* com.example.service.MyService.*(..)) && args(java.lang.String)") public void beforeAdvice (JoinPoint joinPoint) { } }
1.3.2.5. @annotation
表达式 @annotation
用来匹配方法上的注解。
1 2 3 4 5 6 7 @Aspect public class MyAspect { @Before("@annotation(org.springframework.transaction.annotation.Transactional)") public void beforeAdvice (JoinPoint joinPoint) { } }
值得注意的是,@annotation
只匹配直接在方法上声明的注解,而不包括继承自父类或接口的注解。如果想要匹配继承的注解,可以使用within
表达式结合@annotation
来定义更准确的切点。
1 2 3 4 5 6 7 8 @Aspect public class MyAspect { @Before("@within(com.example.annotation.MyAnnotation) && @annotation(org.springframework.transaction.annotation.Transactional)") public void beforeAdvice (JoinPoint joinPoint) { } }
1.3.2.6. @within
表达式 @within
用来匹配类上的注解,它支持匹配类继承上的注解 如
1 2 3 4 5 6 7 8 9 @Aspect public class MyAspect { @Before("@within(org.springframework.stereotype.Controller)") public void beforeAdvice (JoinPoint joinPoint) { } }
1.3.2.7. @target
@target
是用来匹配在目标对象上声明的注解。 和@within
的区别是 @within
可以执行继承的,而@target
只能支持对象本身的,不支持继承的
1 2 3 4 5 6 7 8 9 @Aspect public class MyAspect { @Before("@target(org.springframework.stereotype.Service)") public void beforeAdvice (JoinPoint joinPoint) { } }
1.3.2.8. @args
@args
是用来匹配传入的参数带有指定注解的方法的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @MyAnnotation public class MyClass { public void method1 () { } public void method2 () { } } @Aspect public class MyAspect { @Before("@target(com.example.annotation.MyAnnotation)") public void beforeTargetAdvice (JoinPoint joinPoint) { } @Before("@within(com.example.annotation.MyAnnotation)") public void beforeWithinAdvice (JoinPoint joinPoint) { } }