Java开发经验——Throwable/Exception异常处理方式

摘要

文章主要探讨了 Java 开发中 Throwable 和 Exception 的异常处理方式。阿里巴巴 Java 开发手册规定,RPC 调用、二方包、动态代理类等场景推荐使用 Throwable,因为这些场景可能会出现类似 NoClassDefFoundError 这样的严重错误,使用 Throwable 可以防止遗漏。而在普通 Controller 代码中,推荐使用 Exception,因为使用 Throwable 可能会误吞 Error,而 Error 通常是 JVM 级别的严重问题,不应被业务代码处理。文章还总结了不同场景下使用 Throwable 和 Exception 的建议。

1. 阿里巴巴 Java 开发手册中的这条【强制】规则主要是针对 RPC 调用、二方包(第三方 SDK)、动态代理类 这些场景,而不是普通的 Controller 代码。

1.1. 为什么在 RPC、二方包、动态代理调用时需要使用 Throwable?

  • 这些方法通常涉及外部依赖 ,可能会抛出 未受检的 Error(如 NoClassDefFoundError、OutOfMemoryError)。
  • 业务代码需要确保,即使发生 Error,也能拦截到,避免影响整个应用的稳定性。

1.1.1. 示例: RPC 调用

kotlin 复制代码
try {
    RpcResponse response = remoteService.call(request);
    return response.getData();
} catch (Throwable t) {  // 这里使用 Throwable 兜底
    log.error("RPC 调用失败", t);
    return Response.error("远程服务调用失败");
}

如果远程服务抛出了 NoClassDefFoundError 或 OutOfMemoryError,捕获 Throwable 可以保证不会影响整个应用。

1.1.2. 示例:调用第三方SDK

php 复制代码
try {
    thirdPartyService.process();
} catch (Throwable t) {
    log.error("调用第三方服务异常", t);
}

如果第三方 SDK 发生 Error(如 ServiceConfigurationError),应用可以优雅地处理,而不会直接崩溃。

2. 在普通 Controller 代码中,应该使用 Exception

普通业务逻辑和 Controller 层代码,不应该捕获 Throwable ,而是应该捕获 Exception ,防止吞掉 Error 。

比如:

less 复制代码
@PostMapping("/queryPage/list")
public Response<Page<OrderListDTO>> queryPageList(@RequestBody OrderQueryPageRequest request) {
    try {
        checkOrderQueryPageRequest(request);
        Page<OrderListDTO> queryPageResult = orderService.list(request);
        return Response.success(queryPageResult);
    } catch (IllegalArgumentException e) {
        log.warn("[进件管理] 参数错误: {}", e.getMessage(), e);
        return Response.error("[进件管理] 参数错误:" + e.getMessage());
    } catch (Exception e) {
        log.error("[进件管理] 分页查询异常", e);
        return Response.error("[进件管理] 分页查询异常:" + e.getMessage());
    }
}

在 Controller 层, Exception 足够处理常见的业务异常 ,没有必要使用 Throwable,因为:

  • Throwable 会捕获 Error,但 Error 通常表示 JVM 级别的严重问题,不应该被业务代码处理。
  • Exception 已经足够处理大部分的 业务异常 和 运行时异常 (RuntimeException)。

3. Throwable/Exception异常处理方式总结

3.1. 什么时候用 Throwable?

场景 使用 Throwable 使用 Exception
RPC 调用(远程服务) ✅ 推荐 ,防止 NoClassDefFoundError ❌ 可能遗漏 Error
二方包(第三方 SDK) ✅ 推荐 ,防止 ServiceConfigurationError ❌ 可能遗漏 Error
动态代理调用(如 CGLib、JDK Proxy) ✅ 推荐 ,防止 AbstractMethodError ❌ 可能遗漏 Error
普通业务代码(如 Service、Controller) ❌ 不推荐 ,会误吞 Error ✅ 推荐,保证异常处理可控
Spring 全局异常处理( @ControllerAdvice ) ✅ 可以,兜底处理所有异常 ✅ 推荐

3.2. 异常处理的最终结论

在 Controller 代码中,应该使用 Exception

  • ❌ 不要 catch (Throwable t)
  • ✅ 推荐 catch (Exception e)

在调用 RPC、第三方 SDK、动态代理时,使用 Throwable

  • ✅ catch (Throwable t) { log.error("异常", t); }
  • 这样可以防止 Error 直接导致应用崩溃。

