声明通知 (Declaring Advice)
通知(Advice)与切入点表达式相关联,在切入点匹配的方法执行之前、之后或前后运行。切入点表达式既可以是内联切入点,也可以是对命名切入点的引用。
前置通知 (Before Advice)
你可以使用 @Before 注解在切面中声明前置通知。
以下示例使用内联切入点表达式:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
@Before("execution(* com.xyz.dao.*.*(..))")
fun doAccessCheck() {
// ...
}
}如果使用命名切入点,可以将上述示例重写如下:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
fun doAccessCheck() {
// ...
}
}后置返回通知 (After Returning Advice)
后置返回通知在匹配的方法执行正常返回时运行。你可以使用 @AfterReturning 注解来声明它。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("execution(* com.xyz.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
@AfterReturning("execution(* com.xyz.dao.*.*(..))")
fun doAccessCheck() {
// ...
}
}有时,你需要在通知正文中访问实际返回的值。你可以使用绑定返回值的 @AfterReturning 形式:
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="execution(* com.xyz.dao.*.*(..))",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ... 使用 retVal
}
}@Aspect
class AfterReturningExample {
@AfterReturning(
pointcut = "execution(* com.xyz.dao.*.*(..))",
returning = "retVal")
fun doAccessCheck(retVal: Any?) {
// ... 使用 retVal
}
}returning 属性中使用的名称必须与通知方法中的参数名称对应。当方法执行返回时,返回值将作为对应的参数值传递给通知方法。returning 子句还将匹配限制为仅返回指定类型(本例中为 Object,匹配任何返回值)的方法执行。
注意,使用后置返回通知时,无法返回完全不同的引用。
异常通知 (After Throwing Advice)
异常通知在匹配的方法执行通过抛出异常退出时运行。你可以使用 @AfterThrowing 注解来声明:
@Aspect
public class AfterThrowingExample {
@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
public void doRecoveryActions() {
// ...
}
}@Aspect
class AfterThrowingExample {
@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
fun doRecoveryActions() {
// ...
}
}通常,你希望通知仅在抛出给定类型的异常时运行,并且需要在通知正文中访问抛出的异常。使用 throwing 属性可以限制匹配并绑定异常参数:
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="execution(* com.xyz.dao.*.*(..))",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}@Aspect
class AfterThrowingExample {
@AfterThrowing(
pointcut = "execution(* com.xyz.dao.*.*(..))",
throwing = "ex")
fun doRecoveryActions(ex: DataAccessException) {
// ...
}
}throwing 子句还将匹配限制为仅抛出指定类型(本例中为 DataAccessException)异常的方法执行。@AfterThrowing 仅接收来自连接点(目标方法)本身的异常,而不处理来自同一切面的其他通知(如 @After)的异常。
后置最终通知 (After (Finally) Advice)
后置(最终)通知在匹配的方法执行退出(无论是正常返回还是抛出异常)时运行。使用 @After 注解声明。此通知必须准备好处理正常和异常返回条件,通常用于释放资源。
@Aspect
public class AfterFinallyExample {
@After("execution(* com.xyz.dao.*.*(..))")
public void doReleaseLock() {
// ...
}
}@Aspect
class AfterFinallyExample {
@After("execution(* com.xyz.dao.*.*(..))")
fun doReleaseLock() {
// ...
}
}注意,@After 类似于 try-catch 语句中的 finally 块。
环绕通知 (Around Advice)
最后一种是环绕通知。环绕通知在匹配方法的执行“周围”运行。它有机会在方法运行之前和之后执行工作,并决定方法何时、如何甚至是否真正运行。如果需要在方法执行前后以线程安全的方式共享状态(例如启动和停止计时器),通常使用环绕通知。
建议
请始终使用满足你要求的最小强度的通知形式。例如,如果前置通知就足够了,请不要使用环绕通知。
环绕通知通过 @Around 注解声明。方法应声明 Object 作为其返回类型,并且第一个参数必须是 ProceedingJoinPoint 类型。在通知正文中,必须调用 ProceedingJoinPoint 的 proceed() 方法,底层方法才会运行。
@Aspect
public class AroundExample {
@Around("execution(* com.xyz..service.*.*(..))")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// 执行前逻辑(如记录开始时间)
Object retVal = pjp.proceed();
// 执行后逻辑(如记录结束时间)
return retVal;
}
}@Aspect
class AroundExample {
@Around("execution(* com.xyz..service.*.*(..))")
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
// 执行前逻辑
val retVal = pjp.proceed()
// 执行后逻辑
return retVal
}
}⚠️ 重要
如果你将环绕通知方法的返回类型声明为 void,调用者将始终收到 null,从而忽略 proceed() 的任何结果。因此,建议环绕通知始终返回 Object 并在通常情况下返回 pjp.proceed() 的结果。
通知参数 (Advice Parameters)
访问当前 JoinPoint
任何通知方法都可以将 org.aspectj.lang.JoinPoint 声明为其第一个参数(环绕通知则是 ProceedingJoinPoint)。JoinPoint 提供了一些有用的方法:
getArgs(): 返回方法参数。getThis(): 返回代理对象。getTarget(): 返回目标对象。getSignature(): 返回被通知方法的描述。toString(): 打印被通知方法的有用描述。
将参数传递给通知
除了绑定返回值和异常,你还可以绑定方法参数。在 args 表达式中使用参数名代替类型名时,对应参数的值将在通知调用时传递。
@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
// ... 在此直接使用 account 对象
}@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
fun validateAccount(account: Account) {
// ...
}通过这种方式,args(account,..) 既起到了匹配限制的作用(要求第一个参数必须是 Account 实例),又起到了参数绑定的作用。
同样,也可以为注解绑定参数:
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)")
fun audit(auditable: Auditable) {
val code = auditable.value()
// ...
}参数名确定机制
Spring AOP 使用以下顺序来确定参数名称:
argNames属性:显式通过注解指定。- Kotlin 反射:如果是 Kotlin 代码。
- 标准 Java 反射:如果编译时开启了
-parameters标志(Java 8+ 推荐)。 - 切点推导:根据表达式推断参数名。
通知顺序 (Advice Ordering)
当多个通知要在同一个连接点运行时,Spring AOP 遵循与 AspectJ 相同的优先级规则。
- “进入”连接点时,优先级最高的通知先运行(例如两个
@Before,高优先级先运行)。 - “退出”连接点时,优先级最高的通知最后运行(例如两个
@After,高优先级后运行)。
如何控制优先级?
- 不同切面之间:让切面实现
Ordered接口或标注@Order注解。值越小,优先级越高。 - 同一切面内部:优先级顺序固定为:
@Around,@Before,@After,@AfterReturning,@AfterThrowing。但请注意,@After的行为类似于finally,实际上它会在同一切面的返回或异常通知之后执行。
补充教学
1. 核心五大通知执行顺序图 (Spring 5.x+)
理解通知的层级关系对调试至关重要:
Around (前半段开始)
Before
Target Method Execution
AfterReturning / AfterThrowing
After (Finally)
Around (后半段结束)2. ProceedingJoinPoint.proceed() 的进阶用法
在环绕通知中,你可以修改传递给目标方法的参数:
@Around("execution(* serialize(..))")
public Object tweakArguments(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
if (args.length > 0 && args[0] instanceof String) {
args[0] = ((String) args[0]).trim(); // 去除首尾空格
}
return pjp.proceed(args); // 带有修改后参数的调用
}3. 切记:不要拦截“核心基础设施”
避免编写拦截 Spring 核心 Bean(如 BeanPostProcessor 或 AOP 切面本身)的切入点。这可能导致死循环或 Bean 实例化过早的警告。切面的目标应该是你的业务 Service 或 DAO。
4. 异常处理的最佳实践
在 AfterThrowing 中,如果你捕获了异常并记录了日志,请记住 AOP 默认不会阻止异常继续向上抛出。如果你想在 AOP 层“吃掉”异常并返回默认值,必须使用 Around 通报,并手动 try-catch 住 pjp.proceed()。
5. JoinPoint vs ProceedingJoinPoint
JoinPoint:只读。可以查看方法名、参数,但无法控制执行流程。适用于Before,After等。ProceedingJoinPoint:可控。只能在Around中使用。它拥有proceed()方法,就像手中握着目标方法的“点火钥匙”。