摘要
文章主要探讨了 Java 开发中 Throwable 和 Exception 的异常处理方式。阿里巴巴 Java 开发手册规定,RPC 调用、二方包、动态代理类等场景推荐使用 Throwable,因为这些场景可能会出现类似 NoClassDefFoundError 这样的严重错误,使用 Throwable 可以防止遗漏。而在普通 Controller 代码中,推荐使用 Exception,因为使用 Throwable 可能会误吞 Error,而 Error 通常是 JVM 级别的严重问题,不应被业务代码处理。文章还总结了不同场景下使用 Throwable 和 Exception 的建议。
1. 规则主要是针对 RPC 调用、二方包(第三方 SDK)、动态代理类 这些场景,而不是普通的 Controller 代码。
1.1. 为什么在 RPC、二方包、动态代理调用时需要使用 Throwable
?
- 这些方法通常涉及外部依赖,可能会抛出 未受检的
Error
(如NoClassDefFoundError
、OutOfMemoryError
)。 - 业务代码需要确保,即使发生
Error
,也能拦截到,避免影响整个应用的稳定性。
1.1.1. 示例: RPC 调用
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
try {thirdPartyService.process();
} catch (Throwable t) {log.error("调用第三方服务异常", t);
}
如果第三方 SDK 发生 Error
(如 ServiceConfigurationError
),应用可以优雅地处理,而不会直接崩溃。
2. 在普通 Controller 代码中,应该使用 Exception
普通业务逻辑和 Controller 层代码,不应该捕获 Throwable
,而是应该捕获 Exception
,防止吞掉 Error
。
比如:
@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 调用(远程服务) | ✅ 推荐,防止 | ❌ 可能遗漏 |
二方包(第三方 SDK) | ✅ 推荐,防止 | ❌ 可能遗漏 |
动态代理调用(如 CGLib、JDK Proxy) | ✅ 推荐,防止 | ❌ 可能遗漏 |
普通业务代码(如 Service、Controller) | ❌ 不推荐,会误吞 | ✅ 推荐,保证异常处理可控 |
Spring 全局异常处理( | ✅ 可以,兜底处理所有异常 | ✅ 推荐 |
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. 全局异常处理类
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. 关键点说明
@RestControllerAdvice
-
- 作用于所有
@RestController
,统一拦截异常并返回 JSON 响应。 - 如果是
@ControllerAdvice
,需要配合@ResponseBody
才能返回 JSON。
- 作用于所有
- 异常分类处理
-
IllegalArgumentException
:参数错误,如Assert
失败、入参校验不通过。BusinessException
:自定义业务异常,代表业务逻辑失败(如订单状态异常)。RuntimeException
:所有运行时异常(NullPointerException
、IndexOutOfBoundsException
)。Throwable
(兜底):
-
-
- 避免
Error
(如OutOfMemoryError
)直接导致应用崩溃。 - 但一般不应该在业务代码里捕获
Throwable
。
- 避免
-
- 日志级别
-
warn
:业务异常,开发人员关注即可。error
:系统异常或Throwable
,需要运维排查。
4.2. 自定义业务异常类
public class BusinessException extends RuntimeException {public BusinessException(String message) {super(message);}public BusinessException(String message, Throwable cause) {super(message, cause);}
}
4.3. 全局异常处理示例
@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. 业务异常(参数错误)
请求:
GET /order/-1
返回:
{"code": "ERROR","message": "参数错误:订单 ID 不能为空或小于等于 0"
}
4.3.1.2. 业务异常(订单不存在)
请求:
GET /order/999
返回:
{"code": "ERROR","message": "业务异常:订单不存在"
}
4.3.1.3. 系统异常
如果代码出现 NullPointerException
:
{"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 里手动捕获业务异常
@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. 🔥 关键点说明
- 业务异常(参数错误、业务失败)自己处理
-
IllegalArgumentException
(参数错误)BusinessException
(自定义业务异常)- 返回友好的错误信息,不让前端看到堆栈信息。
- 系统异常(
Exception
)不直接处理
-
NullPointerException
IndexOutOfBoundsException
DatabaseException
- 直接抛出,避免误吞
Error
,并确保日志完整。
- 日志级别
-
- 业务异常
warn
(开发关注) - 系统异常
error
(运维关注)
- 业务异常
4.8. ❌ 反面示例:全部 catch
但不抛出
catch (Exception e) {log.error("[进件管理] 查询异常:" + e.getMessage(), e);return Response.error("[进件管理] 查询失败");
}
- 问题:
-
- 吞掉异常,后续代码不知道哪里出了问题。
- 所有异常都变成普通业务异常,影响监控和排查。
- Error 也被吞掉,可能导致 JVM 崩溃时没有日志。
4.9. 🚀 最佳方案
- 业务异常 在 Controller 层 catch 并返回友好信息。
- 系统异常 让 Spring 兜底,避免误吞(可以配合
@ControllerAdvice
)。 - 错误日志要区分:
-
- 业务异常:
warn
(轻量级,不影响系统) - 系统异常:
error
(需要关注和报警)
- 业务异常:
4.10. 💡 结论
4.10.1. ✅ 如果没有全局异常处理
- Controller 层 需要
catch
业务异常,防止影响用户体验。 - 系统异常 不要吞掉,应抛出给 Spring 处理。
4.10.2. 🚀 最佳做法
- 业务异常(如
IllegalArgumentException
、BusinessException
)自己catch
并返回友好信息。 - 其他异常(如
NullPointerException
、数据库异常)直接抛出,避免误吞。 - 最终还是建议使用
@ControllerAdvice
统一管理,让代码更简洁!
代码位置 | 处理的异常 | 使用的异常类型 | 返回状态码 |
Controller | 参数错误 |
|
|
Service | 业务逻辑异常 |
|
|
全局异常处理 | 运行时异常 |
|
|
全局异常处理 | 未知错误 |
|
|
4.10.3. 最佳实践
- 业务代码中 只捕获
Exception
,避免误吞Error
。 - 调用 RPC/第三方 SDK 时,建议捕获
Throwable
兜底,防止Error
影响系统稳定。 - Controller 层不要直接
catch
异常,让全局异常处理器统一管理。 - 不同类型的异常,返回不同的 HTTP 状态码,方便前端或调用方识别。