Spring Boot 的全局异常处理器

1. 基础结构设计

1.1 统一响应类

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ApiResponse<T> {
    private Integer code;      // 状态码
    private String message;    // 消息
    private T data;            // 数据
    private Long timestamp;    // 时间戳

    public static <T> ApiResponse<T> success(T data) {
        return ApiResponse.<T>builder()
                .code(200)
                .message("成功")
                .data(data)
                .timestamp(System.currentTimeMillis())
                .build();
    }

    public static <T> ApiResponse<T> success() {
        return success(null);
    }

    public static <T> ApiResponse<T> error(Integer code, String message) {
        return ApiResponse.<T>builder()
                .code(code)
                .message(message)
                .timestamp(System.currentTimeMillis())
                .build();
    }
}

1.2 错误码枚举

java 复制代码
public enum ErrorCode {
    SUCCESS(200, "成功"),
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未授权"),
    FORBIDDEN(403, "禁止访问"),
    NOT_FOUND(404, "资源不存在"),
    INTERNAL_ERROR(500, "服务器内部错误"),
    
    // 业务错误码
    USER_NOT_EXIST(1001, "用户不存在"),
    INVALID_TOKEN(1002, "无效的token"),
    DUPLICATE_DATA(1003, "数据重复"),
    DATA_NOT_FOUND(1004, "数据不存在");

    private final Integer code;
    private final String message;

    ErrorCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

2. 自定义异常类

java 复制代码
/**
 * 基础异常类
 */
public class BaseException extends RuntimeException {
    private Integer code;
    
    public BaseException(Integer code, String message) {
        super(message);
        this.code = code;
    }
    
    public BaseException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
    }
    
    public BaseException(ErrorCode errorCode, String message) {
        super(message);
        this.code = errorCode.getCode();
    }
    
    public Integer getCode() {
        return code;
    }
}

/**
 * 业务异常
 */
public class BusinessException extends BaseException {
    public BusinessException(ErrorCode errorCode) {
        super(errorCode);
    }
    
    public BusinessException(ErrorCode errorCode, String message) {
        super(errorCode, message);
    }
    
    public BusinessException(Integer code, String message) {
        super(code, message);
    }
}

/**
 * 参数校验异常
 */
public class ValidationException extends BaseException {
    private Map<String, String> errors;
    
    public ValidationException(Map<String, String> errors) {
        super(ErrorCode.BAD_REQUEST);
        this.errors = errors;
    }
    
    public Map<String, String> getErrors() {
        return errors;
    }
}

3. 全局异常处理器

