摘要
文章主要探讨了 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. 关键点说明
@RestControllerAdvice
-
- 作用于所有
@RestController
,统一拦截异常并返回 JSON 响应。 - 如果是
@ControllerAdvice
,需要配合@ResponseBody
才能返回 JSON。
- 作用于所有
- 异常分类处理
-
IllegalArgumentException
:参数错误,如Assert
失败、入参校验不通过。BusinessException
:自定义业务异常,代表业务逻辑失败(如订单状态异常)。RuntimeException
:所有运行时异常(NullPointerException
、IndexOutOfBoundsException
)。Throwable
(兜底):
-
-
- 避免
Error
(如OutOfMemoryError
)直接导致应用崩溃。 - 但一般不应该在业务代码里捕获
Throwable
。
- 避免
-
- 日志级别
-
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. 🔥 关键点说明
- 业务异常(参数错误、业务失败)自己处理
-
IllegalArgumentException
(参数错误)BusinessException
(自定义业务异常)- 返回友好的错误信息,不让前端看到堆栈信息。
- 系统异常(
Exception
)不直接处理
-
NullPointerException
IndexOutOfBoundsException
DatabaseException
- 直接抛出,避免误吞
Error
,并确保日志完整。
- 日志级别
-
- 业务异常
warn
(开发关注) - 系统异常
error
(运维关注)
- 业务异常
4.8. ❌ 反面示例:全部 catch
但不抛出
lua
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 | 参数错误 | IllegalArgumentException |
400 BAD_REQUEST |
Service | 业务逻辑异常 | BusinessException |
400 BAD_REQUEST |
全局异常处理 | 运行时异常 | RuntimeException |
500 INTERNAL_SERVER_ERROR |
全局异常处理 | 未知错误 | Throwable |
500 INTERNAL_SERVER_ERROR |
4.10.3. 最佳实践
- 业务代码中 只捕获
Exception
,避免误吞Error
。 - 调用 RPC/第三方 SDK 时,建议捕获
Throwable
兜底,防止Error
影响系统稳定。 - Controller 层不要直接
catch
异常,让全局异常处理器统一管理。 - 不同类型的异常,返回不同的 HTTP 状态码,方便前端或调用方识别。