在全局异常处理( @ControllerAdvice )中,可以使用 Throwable

  • 防止 Error 影响整个应用 ,但 Controller 层本身仍然应该捕获 Exception。

4. 通用Spring全局异常处理类

4.1. 全局异常处理类

kotlin 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理业务异常(如参数错误、校验失败等)
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Response<String> handleIllegalArgumentException(IllegalArgumentException e) {
        log.warn("[参数异常] {}", e.getMessage(), e);
        return Response.error("参数错误:" + e.getMessage());
    }

    /**
     * 处理通用业务异常
     */
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Response<String> handleBusinessException(BusinessException e) {
        log.warn("[业务异常] {}", e.getMessage(), e);
        return Response.error("业务异常:" + e.getMessage());
    }

    /**
     * 处理所有运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Response<String> handleRuntimeException(RuntimeException e) {
        log.error("[系统异常] ", e);
        return Response.error("系统错误,请联系管理员");
    }

    /**
     * 兜底异常处理(Throwable),防止 Error 影响系统稳定性
     */
    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Response<String> handleThrowable(Throwable t) {
        log.error("[严重错误] ", t);
        return Response.error("系统发生严重错误,请稍后重试");
    }
}

4.1.1. 关键点说明

  1. @RestControllerAdvice
    • 作用于所有 @RestController,统一拦截异常并返回 JSON 响应。
    • 如果是 @ControllerAdvice,需要配合 @ResponseBody 才能返回 JSON。
  1. 异常分类处理
    • IllegalArgumentException:参数错误,如 Assert 失败、入参校验不通过。
    • BusinessException:自定义业务异常,代表业务逻辑失败(如订单状态异常)。
    • RuntimeException:所有运行时异常(NullPointerException、IndexOutOfBoundsException)。
    • Throwable(兜底):
      • 避免 Error(如 OutOfMemoryError)直接导致应用崩溃。
      • 但一般不应该在业务代码里捕获 Throwable。
  1. 日志级别
    • warn:业务异常,开发人员关注即可。
    • error:系统异常或 Throwable,需要运维排查。

4.2. 自定义业务异常类

scala 复制代码
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }

    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
}

4.3. 全局异常处理示例

less 复制代码
@RestController
@RequestMapping("/order")
public class OrderController {

    @GetMapping("/{id}")
    public Response<OrderDTO> getOrder(@PathVariable Long id) {
        if (id == null || id <= 0) {
            throw new IllegalArgumentException("订单 ID 不能为空或小于等于 0");
        }
        if (id == 999) {
            throw new BusinessException("订单不存在");
        }
        return Response.success(new OrderDTO(id, "测试订单"));
    }
}

4.3.1. 返回结果示例

4.3.1.1. 业务异常(参数错误)

请求:

sql 复制代码
GET /order/-1

返回:

css 复制代码
{
  "code": "ERROR",
  "message": "参数错误:订单 ID 不能为空或小于等于 0"
}
4.3.1.2. 业务异常(订单不存在)

请求:

sql 复制代码
GET /order/999

返回:

css 复制代码
{
  "code": "ERROR",
  "message": "业务异常:订单不存在"
}
4.3.1.3. 系统异常

如果代码出现 NullPointerException:

css 复制代码
{
  "code": "ERROR",
  "message": "系统错误,请联系管理员"
}

4.4. 如果没有全局统一处理。Controller 层要不要直接 catch 异常?

在 没有全局异常处理( @ControllerAdvice ) 的情况下,Controller 层需要自己 catch 异常 ,但要遵循以下 最佳实践:

4.5. 🚫 不推荐:直接不捕获

如果不 catch 异常,Spring MVC 默认会返回 500 Internal Server Error,但:

  • 前端无法区分是 业务异常 还是 系统异常。
  • 日志可能没有详细的错误信息,不利于排查问题。
  • 可能会暴露敏感信息(如 NullPointerException 可能会泄露内部字段结构)。

4.6. ✅ 推荐方案:在 Controller 层 catch 业务异常,但不处理系统异常

4.6.1. 示例:在 Controller 里手动捕获业务异常