java 复制代码
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<?> handleBusinessException(BusinessException e) {
        log.error("业务异常: {}", e.getMessage(), e);
        return ApiResponse.error(e.getCode(), e.getMessage());
    }

    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(ValidationException.class)
    public ApiResponse<?> handleValidationException(ValidationException e) {
        log.error("参数校验异常: {}", e.getMessage());
        Map<String, Object> result = new HashMap<>();
        result.put("message", e.getMessage());
        result.put("errors", e.getErrors());
        return ApiResponse.error(e.getCode(), "参数校验失败").data(result);
    }

    /**
     * 处理参数绑定异常 (@Valid 触发的异常)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<?> handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e) {
        log.error("参数校验异常: {}", e.getMessage());
        
        Map<String, String> errors = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .collect(Collectors.toMap(
                        FieldError::getField,
                        fieldError -> fieldError.getDefaultMessage() != null ? 
                                fieldError.getDefaultMessage() : "参数错误"
                ));
        
        Map<String, Object> result = new HashMap<>();
        result.put("message", "参数校验失败");
        result.put("errors", errors);
        
        return ApiResponse.error(ErrorCode.BAD_REQUEST.getCode(), "参数校验失败")
                .data(result);
    }

    /**
     * 处理请求参数异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ApiResponse<?> handleConstraintViolationException(
            ConstraintViolationException e) {
        log.error("请求参数异常: {}", e.getMessage());
        
        Map<String, String> errors = e.getConstraintViolations()
                .stream()
                .collect(Collectors.toMap(
                        violation -> violation.getPropertyPath().toString(),
                        ConstraintViolation::getMessage
                ));
        
        Map<String, Object> result = new HashMap<>();
        result.put("message", "请求参数错误");
        result.put("errors", errors);
        
        return ApiResponse.error(ErrorCode.BAD_REQUEST.getCode(), "请求参数错误")
                .data(result);
    }

    /**
     * 处理404异常
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ApiResponse<?> handleNotFoundException(NoHandlerFoundException e) {
        log.error("资源不存在: {}", e.getRequestURL());
        return ApiResponse.error(ErrorCode.NOT_FOUND.getCode(), 
                "接口 [" + e.getRequestURL() + "] 不存在");
    }

    /**
     * 处理权限异常
     */
    @ExceptionHandler(AccessDeniedException.class)
    public ApiResponse<?> handleAccessDeniedException(AccessDeniedException e) {
        log.error("权限异常: {}", e.getMessage());
        return ApiResponse.error(ErrorCode.FORBIDDEN.getCode(), 
                "无访问权限");
    }

    /**
     * 处理认证异常
     */
    @ExceptionHandler(AuthenticationException.class)
    public ApiResponse<?> handleAuthenticationException(AuthenticationException e) {
        log.error("认证异常: {}", e.getMessage());
        return ApiResponse.error(ErrorCode.UNAUTHORIZED.getCode(), 
                "认证失败");
    }

    /**
     * 处理所有未捕获的异常
     */
    @ExceptionHandler(Exception.class)
    public ApiResponse<?> handleGlobalException(Exception e) {
        log.error("系统异常: ", e);
        // 生产环境可以返回通用错误信息
        String message = "系统繁忙,请稍后重试";
        // 开发环境可以返回详细错误
        // String message = e.getMessage();
        
        return ApiResponse.error(ErrorCode.INTERNAL_ERROR.getCode(), message);
    }

    /**
     * 处理HTTP请求方法不支持异常
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ApiResponse<?> handleHttpRequestMethodNotSupportedException(
            HttpRequestMethodNotSupportedException e) {
        log.error("HTTP方法不支持: {}", e.getMethod());
        return ApiResponse.error(405, 
                "不支持 [" + e.getMethod() + "] 请求方法");
    }
}

4. 配置类

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void configureHandlerExceptionResolvers(
            List<HandlerExceptionResolver> resolvers) {
        // 确保自定义的异常处理器优先级最高
        resolvers.add(0, new HandlerExceptionResolver() {
            @Override
            public ModelAndView resolveException(
                    HttpServletRequest request,
                    HttpServletResponse response,
                    Object handler,
                    Exception ex) {
                return null; // 返回null让@ControllerAdvice处理
            }
        });
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加全局拦截器示例
        registry.addInterceptor(new LogInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/error");
    }
}

5. 使用示例

5.1 Controller 示例

java 复制代码
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
    
    @GetMapping("/{id}")
    public ApiResponse<User> getUser(@PathVariable @Min(1) Long id) {
        // 模拟业务异常
        if (id > 100) {
            throw new BusinessException(ErrorCode.USER_NOT_EXIST);
        }
        return ApiResponse.success(new User(id, "张三"));
    }
    
    @PostMapping
    public ApiResponse<User> createUser(@Valid @RequestBody UserCreateRequest request) {
        // 参数校验由 @Valid 自动处理
        return ApiResponse.success(new User(1L, request.getName()));
    }
    
    @GetMapping("/test")
    public ApiResponse<String> testValidation(
            @RequestParam @NotBlank String name,
            @RequestParam @Min(1) @Max(100) Integer age) {
        return ApiResponse.success("验证通过");
    }
    
    @GetMapping("/error")
    public ApiResponse<String> testError() {
        // 测试系统异常
        throw new RuntimeException("测试系统异常");
    }
}

5.2 DTO 类(使用参数校验)

java 复制代码
@Data
public class UserCreateRequest {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
    private String name;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄不能小于1")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
}

5.3 业务层异常使用

java 复制代码
@Service
public class UserService {
    
    public User getUserById(Long id) {
        User user = userRepository.findById(id);
        if (user == null) {
            throw new BusinessException(ErrorCode.USER_NOT_EXIST, 
                    "用户ID: " + id + " 不存在");
        }
        
        // 其他业务校验
        if (user.isDisabled()) {
            throw new BusinessException(1005, "用户已被禁用");
        }
        
        return user;
    }
    
    public void createUser(UserCreateRequest request) {
        // 检查用户是否存在
        if (userRepository.existsByName(request.getName())) {
            Map<String, String> errors = new HashMap<>();
            errors.put("name", "用户名已存在");
            throw new ValidationException(errors);
        }
        
        // 保存用户
        userRepository.save(request);
    }
}

6. 测试

6.1 测试Controller

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
class GlobalExceptionHandlerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void testBusinessException() throws Exception {
        mockMvc.perform(get("/api/users/101"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(1001))
                .andExpect(jsonPath("$.message").value("用户不存在"));
    }
    
    @Test
    void testValidationException() throws Exception {
        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"name\":\"\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(400))
                .andExpect(jsonPath("$.data.errors.name").exists());
    }
}

7. 高级配置

7.1 自定义错误页面(可选)

yaml 复制代码
# application.yml
server:
  error:
    whitelabel:
      enabled: false
    path: /error

7.2 日志配置

yaml 复制代码
logging:
  level:
    com.example.exception: DEBUG
  file:
    name: logs/app.log
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

8. 最佳实践建议

  1. 异常分类清晰:业务异常、系统异常、参数异常等要区分清楚
  2. 错误信息友好:给用户的错误信息要友好,技术细节只在日志中记录
  3. 日志记录完整:异常发生时记录完整的堆栈信息
  4. 错误码统一:定义统一的错误码规范
  5. 安全考虑:生产环境不要返回敏感信息和堆栈信息
  6. 性能考虑:异常处理不要影响正常请求性能

这个全局异常处理器提供了:

  • ✅ 统一响应格式
  • ✅ 业务异常处理
  • ✅ 参数校验异常处理
  • ✅ 系统异常处理
  • ✅ HTTP状态码映射
  • ✅ 日志记录
  • ✅ 可扩展性

你可以根据实际业务需求进行调整和扩展。

相关推荐
上天夭1 小时前
PyTorch的Dataloader模块解析
人工智能·pytorch·python
风象南1 小时前
Spring Boot实现HTTPS双向认证
java·spring boot·后端
dTTb1 小时前
python元组和字典
python
嘻哈baby1 小时前
Prometheus + Grafana 监控系统搭建实战:从零到生产可用
后端
5***84641 小时前
【SpringBoot3】Spring Boot 3.0 集成 Mybatis Plus
spring boot·后端·mybatis
Java水解1 小时前
MySQL - 一文理清存储引擎:InnoDB vs MyISAM 核心差异
后端
sheji34161 小时前
【开题答辩全过程】以 基于Spring Boot的流浪动物救助系统设计为例,包含答辩的问题和答案
java·spring boot·后端
今天也很困1 小时前
用户密码安全存储:Go 实现 SM3 哈希加盐
后端
a***81391 小时前
SpringBoot集成Prometheus
spring boot·后端·prometheus