以下是添加了详细中文注释说明的 global-exception-handler.java.ftl 模板文件,涵盖异常处理的设计哲学、各类异常的处理策略、安全考量及企业级最佳实践:
package ${package.Config};
<#-- 导入自定义业务异常 -->
import ${package.Exception}.BusinessException;
<#-- 导入统一响应类 -->
import ${package.Common}.ApiResponse;
<#-- 导入 Spring Validation 异常 -->
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
<#-- 导入 JSR-303 路径变量校验异常 -->
import javax.validation.ConstraintViolationException;
<#-- 导入验证结果相关类 -->
import org.springframework.validation.FieldError;
/**
* <p>
* 全局异常处理器
* </p>
*
* <p>
* 【核心作用】
* 1. 统一捕获 Controller 层抛出的所有异常
* 2. 将异常转换为标准的 {@link ApiResponse} 格式返回给前端
* 3. 避免将内部堆栈信息暴露给客户端(安全要求)
* 4. 实现“异常即契约”的 API 设计理念
* </p>
*
* <p>
* 【设计原则】
* - 使用 {@code @RestControllerAdvice}:组合了 {@code @ControllerAdvice} 和 {@code @ResponseBody}
* - 全局生效(所有 Controller)
* - 返回值自动序列化为 JSON
* - 异常处理方法按 **具体到通用** 顺序排列(Java 方法重载解析规则)
* - 每个异常类型有独立的处理逻辑,职责清晰
* - 生产环境绝不返回堆栈信息(防止信息泄露)
* </p>
*
* <p>
* 【异常处理优先级】
* 1. {@link BusinessException} → 业务异常(最高优先级)
* 2. {@link MethodArgumentNotValidException} → @RequestBody 校验失败
* 3. {@link BindException} → 表单参数(application/x-www-form-urlencoded)校验失败
* 4. {@link ConstraintViolationException} → 路径变量/请求参数校验失败
* 5. {@link Exception} → 通用兜底(最低优先级)
* </p>
*
* <p>
* 【安全红线】
* - 所有异常消息必须经过脱敏处理
* - 系统级异常(如数据库连接失败)返回通用友好提示
* - 详细错误日志应记录到服务端(ELK/Sentry),而非返回给前端
* </p>
*
* @author ${author}
* @since ${date}
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常({@link BusinessException})
*
* <p>
* 【业务异常特点】
* - 由 Service 层主动抛出
* - 表示可预期的业务规则违反(如“用户名已存在”)
* - 包含语义化的错误码(code)和用户友好消息(message)
* </p>
*
* <p>
* 【处理策略】
* - 直接使用异常中的 code 和 message
* - 返回 200 HTTP 状态码(因为这是“成功执行但业务失败”)
* - 前端可根据 code 做差异化处理(如跳转、提示等)
* </p>
*
* <p>
* 【示例】
* throw new BusinessException("USER_EXISTS", "用户名已存在");
* → 返回:{ "code": "USER_EXISTS", "message": "用户名已存在", "data": null }
* </p>
*
* @param e 捕获的业务异常实例
* @return 标准化的失败响应
*/
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException e) {
// 直接使用异常中携带的错误码和消息
return ApiResponse.fail(e.getCode(), e.getMessage());
}
/**
* 处理 {@code @Valid} + {@code @RequestBody} 校验失败异常
*
* <p>
* 【触发场景】
* Controller 方法参数使用 {@code @Valid @RequestBody UserCreateDTO dto}
* 且 DTO 中的字段校验失败(如 @NotBlank、@Email 等)
* </p>
*
* <p>
* 【异常结构】
* - {@link MethodArgumentNotValidException#getBindingResult()} 包含所有校验错误
* - 通常只取第一个错误(避免返回过多错误信息)
* - {@link FieldError#getDefaultMessage()} 是校验注解中定义的消息
* </p>
*
* <p>
* 【处理策略】
* - 错误码固定为 "VALIDATION_ERROR"
* - 消息取第一个字段的校验失败提示
* - 返回 200 HTTP 状态码(与业务异常一致)
* </p>
*
* <p>
* 【扩展建议】
* 若需返回所有错误,可遍历 bindingResult.getAllErrors()
* 但通常前端只需第一个错误即可
* </p>
*
* @param e 参数校验异常
* @return 标准化的校验失败响应
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException e) {
// 获取第一个校验错误(通常足够)
FieldError fieldError = e.getBindingResult().getFieldError();
// 若无错误(理论上不会发生),返回通用消息
String message = fieldError != null ? fieldError.getDefaultMessage() : "请求参数校验失败";
return ApiResponse.fail("VALIDATION_ERROR", message);
}
/**
* 处理表单参数(application/x-www-form-urlencoded)校验失败异常
*
* <p>
* 【触发场景】
* - 使用表单提交(非 JSON)
* - Controller 方法参数使用 {@code @Valid UserForm form}
* - 表单字段校验失败
* </p>
*
* <p>
* 【与 MethodArgumentNotValidException 的区别】
* - {@code MethodArgumentNotValidException}:用于 {@code @RequestBody}(JSON)
* - {@code BindException}:用于普通表单参数(非 {@code @RequestBody})
* </p>
*
* <p>
* 【处理策略】
* - 与 JSON 校验异常处理逻辑一致
* - 统一返回 "VALIDATION_ERROR" 错误码
* </p>
*
* @param e 表单绑定异常
* @return 标准化的校验失败响应
*/
@ExceptionHandler(BindException.class)
public ApiResponse<Void> handleBindException(BindException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
String message = fieldError != null ? fieldError.getDefaultMessage() : "表单参数校验失败";
return ApiResponse.fail("VALIDATION_ERROR", message);
}
/**
* 处理路径变量/请求参数校验失败异常(JSR-303)
*
* <p>
* 【触发场景】
* Controller 方法签名中直接对参数校验,例如:
* {@code public User getUser(@PathVariable @Min(1) Long id)}
* </p>
*
* <p>
* 【异常特点】
* - 抛出 {@link ConstraintViolationException}
* - 错误信息在 {@code getConstraintViolations()} 集合中
* </p>
*
* <p>
* 【处理策略】
* - 取第一个约束违反的错误消息
* - 返回统一校验错误码
* </p>
*
* @param e 约束违反异常
* @return 标准化的校验失败响应
*/
@ExceptionHandler(ConstraintViolationException.class)
public ApiResponse<Void> handleConstraintViolationException(ConstraintViolationException e) {
// 获取第一个约束违反的错误消息
String message = e.getConstraintViolations().stream()
.findFirst()
.map(violation -> violation.getMessage())
.orElse("请求参数格式错误");
return ApiResponse.fail("VALIDATION_ERROR", message);
}
/**
* 通用异常兜底处理(最后防线)
*
* <p>
* 【捕获范围】
* 所有未被上述处理器捕获的异常,包括:
* - NullPointerException
* - SQLException
* - 自定义未分类异常
* - 第三方库异常
* </p>
*
* <p>
* 【安全处理原则】
* - **绝不**将 e.getMessage() 或 e.printStackTrace() 返回给前端!
* (可能包含数据库密码、文件路径等敏感信息)
* - 返回通用友好提示:“系统繁忙,请稍后再试”
* - **必须**在服务端记录完整堆栈(通过日志框架)
* </p>
*
* <p>
* 【日志记录建议】
* 在实际项目中,此处应添加:
* {@code log.error("系统异常", e);}
* 并集成 ELK/Sentry 等监控系统
* </p>
*
* <p>
* 【HTTP 状态码】
* 虽然返回 200(因 ApiResponse 统一包装),但可通过 code 区分
* 前端应统一处理 "SYSTEM_ERROR" code
* </p>
*
* @param e 任意未处理的异常
* @return 通用系统错误响应
*/
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
// 【安全红线】生产环境禁止暴露异常细节!
// 正确做法:记录日志(此处省略,实际项目必须添加)
// log.error("未处理的系统异常", e);
// 返回通用友好提示
return ApiResponse.fail("SYSTEM_ERROR", "系统繁忙,请稍后再试");
}
}
🔍 关键设计说明补充
1. 为什么所有异常都返回 HTTP 200?
- 统一响应体设计:
ApiResponse已包含code字段区分成功/失败 - 前端处理简化:无需处理 HTTP 4xx/5xx,统一解析
code - 符合 RESTful 争议实践:部分团队偏好 HTTP 状态码,但统一响应体更灵活
💡 若需返回真实 HTTP 状态码,可修改为:
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.fail(...));
2. 异常处理顺序的重要性
// ❌ 错误:通用异常在前,会屏蔽所有具体异常
@ExceptionHandler(Exception.class)
public ApiResponse<?> handleAll(Exception e) { ... }
@ExceptionHandler(BusinessException.class) // 永远不会执行!
public ApiResponse<?> handleBusiness(BusinessException e) { ... }
3. 生产环境日志记录
在 handleException 中应添加:
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
// 生成唯一错误ID,便于追踪
String errorId = UUID.randomUUID().toString();
log.error("系统异常 [ID: {}]", errorId, e); // 记录完整堆栈
return ApiResponse.fail("SYSTEM_ERROR", "系统繁忙,请稍后再试(错误ID: " + errorId + ")");
}
4. 前端错误处理建议
// axios 响应拦截器
axios.interceptors.response.use(
response => {
const { code, message, data } = response.data;
if (code !== 200) {
if (code === 'VALIDATION_ERROR') {
ElMessage.warning(message);
} else if (code === 'SYSTEM_ERROR') {
ElMessage.error('系统错误,请联系管理员');
} else {
ElMessage.error(message);
}
return Promise.reject(new Error(message));
}
return data;
}
);
✅ 此模板文件是 系统健壮性的最后防线,正确配置后可:
- 提升 API 可用性(避免 500 错误)
- 增强系统安全性(防止信息泄露)
- 改善用户体验(友好错误提示)
- 降低运维成本(结构化错误码)
可直接用于企业级项目,是高质量后端服务的必备组件。
3011

被折叠的 条评论
为什么被折叠?