less 复制代码
@PostMapping("/queryPage/list")
public Response<Page<OrderListDTO>> queryPageList(@RequestBody OrderQueryPageRequest request) {
    try {
        checkOrderQueryPageRequest(request);
        Page<OrderListDTO> queryPageResult = orderService.list(request);
        return Response.success(queryPageResult);
    } catch (IllegalArgumentException e) {
        // 业务异常(如参数校验失败)
        log.warn("[进件管理] 参数错误: {}", e.getMessage(), e);
        return Response.error("[进件管理] 参数错误:" + e.getMessage());
    } catch (BusinessException e) {
        // 自定义业务异常
        log.warn("[进件管理] 业务异常: {}", e.getMessage(), e);
        return Response.error("[进件管理] 业务异常:" + e.getMessage());
    } catch (Exception e) {
        // **系统异常** 直接抛出,不吞掉,避免影响排查
        log.error("[进件管理] 系统异常", e);
        throw e;  // 让 Spring 处理,避免误吞
    }
}

4.7. 🔥 关键点说明

  1. 业务异常(参数错误、业务失败)自己处理
    • IllegalArgumentException(参数错误)
    • BusinessException(自定义业务异常)
    • 返回友好的错误信息,不让前端看到堆栈信息。
  1. 系统异常( Exception )不直接处理
    • NullPointerException
    • IndexOutOfBoundsException
    • DatabaseException
    • 直接抛出,避免误吞 Error ,并确保日志完整。
  1. 日志级别
    • 业务异常 warn(开发关注)
    • 系统异常 error(运维关注)

4.8. ❌ 反面示例:全部 catch 但不抛出

lua 复制代码
catch (Exception e) {
    log.error("[进件管理] 查询异常:" + e.getMessage(), e);
    return Response.error("[进件管理] 查询失败");
}
  • 问题:
    1. 吞掉异常,后续代码不知道哪里出了问题。
    2. 所有异常都变成普通业务异常,影响监控和排查。
    3. Error 也被吞掉,可能导致 JVM 崩溃时没有日志。

4.9. 🚀 最佳方案

  1. 业务异常 在 Controller 层 catch 并返回友好信息。
  2. 系统异常 让 Spring 兜底,避免误吞(可以配合 @ControllerAdvice)。
  3. 错误日志要区分:
    • 业务异常 :warn(轻量级,不影响系统)
    • 系统异常 :error(需要关注和报警)

4.10. 💡 结论

4.10.1. ✅ 如果没有全局异常处理

  • Controller 层 需要 catch 业务异常,防止影响用户体验。
  • 系统异常 不要吞掉,应抛出给 Spring 处理。

4.10.2. 🚀 最佳做法

  • 业务异常(如 IllegalArgumentException、BusinessException)自己 catch 并返回友好信息。
  • 其他异常(如 NullPointerException、数据库异常)直接抛出,避免误吞。
  • 最终还是建议使用 @ControllerAdvice 统一管理,让代码更简洁!
代码位置 处理的异常 使用的异常类型 返回状态码
Controller 参数错误 IllegalArgumentException 400 BAD_REQUEST
Service 业务逻辑异常 BusinessException 400 BAD_REQUEST
全局异常处理 运行时异常 RuntimeException 500 INTERNAL_SERVER_ERROR
全局异常处理 未知错误 Throwable 500 INTERNAL_SERVER_ERROR

4.10.3. 最佳实践

  1. 业务代码中 只捕获 Exception,避免误吞 Error。
  2. 调用 RPC/第三方 SDK 时,建议捕获 Throwable 兜底,防止 Error 影响系统稳定。
  3. Controller 层不要直接 catch 异常,让全局异常处理器统一管理。
  4. 不同类型的异常,返回不同的 HTTP 状态码,方便前端或调用方识别。

博文参考

相关推荐
小满zs1 小时前
Go语言第十七章(反射)
后端·go
小满zs1 小时前
Go语言第十五章(JSON序列化)
后端·go
小满zs1 小时前
Go语言第十六章(自定义类型)
后端·go
金銀銅鐵2 小时前
[Java] 用 GUI 展示 EnumMap 增删键值对时的内部变化
java·后端·ai编程
学长毕设2 小时前
基于SpringBoot的上门洗车服务系统设计与实现
java·spring boot·后端
小溪学编程5 小时前
Spring @Bean 与 @Component 注解的区别详解
java·后端·spring
IT_陈寒5 小时前
Python的列表推导式用不好真的会爆内存你知道吗
前端·人工智能·后端
摸爬滚打的小李7 小时前
TomCat
后端
itmigrate7 小时前
Rust 里的所有权误区:不是不写 clone 就叫高性能
开发语言·后端·rust
码事漫谈7 小时前
Deepseek v5 来了
后端