乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@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) {
        // 异常后通知逻辑
    }
}

常用属性

属性类型说明
valueString切入点表达式,或引用 @Pointcut 方法名
pointcutString与 value 等价,用于替代 value 声明切点
throwingString将目标方法抛出的异常绑定到 Advice 方法的同名参数。若异常类型不匹配,绑定失败
argNamesString参数名称,用于绑定目标方法参数(通常可省略)

核心原理

@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,则可以接收所有异常。

上一页
@AfterReturning
下一页
@Around