@AfterThrowing
定义与作用
@AfterThrowing 声明异常后通知(After Throwing Advice),在目标方法抛出异常后触发。与 @After 不同,@AfterThrowing 只在方法抛出异常时执行,正常返回时不会执行。它的核心优势是可以访问目标方法抛出的异常对象,适用于异常告警、错误日志记录、失败统计、补偿操作等场景。
关键特性:
- 只在目标方法抛出异常时执行,正常返回时不执行
- 可以通过
throwing属性将异常对象绑定到 Advice 方法的参数 - 无法阻止异常的传播,即使
@AfterThrowing中处理了异常,原异常仍会继续抛给调用方(除非在@AfterThrowing中抛出新异常覆盖原异常,但不推荐) - 可以访问
JoinPoint对象获取方法上下文
适用位置与常用属性
适用位置
@AfterThrowing 标注在切面类的方法上,表示该方法是一个异常后通知。该方法必须位于 @Aspect 切面类内部。
@Aspect
@Component
public class ExceptionAspect {
@AfterThrowing(pointcut = "execution(* com.feixiang.service.*.*(..))", throwing = "ex")
public void handleException(JoinPoint joinPoint, Exception ex) {
// 异常后通知逻辑
}
}
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | String | 切入点表达式,或引用 @Pointcut 方法名 |
pointcut | String | 与 value 等价,用于替代 value 声明切点 |
throwing | String | 将目标方法抛出的异常绑定到 Advice 方法的同名参数。若异常类型不匹配,绑定失败 |
argNames | String | 参数名称,用于绑定目标方法参数(通常可省略) |
核心原理
@AfterThrowing 在代理链中的执行位置
@AfterThrowing 在目标方法抛出异常后、异常传播给调用方前执行:
throwing 属性绑定机制
绑定规则:
throwing = "ex"表示将异常对象绑定到 Advice 方法中名为ex的参数- Advice 方法中
ex参数的类型必须是目标方法抛出异常类型的父类或相同类型 - 如果
throwing指定的名称在 Advice 方法中找不到同名参数,启动时不会报错,但无法注入异常 - 如果 Advice 方法参数类型与抛出的异常类型不兼容(例如目标抛
NullPointerException但参数声明为SQLException),参数值为null
完整示例:飞翔科技学生管理系统异常告警
场景简述
广州飞翔科技公司的运维工程师李眉负责监控学生成绩管理系统的稳定性。她要求:当 Service 层方法抛出异常时,必须立即记录错误日志,并将关键异常信息发送到运维告警群。后端开发小崔决定使用 @AfterThrowing 实现统一的异常处理,避免在每个业务方法的 catch 块中重复编写告警代码。
操作前代码/配置
在未使用 @AfterThrowing 之前,小崔在每个 Service 方法中手动处理异常:
@Service
public class StudentScoreService {
private final ScoreDao scoreDao;
public StudentScoreService(ScoreDao scoreDao) {
this.scoreDao = scoreDao;
}
public Score queryScore(Long studentId) {
try {
if (studentId == null) {
throw new IllegalArgumentException("学号不能为 null");
}
return scoreDao.findByStudentId(studentId);
} catch (Exception e) {
// 重复!每个方法都要写异常处理
System.err.println("[ERROR] 查询成绩失败,学号=" + studentId + ",异常=" + e.getMessage());
sendAlert("成绩查询异常:" + e.getMessage()); // 发送告警
throw e; // 继续抛出
}
}
public void updateScore(Long studentId, Double newScore) {
try {
if (newScore < 0 || newScore > 100) {
throw new IllegalArgumentException("成绩必须在 0-100 之间");
}
scoreDao.updateScore(studentId, newScore);
} catch (Exception e) {
// 重复!异常处理再次出现在业务方法中
System.err.println("[ERROR] 更新成绩失败,学号=" + studentId + ",异常=" + e.getMessage());
sendAlert("成绩更新异常:" + e.getMessage());
throw e;
}
}
private void sendAlert(String message) {
// 模拟发送告警到钉钉/企业微信
System.out.println("[ALERT] 发送告警:" + message);
}
}
问题:异常处理代码高度重复,业务逻辑被 try-catch 块淹没。如果李眉要求增加"记录异常堆栈到 ELK",所有方法都要修改。
使用该注解的完整代码
第一步:定义异常告警切面
@Aspect
@Component
public class ExceptionAlertAspect {
@Pointcut("execution(* com.feixiang.student.service.*.*(..))")
public void serviceLayer() {}
// 捕获所有异常,记录错误日志并发送告警
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void handleException(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
// 构建错误信息
String errorMsg = String.format(
"[飞翔科技-异常] 类=%s, 方法=%s, 参数=%s, 异常类型=%s, 异常信息=%s",
className, methodName, Arrays.toString(args),
ex.getClass().getSimpleName(), ex.getMessage()
);
// 记录错误日志
System.err.println(errorMsg);
// 发送运维告警
sendAlert(errorMsg);
// 记录异常统计(实际项目中可写入 Prometheus / Micrometer)
recordExceptionMetric(className, methodName, ex.getClass().getSimpleName());
}
// 专门处理 IllegalArgumentException(参数校验失败)
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void handleIllegalArgument(JoinPoint joinPoint, IllegalArgumentException ex) {
String methodName = joinPoint.getSignature().getName();
System.err.println("[飞翔科技-参数异常] 方法 " + methodName + " 参数校验失败:" + ex.getMessage());
}
// 专门处理数据库访问异常
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void handleDataAccess(JoinPoint joinPoint, DataAccessException ex) {
String methodName = joinPoint.getSignature().getName();
System.err.println("[飞翔科技-数据库异常] 方法 " + methodName + " 数据库操作失败");
// 数据库异常升级告警级别
sendCriticalAlert("数据库异常,请立即检查:" + methodName);
}
private void sendAlert(String message) {
System.out.println("[飞翔科技-告警] 发送普通告警:" + message);
}
private void sendCriticalAlert(String message) {
System.out.println("[飞翔科技-告警] 发送紧急告警:" + message);
}
private void recordExceptionMetric(String className, String methodName, String exceptionType) {
System.out.println("[飞翔科技-监控] 记录异常指标:" + className + "." + methodName + " -> " + exceptionType);
}
}
第二步:纯净的业务 Service
@Service
public class StudentScoreService {
private final ScoreDao scoreDao;
public StudentScoreService(ScoreDao scoreDao) {
this.scoreDao = scoreDao;
}
public Score queryScore(Long studentId) {
// 纯粹的业务逻辑,无任何异常处理代码
if (studentId == null) {
throw new IllegalArgumentException("学号不能为 null");
}
return scoreDao.findByStudentId(studentId);
}
public void updateScore(Long studentId, Double newScore) {
// 纯粹的业务逻辑
if (newScore < 0 || newScore > 100) {
throw new IllegalArgumentException("成绩必须在 0-100 之间");
}
scoreDao.updateScore(studentId, newScore);
}
public void deleteScore(Long studentId) {
// 纯粹的业务逻辑
scoreDao.deleteByStudentId(studentId);
}
}
第三步:配置类
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
操作后运行结果及分析
测试代码:
public class AfterThrowingDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
StudentScoreService service = ctx.getBean(StudentScoreService.class);
// 测试参数异常
System.out.println("=== 测试非法参数 ===");
try {
service.queryScore(null);
} catch (Exception e) {
System.err.println("客户端捕获异常:" + e.getMessage());
}
System.out.println("\n=== 测试成绩范围异常 ===");
try {
service.updateScore(2024001L, 150.0);
} catch (Exception e) {
System.err.println("客户端捕获异常:" + e.getMessage());
}
System.out.println("\n=== 测试正常执行(@AfterThrowing 不触发)==="");
service.deleteScore(2024001L);
ctx.close();
}
}
控制台输出:
=== 测试非法参数 ===
[飞翔科技-参数异常] 方法 queryScore 参数校验失败:学号不能为 null
[飞翔科技-异常] 类=StudentScoreService, 方法=queryScore, 参数=[null], 异常类型=IllegalArgumentException, 异常信息=学号不能为 null
[飞翔科技-告警] 发送普通告警:[飞翔科技-异常] 类=StudentScoreService, 方法=queryScore, 参数=[null], 异常类型=IllegalArgumentException, 异常信息=学号不能为 null
[飞翔科技-监控] 记录异常指标:StudentScoreService.queryScore -> IllegalArgumentException
客户端捕获异常:学号不能为 null
=== 测试成绩范围异常 ===
[飞翔科技-参数异常] 方法 updateScore 参数校验失败:成绩必须在 0-100 之间
[飞翔科技-异常] 类=StudentScoreService, 方法=updateScore, 参数=[2024001, 150.0], 异常类型=IllegalArgumentException, 异常信息=成绩必须在 0-100 之间
[飞翔科技-告警] 发送普通告警:[飞翔科技-异常] 类=StudentScoreService, 方法=updateScore, 参数=[2024001, 150.0], 异常类型=IllegalArgumentException, 异常信息=成绩必须在 0-100 之间
[飞翔科技-监控] 记录异常指标:StudentScoreService.updateScore -> IllegalArgumentException
客户端捕获异常:成绩必须在 0-100 之间
=== 测试正常执行(@AfterThrowing 不触发)===
[业务] 删除学生 2024001 的成绩
分析:
@AfterThrowing在queryScore(null)和updateScore(2024001L, 150.0)抛出异常后触发throwing = "ex"将异常对象绑定到 Advice 方法的ex参数- 多个
@AfterThrowing可以针对不同类型的异常做不同处理(IllegalArgumentException、DataAccessException等) deleteScore正常执行,@AfterThrowing没有触发,证明它只在异常时执行- 业务代码完全纯净,异常处理逻辑统一由切面维护
易错场景与面试考点
易错场景一:@AfterThrowing 中试图吞掉异常
反例:
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void swallowException(Exception ex) {
// 错误!这不会阻止异常传播
System.err.println("异常已处理:" + ex.getMessage());
// 没有 throw 语句,但原异常仍会继续传播
}
现象:即使 @AfterThrowing 中"处理"了异常,客户端仍然会收到原异常。
原因:@AfterThrowing 的设计目的是"观察"异常,而非"处理"异常。原异常在 @AfterThrowing 执行完毕后继续传播。
正确做法:若需吞掉异常或转换为其他异常,使用 @Around:
@Around("serviceLayer()")
public Object handleException(ProceedingJoinPoint joinPoint) {
try {
return joinPoint.proceed();
} catch (Exception e) {
// 可以吞掉异常,返回默认值
System.err.println("异常已捕获:" + e.getMessage());
return null; // 返回默认值,异常不再传播
}
}
易错场景二:throwing 属性名称与 Advice 方法参数名不一致
反例:
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logError(JoinPoint joinPoint, Exception error) { // 错误!参数名是 error,不是 ex
System.out.println("异常:" + error); // error 始终为 null
}
正确做法:
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logError(JoinPoint joinPoint, Exception ex) { // 正确:参数名与 throwing 一致
System.out.println("异常:" + ex.getMessage());
}
面试高频考点
考点一:@AfterThrowing 和 @After 在异常场景下的区别?
两者都在目标方法抛出异常后执行,但
@AfterThrowing只在异常时执行且可以访问异常对象;@After在正常和异常时都会执行但无法访问异常对象。如果方法正常返回,@AfterThrowing不执行而@After执行。实际项目中常将两者配合使用:@AfterThrowing负责异常告警,@After负责资源释放。
考点二:@AfterThrowing 能否阻止异常传播?
不能。
@AfterThrowing的设计目的是观察异常(如记录日志、发送告警),而非处理异常。即使@AfterThrowing方法正常完成,原异常仍会继续传播给调用方。如果@AfterThrowing自身抛出异常,则会覆盖原异常。若要阻止异常传播或转换异常,必须使用@Around,在try-catch中捕获异常并决定后续行为。
考点三:@AfterThrowing 的 throwing 属性与异常类型匹配规则?
throwing = "ex"要求 Advice 方法中有名为ex的参数。Spring 会将目标方法抛出的异常注入该参数,但要求参数类型与异常类型兼容。例如目标方法抛出IllegalArgumentException,Advice 方法参数声明为RuntimeException或Exception可以接收,但声明为SQLException则无法匹配,参数值为null。如果声明为Throwable,则可以接收所有异常。