在 Spring AOP 中,通知(advice)是切面的一部分,它定义了在何时、何地以及如何应用切面逻辑。通知可以在目标方法执行前、执行后或者在执行过程中(环绕通知)被执行。以下是几种常见的 AOP 通知类型:
-
前置通知(Before advice):
- 在目标方法执行之前执行。
- 可以用于执行一些前置逻辑,如权限检查、日志记录等。
- 使用
@Before
注解。
@Before("execution(* com.example.service.*.*(..))") public void beforeAdvice() { // 前置逻辑 }
-
后置通知(After returning advice):
- 在目标方法成功执行后执行。
- 可以用于处理方法返回值、日志记录等。
- 使用
@AfterReturning
注解。
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result") public void afterReturningAdvice(Object result) { // 后置逻辑,可以访问目标方法的返回值 }
-
异常通知(After throwing advice):
- 在目标方法抛出异常时执行。
- 用于处理异常、记录日志等。
- 使用
@AfterThrowing
注解。
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception") public void afterThrowingAdvice(Exception exception) { // 异常处理逻辑 }
-
后置通知(After advice):
- 无论目标方法成功执行与否,在方法执行后都会执行。
- 常用于资源清理等。
- 使用
@After
注解。
@After("execution(* com.example.service.*.*(..))") public void afterAdvice() { // 后置逻辑 }
-
环绕通知(Around advice):
- 在目标方法执行前和执行后都会执行。
- 提供最大的灵活性,可以完全控制目标方法的执行。
- 使用
@Around
注解,需要使用ProceedingJoinPoint
来手动控制目标方法的执行。
@Around("execution(* com.example.service.*.*(..))") public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 环绕通知前逻辑 Object result = proceedingJoinPoint.proceed(); // 执行目标方法 // 环绕通知后逻辑 return result; }
通过使用这些不同类型的通知,可以实现对目标方法在不同阶段的控制和处理。在实际应用中,可以根据需求选择合适的通知类型来实现特定的切面逻辑。
切入点表达式
通知语法中的切入点表达式(Pointcut Expression)使用 AspectJ 的语法,它允许你定义在哪些地方(哪些方法)应用通知。以下是一些常见的通知语法细节:
-
通配符
*
:*
表示匹配任意字符,通常用于匹配方法名、类名或包名中的部分内容。- 示例:
execution(* com.example.service.*.*(..))
: 匹配com.example.service
包下的所有类的所有方法。
-
两个点
..
:..
表示匹配任意数量的参数。- 示例:
execution(* com.example.service.*.*(..))
: 匹配com.example.service
包下的所有类的所有方法,无论方法参数的数量是多少。
-
全限定类名:
- 可以使用全限定类名来匹配指定的类。
- 示例:
execution(* com.example.service.MyService.*(..))
: 匹配com.example.service
包下的MyService
类的所有方法。
-
方法名匹配:
- 可以使用方法名来匹配特定的方法。
- 示例:
execution(* com.example.service.MyService.doSomething(..))
: 匹配MyService
类中的doSomething
方法。
-
参数类型匹配:
- 可以使用参数类型来匹配特定的方法。
- 示例:
execution(* com.example.service.MyService.someMethod(String, int))
: 匹配MyService
类中的someMethod
方法,该方法有一个String
类型和一个int
类型的参数。
-
组合使用:
- 可以通过逻辑运算符
&&
(与)、||
(或)、!
(非)来组合多个条件。 - 示例:
execution(* com.example.service.*.*(String) && args(myArg))
: 匹配包名为com.example.service
下的所有类的方法,方法参数为一个String
类型且值为myArg
。
- 可以通过逻辑运算符
-
注解匹配:
- 使用
@annotation
可以匹配被特定注解标注的方法。 - 示例:
@Before("@annotation(com.example.annotation.Loggable))"
: 在所有被@Loggable
注解标注的方法前执行前置通知。
- 使用
这些是一些常见的通知语法细节,根据实际需要,可以灵活组合使用这些元素来定义切入点表达式,从而实现对目标方法的精确匹配。
语法细节
- 用
*
号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限。 - 在包名的部分,一个
*
号只能代表包的层次结构中的一层,表示这一层是任意的。例如:*.Hello
匹配com.Hello
,不匹配com.atguigu.Hello
。 - 在包名的部分,使用
*..
表示包名任意、包的层次深度任意。 - 在类名的部分,类名部分整体用
*
号代替,表示类名任意。 - 在类名的部分,可以使用
*
号代替类名的一部分。例如:*Service
匹配所有名称以Service
结尾的类或接口。 - 在方法名部分,可以使用
*
号表示方法名任意。 - 在方法名部分,可以使用
*
号代替方法名的一部分。例如:*Operation
匹配所有方法名以Operation
结尾的方法。 - 在方法参数列表部分,使用
(..)
表示参数列表任意。 - 在方法参数列表部分,使用
(int,..)
表示参数列表以一个int类型的参数开头。 - 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的。
- 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的。
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符。例如:
execution(public int ..Service.(.., int))
正确。execution( int ..Service.*(.., int))
错误。
重用切入点表达式
在Spring AOP中,可以通过给切入点表达式命名并重用它们来提高代码的可维护性。这可以通过@Pointcut
注解来实现,将切入点表达式定义在一个方法中,然后在通知方法中引用这个方法。这样可以减少代码冗余,使代码更清晰和易读。以下是一个简单的示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class MyAspect {
// 定义切入点表达式
@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceMethods() {}
// 应用切入点表达式的前置通知
@Before("serviceMethods()")
public void beforeServiceMethods() {
System.out.println("Before executing service methods");
}
}
在不同切面中重用切入点表达式同样是可能的。通过将切入点表达式定义为一个独立的方法,并在不同的切面中引用它,可以实现切入点表达式的重用。以下是一个示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class MyAspect1 {
// 定义切入点表达式
@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceMethods() {}
// 应用切入点表达式的前置通知
@Before("serviceMethods()")
public void beforeServiceMethods() {
System.out.println("Before executing service methods in MyAspect1");
}
}
@Aspect
public class MyAspect2 {
// 引用另一个切面的切入点表达式
@Pointcut("com.example.aspect.MyAspect1.serviceMethods()")
private void serviceMethodsInMyAspect1() {}
// 应用切入点表达式的前置通知
@Before("serviceMethodsInMyAspect1()")
public void beforeServiceMethodsInMyAspect1() {
System.out.println("Before executing service methods in MyAspect2");
}
}
在上面的例子中,MyAspect2
切面引用了 MyAspect1
切面中定义的切入点表达式 serviceMethods()
。这样,两个切面都可以重用相同的切入点表达式,实现了代码的重用和可维护性。
需要注意的是,在不同的切面中引用切入点表达式时,需要使用全限定类名来指定切面的位置。在@Pointcut
注解中,使用com.example.aspect.MyAspect1.serviceMethods()
来引用MyAspect1
中的切入点表达式。
在上面的例子中,通过@Pointcut
注解定义了一个切入点表达式的方法 serviceMethods()
,该方法包含了匹配 com.example.service
包下的所有类的所有方法的表达式。然后,在前置通知方法 beforeServiceMethods()
中使用 @Before
注解引用了这个切入点表达式。
通过这种方式,如果切入点表达式需要修改,只需在一个地方进行修改,不需要在多个通知方法中重复定义相同的表达式。这提高了代码的可维护性,同时使切入点的定义更加集中和清晰。