2. Java异常处理与注解
问题背景
在Java后端开发中,异常处理和注解是两套不可或缺的机制。异常处理确保程序在遇到错误时能优雅降级或恢复,注解则提供声明式元数据标记,简化配置与代码增强。面试高频考点聚焦于:异常分类体系、try-catch-finally执行顺序、受检与非受检异常选择、自定义注解实现、注解处理器(APT/反射)原理,以及二者结合的典型场景(如Spring的@Transactional事务回滚)。
核心概念
- 异常(Exception):程序运行期间出现的非正常情况,Java将异常封装为对象,通过抛出(throw)和捕获(catch)机制传递控制权。
- 错误(Error):JVM级严重问题,无需捕获,如OutOfMemoryError、StackOverflowError。
- 受检异常(Checked Exception):编译器强制处理,必须捕获或声明抛出,如IOException、SQLException。
- 非受检异常(Unchecked Exception):运行时异常(RuntimeException及其子类),编译器不强制处理,如NullPointerException、IllegalArgumentException。
- 注解(Annotation):一种元数据形式,使用
@interface声明,可标记在类、方法、字段上,通过反射或编译期处理器提取信息。 - 元注解:用于修饰注解的注解,如@Retention(保留策略)、@Target(作用目标)、@Inherited(是否继承)、@Documented(是否生成文档)。
原理机制
- 异常抛出链:方法调用栈中抛出异常,JVM逐层寻找匹配的catch块;若未找到,线程终止并打印堆栈。
- finally块执行:无论是否发生异常,finally块都会执行(除非JVM崩溃或线程被kill)。注意finally中return会覆盖try/catch中的return值。
- 字节码层面的异常表:编译器将异常处理信息写入Class文件的异常表中,每个catch块对应一个handler,包含起始PC、结束PC、跳转PC。
- 注解保留策略:
RetentionPolicy.SOURCE:仅源码阶段,编译丢弃(如@Override)。RetentionPolicy.CLASS:保留到.class文件,但运行时不可反射。RetentionPolicy.RUNTIME:运行时保留,可通过反射获取。
- 注解处理方式:
- 编译期处理(APT):通过AbstractProcessor在编译时生成代码或校验。
- 运行时处理:通过反射API(
getAnnotation、getDeclaredAnnotations)读取注解并执行逻辑。
流程步骤:自定义注解实现异常处理
- 定义异常处理注解,如
@ExceptionHandle,设置@Retention(RUNTIME)和@Target(METHOD)。 - 在需要统一处理异常的方法上添加该注解,并指定处理策略(如重试次数、降级方法)。
- 编写AOP切面或反射工具,拦截方法执行,catch异常后获取方法上的
@ExceptionHandle注解。 - 根据注解配置执行自定义处理逻辑(如记录告警、调用补偿方法)。
案例说明
场景:模拟数据库操作失败,使用自定义注解实现自动重试。
// 1. 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
int times() default 3;
long interval() default 1000; // 毫秒
}
// 2. 使用注解
public class OrderService {
@Retry(times = 2, interval = 500)
public boolean createOrder(Order order) {
// 可能抛出SQLException
return orderDao.insert(order) > 0;
}
}
// 3. 反射调用并实现重试
public static Object invokeWithRetry(Object obj, Method method, Object... args) {
Retry retry = method.getAnnotation(Retry.class);
if (retry == null) {
return method.invoke(obj, args);
}
int maxTimes = retry.times();
long interval = retry.interval();
for (int i = 0; i < maxTimes; i++) {
try {
return method.invoke(obj, args);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (i == maxTimes - 1) {
throw new RuntimeException("重试耗尽", cause);
}
Thread.sleep(interval);
}
}
return null;
}
表格总结:异常与注解核心对比
| 维度 | 异常处理 | 注解机制 |
|---|---|---|
| 核心目的 | 控制错误传递,保证程序稳定性 | 声明式元数据,减少重复代码 |
| 生命周期 | 运行时发生 | 编译/运行时保留(取决于@Retention) |
| 关键技术 | try-catch-finally、throws、throw | 元注解、注解处理器、反射 |
| 常见错误 | 吞异常、滥用受检异常、finally中return | 保留策略错误导致反射拿不到、未处理继承 |
| 协同场景 | Spring @Transactional:通过注解声明事务边界,异常触发回滚 | |
误区提醒
- 误区一:认为finally一定会执行所有代码。若try或catch中有System.exit()或JVM崩溃,finally不会执行。
- 误区二:将受检异常用于业务逻辑分支。受检异常应留给可恢复的外部错误,非受检异常更适合编程错误。
- 误区三:注解保留策略只设为CLASS或SOURCE却期望运行时反射获取,导致空值。
- 误区四:忽略异常性能开销。频繁抛出和捕获异常会严重影响性能,应优先使用状态码或Optional。
实践清单
- ✅ 优先使用非受检异常(如IllegalArgumentException)替代受检异常,除非调用方必须处理(如文件IO)。
- ✅ catch块中避免吞掉异常,至少记录日志(log.error("xxx", e))。
- ✅ finally块中尽量不写return语句,避免覆盖异常。
- ✅ 自定义注解时明确指定@Retention(RUNTIME)以便反射使用,并合理设置@Target限定作用范围。
- ✅ 使用AOP(如Spring @Around)统一处理异常,配合自定义注解实现声明式容错。
- ✅ 对第三方库异常进行包装,提供更友好的业务异常类(继承RuntimeException)。
- ✅ 在敏感操作(如数据库调用)上使用重试注解,提高系统韧性。
面试提示:面试官常问“Spring如何通过@Transactional实现异常回滚?”回答要点:AOP代理拦截方法调用,捕捉到RuntimeException或Error(默认)后调用TransactionAspectSupport的completeTransactionAfterThrowing,执行rollback。可通过rollbackFor和noRollbackFor自定义条件。