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(是否生成文档)。
抛出异常 ArithmeticException try-catch 捕获异常对象 处理逻辑 日志/回滚/重试 @注解 标记处理策略 反射解析 取得注解 → 执行处理 通过注解配置处理行为

原理机制

  • 异常抛出链:方法调用栈中抛出异常,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)读取注解并执行逻辑。

流程步骤:自定义注解实现异常处理

  1. 定义异常处理注解,如@ExceptionHandle,设置@Retention(RUNTIME)和@Target(METHOD)。
  2. 在需要统一处理异常的方法上添加该注解,并指定处理策略(如重试次数、降级方法)。
  3. 编写AOP切面或反射工具,拦截方法执行,catch异常后获取方法上的@ExceptionHandle注解。
  4. 根据注解配置执行自定义处理逻辑(如记录告警、调用补偿方法)。

案例说明

场景:模拟数据库操作失败,使用自定义注解实现自动重试。

// 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自定义条件。