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(如 NoClassDefFoundErrorOutOfMemoryError)。
  • 业务代码需要确保,即使发生 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("远程服务调用失败");
}

如果远程服务抛出了 NoClassDefFoundErrorOutOfMemoryError,捕获 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:所有运行时异常(NullPointerExceptionIndexOutOfBoundsException)。
    • 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. 🚀 最佳做法

  • 业务异常(如 IllegalArgumentExceptionBusinessException)自己 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 状态码,方便前端或调用方识别。

博文参考

相关推荐
一个热爱生活的普通人16 分钟前
Gin与数据库:GORM集成实战
后端·go
用户02731754117919 分钟前
dig 命令深入学习
linux·后端
林川的邹24 分钟前
数据权限框架(easy-data-scope)
后端·程序员·架构
用户945085191249226 分钟前
装饰者模式?No!少生优生?Yes!
后端·设计模式
Achou.Wang1 小时前
go语言中空结构体
开发语言·后端·golang
Asthenia04121 小时前
从源码看 MyBatis-Plus 与 Spring 的 DataSourceTransactionManager 有没有直接关联?
后端
炬火初现1 小时前
Go语言的基础类型
开发语言·后端·golang
卑微小文1 小时前
国内金融资讯平台信息聚合:代理 IP 打破信息孤岛
后端
Asthenia04121 小时前
MyBatis-Plus 查询构建实战:eq/between/in/or/like likeLeft likeRight/gte和lte/动态条件
后端