global-exception-handler.java.ftl 模板文件参考示例

以下是添加了详细中文注释说明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 错误)
  • 增强系统安全性(防止信息泄露)
  • 改善用户体验(友好错误提示)
  • 降低运维成本(结构化错误码)

可直接用于企业级项目,是高质量后端服务的必备组件。

Java 项目中出现 `Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: org/apache/poi/util/IOUtils` 错误,通常是由于类在编译时存在,但在运行时找不到该类的定义,下面是一些可能的解决方法: ### 检查依赖是否完整 `NoClassDefFoundError` 经常是缺少依赖导致的。`org.apache.poi.util.IOUtils` 类属于 Apache POI 库,需要确保在项目的依赖管理文件(如 `pom.xml` 或 `build.gradle`)中包含了正确的 Apache POI 依赖。 **Maven 示例**: ```xml <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.3</version> <!-- 可以根据需要选择合适的版本 --> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.3</version> <!-- 可以根据需要选择合适的版本 --> </dependency> ``` **Gradle 示例**: ```groovy implementation 'org.apache.poi:poi:5.2.3' implementation 'org.apache.poi:poi-ooxml:5.2.3' ``` ### 检查依赖版本兼容性 不同版本的 Apache POI 库可能存在兼容性问题,需要确保所有 POI 相关依赖的版本一致。例如,引用[1]中提到,对于 `poi 4.1.0` 和 `4.1.2` 版本,需要使用 `ooxml-schemas-1.4.jar`,否则可能会出现类找不到的问题。 ### 清理和重新构建项目 有时候,构建工具可能会缓存旧的依赖信息,导致依赖没有正确更新。可以尝试清理项目的构建缓存,然后重新构建项目。 **Maven 示例**: ```sh mvn clean install ``` **Gradle 示例**: ```sh gradle clean build ``` ### 检查类路径 确保在运行项目时,所有必要的 JAR 文件都包含在类路径中。如果是在服务器上运行项目,需要检查服务器的部署配置,确保所有依赖的 JAR 文件都被正确部署。 ### 检查类加载器 有时候,类加载器可能会出现问题,导致某些类无法被正确加载。可以尝试重启应用服务器,或者检查应用服务器的类加载器配置。 ### 检查代码中是否有冲突 检查代码中是否存在重复的类定义,或者是否有其他库与 Apache POI 库存在冲突。可以通过查看项目的依赖树来排查冲突。 **Maven 示例**: ```sh mvn dependency:tree ``` **Gradle 示例**: ```sh gradle dependencies ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值