10.SpringBoot的统一异常处理详解

发布于:2025-07-10 ⋅ 阅读:(21) ⋅ 点赞:(0)

1. 异常处理基础概念

1.1 什么是异常处理

异常处理是程序在运行过程中遇到错误时的处理机制。在Web应用中,异常处理负责捕获和处理各种异常情况,并向客户端返回友好的错误信息。

形象比喻:
想象异常处理就像医院的急诊科:

  • 分诊台(@ExceptionHandler):根据病情类型分配到不同科室
  • 专科医生(具体异常处理器):针对特定问题进行专业处理
  • 病历记录(日志记录):记录处理过程和结果
  • 出院小结(统一响应格式):给病人明确的诊断和建议

1.2 为什么需要统一异常处理

  1. 用户体验:避免直接暴露技术错误信息给用户
  2. 安全性:防止敏感信息泄露(如数据库结构、系统路径等)
  3. 维护性:集中处理异常逻辑,便于维护和修改
  4. 一致性:确保所有错误响应格式统一
  5. 监控性:便于日志记录和错误追踪

1.3 Spring异常处理机制

Spring提供了多种异常处理方式:

  1. @ExceptionHandler:方法级异常处理
  2. @ControllerAdvice:全局异常处理
  3. @RestControllerAdvice:REST接口全局异常处理
  4. HandlerExceptionResolver:底层异常解析器

2. SpringBoot默认异常处理

2.1 默认错误页面

SpringBoot默认提供了基本的异常处理机制:

package com.example.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试控制器 - 演示默认异常处理
 */
@RestController
@RequestMapping("/api/test")
public class TestController {
    
    /**
     * 模拟空指针异常
     */
    @GetMapping("/null-pointer")
    public String nullPointerException() {
        String str = null;
        return str.length() + ""; // 这里会抛出NullPointerException
    }
    
    /**
     * 模拟数组越界异常
     */
    @GetMapping("/array-index")
    public String arrayIndexException() {
        int[] array = {1, 2, 3};
        return array[5] + ""; // 这里会抛出ArrayIndexOutOfBoundsException
    }
    
    /**
     * 模拟除零异常
     */
    @GetMapping("/divide-zero")
    public String divideByZeroException() {
        int result = 10 / 0; // 这里会抛出ArithmeticException
        return result + "";
    }
    
    /**
     * 模拟参数类型错误
     */
    @GetMapping("/users/{id}")
    public String getUserById(@PathVariable Long id) {
        return "User ID: " + id;
    }
}

当访问这些端点时,SpringBoot会返回默认的错误信息,但这些信息对用户不够友好。

2.2 自定义错误页面

可以通过配置文件定制默认错误页面:

# application.yml
server:
  error:
    include-message: always
    include-binding-errors: always
    include-stacktrace: on_param
    include-exception: false
    path: /error

3. 全局异常处理器

3.1 基础全局异常处理器

package com.example.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
 * 全局异常处理器
 * @RestControllerAdvice 是 @ControllerAdvice + @ResponseBody 的组合
 * 用于处理所有Controller抛出的异常
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    /**
     * 处理空指针异常
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, Object> handleNullPointerException(NullPointerException e) {
        logger.error("空指针异常: ", e);
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 500);
        result.put("message", "系统内部错误,请稍后重试");
        result.put("timestamp", LocalDateTime.now());
        result.put("success", false);
        
        return result;
    }
    
    /**
     * 处理数组越界异常
     */
    @ExceptionHandler(ArrayIndexOutOfBoundsException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, Object> handleArrayIndexOutOfBoundsException(ArrayIndexOutOfBoundsException e) {
        logger.error("数组越界异常: ", e);
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 500);
        result.put("message", "数据访问越界");
        result.put("timestamp", LocalDateTime.now());
        result.put("success", false);
        
        return result;
    }
    
    /**
     * 处理算术异常(如除零异常)
     */
    @ExceptionHandler(ArithmeticException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleArithmeticException(ArithmeticException e) {
        logger.error("算术异常: ", e);
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 400);
        result.put("message", "计算错误:" + e.getMessage());
        result.put("timestamp", LocalDateTime.now());
        result.put("success", false);
        
        return result;
    }
    
    /**
     * 处理通用Exception
     * 这个方法会捕获所有没有被具体处理的异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, Object> handleGeneralException(Exception e) {
        logger.error("未处理的异常: ", e);
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 500);
        result.put("message", "系统异常,请联系管理员");
        result.put("timestamp", LocalDateTime.now());
        result.put("success", false);
        
        return result;
    }
}

3.2 统一响应格式

为了保持响应格式的一致性,建议创建统一的响应类:

package com.example.common;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;

/**
 * 统一响应结果类
 * @param <T> 数据类型
 */
public class Result<T> {
    
    /**
     * 响应码
     */
    private Integer code;
    
    /**
     * 响应消息
     */
    private String message;
    
    /**
     * 响应数据
     */
    private T data;
    
    /**
     * 是否成功
     */
    private Boolean success;
    
    /**
     * 时间戳
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime timestamp;
    
    public Result() {
        this.timestamp = LocalDateTime.now();
    }
    
    public Result(Integer code, String message, T data, Boolean success) {
        this();
        this.code = code;
        this.message = message;
        this.data = data;
        this.success = success;
    }
    
    /**
     * 成功响应
     */
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data, true);
    }
    
    public static <T> Result<T> success(String message, T data) {
        return new Result<>(200, message, data, true);
    }
    
    public static <T> Result<T> success() {
        return new Result<>(200, "操作成功", null, true);
    }
    
    /**
     * 失败响应
     */
    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null, false);
    }
    
    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null, false);
    }
    
    public static <T> Result<T> error() {
        return new Result<>(500, "操作失败", null, false);
    }
    
    // getter和setter方法
    public Integer getCode() {
        return code;
    }
    
    public void setCode(Integer code) {
        this.code = code;
    }
    
    public String getMessage() {
        return message;
    }
    
    public void setMessage(String message) {
        this.message = message;
    }
    
    public T getData() {
        return data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
    
    public Boolean getSuccess() {
        return success;
    }
    
    public void setSuccess(Boolean success) {
        this.success = success;
    }
    
    public LocalDateTime getTimestamp() {
        return timestamp;
    }
    
    public void setTimestamp(LocalDateTime timestamp) {
        this.timestamp = timestamp;
    }
}

3.3 使用统一响应格式的异常处理器

package com.example.exception;

import com.example.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

/**
 * 改进的全局异常处理器
 * 使用统一响应格式
 */
@RestControllerAdvice
public class ImprovedGlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(ImprovedGlobalExceptionHandler.class);
    
    /**
     * 处理空指针异常
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleNullPointerException(NullPointerException e) {
        logger.error("空指针异常: ", e);
        return Result.error(500, "系统内部错误,请稍后重试");
    }
    
    /**
     * 处理数组越界异常
     */
    @ExceptionHandler(ArrayIndexOutOfBoundsException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleArrayIndexOutOfBoundsException(ArrayIndexOutOfBoundsException e) {
        logger.error("数组越界异常: ", e);
        return Result.error(500, "数据访问越界");
    }
    
    /**
     * 处理算术异常
     */
    @ExceptionHandler(ArithmeticException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Void> handleArithmeticException(ArithmeticException e) {
        logger.error("算术异常: ", e);
        return Result.error(400, "计算错误:" + e.getMessage());
    }
    
    /**
     * 处理通用异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleGeneralException(Exception e) {
        logger.error("未处理的异常: ", e);
        return Result.error(500, "系统异常,请联系管理员");
    }
}

4. 自定义异常

4.1 创建基础自定义异常

package com.example.exception;

/**
 * 基础业务异常类
 * 所有自定义业务异常都应该继承这个类
 */
public class BaseException extends RuntimeException {
    
    /**
     * 错误码
     */
    private Integer code;
    
    /**
     * 错误消息
     */
    private String message;
    
    public BaseException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
    
    public BaseException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.message = message;
    }
    
    public BaseException(String message) {
        this(500, message);
    }
    
    public BaseException(String message, Throwable cause) {
        this(500, message, cause);
    }
    
    public Integer getCode() {
        return code;
    }
    
    public void setCode(Integer code) {
        this.code = code;
    }
    
    @Override
    public String getMessage() {
        return message;
    }
    
    public void setMessage(String message) {
        this.message = message;
    }
}

4.2 具体的业务异常类

package com.example.exception;

/**
 * 业务异常类
 * 用于处理业务逻辑中的异常情况
 */
public class BusinessException extends BaseException {
    
    public BusinessException(String message) {
        super(400, message);
    }
    
    public BusinessException(Integer code, String message) {
        super(code, message);
    }
    
    public BusinessException(String message, Throwable cause) {
        super(400, message, cause);
    }
}

/**
 * 用户不存在异常
 */
public class UserNotFoundException extends BaseException {
    
    public UserNotFoundException(Long userId) {
        super(404, "用户不存在,ID:" + userId);
    }
    
    public UserNotFoundException(String username) {
        super(404, "用户不存在,用户名:" + username);
    }
}

/**
 * 参数校验异常
 */
public class ValidationException extends BaseException {
    
    public ValidationException(String message) {
        super(400, message);
    }
    
    public ValidationException(String field, String message) {
        super(400, String.format("参数校验失败:%s %s", field, message));
    }
}

/**
 * 权限不足异常
 */
public class PermissionDeniedException extends BaseException {
    
    public PermissionDeniedException() {
        super(403, "权限不足,无法访问该资源");
    }
    
    public PermissionDeniedException(String message) {
        super(403, message);
    }
    
    public PermissionDeniedException(String resource, String action) {
        super(403, String.format("权限不足,无法对资源 %s 执行 %s 操作", resource, action));
    }
}

/**
 * 数据重复异常
 */
public class DuplicateDataException extends BaseException {
    
    public DuplicateDataException(String message) {
        super(409, message);
    }
    
    public DuplicateDataException(String field, String value) {
        super(409, String.format("数据已存在:%s = %s", field, value));
    }
}

/**
 * 资源未找到异常
 */
public class ResourceNotFoundException extends BaseException {
    
    public ResourceNotFoundException(String resource) {
        super(404, resource + " 不存在");
    }
    
    public ResourceNotFoundException(String resource, String id) {
        super(404, String.format("%s 不存在,ID:%s", resource, id));
    }
}

4.3 错误码枚举

为了更好地管理错误码,建议使用枚举:

package com.example.enums;

/**
 * 错误码枚举
 * 统一管理系统中的所有错误码
 */
public enum ErrorCode {
    
    // 成功
    SUCCESS(200, "操作成功"),
    
    // 客户端错误 4xx
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未授权"),
    FORBIDDEN(403, "权限不足"),
    NOT_FOUND(404, "资源不存在"),
    METHOD_NOT_ALLOWED(405, "请求方法不支持"),
    CONFLICT(409, "数据冲突"),
    VALIDATION_FAILED(422, "参数校验失败"),
    
    // 服务器错误 5xx
    INTERNAL_SERVER_ERROR(500, "系统内部错误"),
    SERVICE_UNAVAILABLE(503, "服务不可用"),
    
    // 业务错误 6xxx
    USER_NOT_FOUND(6001, "用户不存在"),
    USER_ALREADY_EXISTS(6002, "用户已存在"),
    INVALID_PASSWORD(6003, "密码错误"),
    ACCOUNT_LOCKED(6004, "账户已锁定"),
    ACCOUNT_EXPIRED(6005, "账户已过期"),
    
    // 数据错误 7xxx
    DATA_NOT_FOUND(7001, "数据不存在"),
    DATA_ALREADY_EXISTS(7002, "数据已存在"),
    DATA_INTEGRITY_VIOLATION(7003, "数据完整性约束冲突"),
    
    // 第三方服务错误 8xxx
    EXTERNAL_SERVICE_ERROR(8001, "外部服务调用失败"),
    PAYMENT_SERVICE_ERROR(8002, "支付服务异常"),
    SMS_SERVICE_ERROR(8003, "短信服务异常");
    
    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;
    }
}

4.4 使用错误码的异常类

package com.example.exception;

import com.example.enums.ErrorCode;

/**
 * 使用错误码的业务异常
 */
public class BizException extends BaseException {
    
    private ErrorCode errorCode;
    
    public BizException(ErrorCode errorCode) {
        super(errorCode.getCode(), errorCode.getMessage());
        this.errorCode = errorCode;
    }
    
    public BizException(ErrorCode errorCode, String customMessage) {
        super(errorCode.getCode(), customMessage);
        this.errorCode = errorCode;
    }
    
    public BizException(ErrorCode errorCode, Throwable cause) {
        super(errorCode.getCode(), errorCode.getMessage(), cause);
        this.errorCode = errorCode;
    }
    
    public ErrorCode getErrorCode() {
        return errorCode;
    }
}

5. 完整的异常处理体系

5.1 完整的全局异常处理器

package com.example.exception;

import com.example.common.Result;
import com.example.enums.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 完整的全局异常处理器
 * 处理系统中可能出现的各种异常
 */
@RestControllerAdvice
public class CompleteGlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(CompleteGlobalExceptionHandler.class);
    
    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(BizException.class)
    @ResponseStatus(HttpStatus.OK) // 业务异常通常返回200,通过业务码区分
    public Result<Void> handleBizException(BizException e, HttpServletRequest request) {
        logger.warn("业务异常 - URL: {}, 异常: {}", request.getRequestURL(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }
    
    /**
     * 处理基础自定义异常
     */
    @ExceptionHandler(BaseException.class)
    @ResponseStatus(HttpStatus.OK)
    public Result<Void> handleBaseException(BaseException e, HttpServletRequest request) {
        logger.warn("自定义异常 - URL: {}, 异常: {}", request.getRequestURL(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }
    
    /**
     * 处理参数校验异常(@Valid)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Map<String, String>> handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e, HttpServletRequest request) {
        
        logger.warn("参数校验异常 - URL: {}", request.getRequestURL());
        
        Map<String, String> errors = new HashMap<>();
        for (FieldError error : e.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        
        return Result.error(ErrorCode.VALIDATION_FAILED.getCode(), "参数校验失败").setData(errors);
    }
    
    /**
     * 处理Bean校验异常
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Map<String, String>> handleBindException(BindException e, HttpServletRequest request) {
        logger.warn("Bean校验异常 - URL: {}", request.getRequestURL());
        
        Map<String, String> errors = new HashMap<>();
        for (FieldError error : e.getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        
        return Result.error(ErrorCode.VALIDATION_FAILED.getCode(), "参数校验失败").setData(errors);
    }
    
    /**
     * 处理单个参数校验异常(@Validated)
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Map<String, String>> handleConstraintViolationException(
            ConstraintViolationException e, HttpServletRequest request) {
        
        logger.warn("参数约束异常 - URL: {}", request.getRequestURL());
        
        Map<String, String> errors = new HashMap<>();
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        for (ConstraintViolation<?> violation : violations) {
            String propertyPath = violation.getPropertyPath().toString();
            String message = violation.getMessage();
            errors.put(propertyPath, message);
        }
        
        return Result.error(ErrorCode.VALIDATION_FAILED.getCode(), "参数校验失败").setData(errors);
    }
    
    /**
     * 处理请求参数缺失异常
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Void> handleMissingServletRequestParameterException(
            MissingServletRequestParameterException e, HttpServletRequest request) {
        
        logger.warn("缺少请求参数 - URL: {}, 参数: {}", request.getRequestURL(), e.getParameterName());
        
        String message = String.format("缺少必要的请求参数:%s", e.getParameterName());
        return Result.error(ErrorCode.BAD_REQUEST.getCode(), message);
    }
    
    /**
     * 处理参数类型不匹配异常
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Void> handleMethodArgumentTypeMismatchException(
            MethodArgumentTypeMismatchException e, HttpServletRequest request) {
        
        logger.warn("参数类型不匹配 - URL: {}, 参数: {}", request.getRequestURL(), e.getName());
        
        String message = String.format("参数类型不匹配:%s", e.getName());
        return Result.error(ErrorCode.BAD_REQUEST.getCode(), message);
    }
    
    /**
     * 处理HTTP请求方法不支持异常
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public Result<Void> handleHttpRequestMethodNotSupportedException(
            HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
        
        logger.warn("请求方法不支持 - URL: {}, 方法: {}", request.getRequestURL(), e.getMethod());
        
        String message = String.format("请求方法 %s 不支持", e.getMethod());
        return Result.error(ErrorCode.METHOD_NOT_ALLOWED.getCode(), message);
    }
    
    /**
     * 处理HTTP消息不可读异常(通常是JSON格式错误)
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Void> handleHttpMessageNotReadableException(
            HttpMessageNotReadableException e, HttpServletRequest request) {
        
        logger.warn("HTTP消息不可读 - URL: {}", request.getRequestURL());
        
        return Result.error(ErrorCode.BAD_REQUEST.getCode(), "请求体格式错误");
    }
    
    /**
     * 处理404异常
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Result<Void> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
        logger.warn("404异常 - URL: {}", request.getRequestURL());
        
        return Result.error(ErrorCode.NOT_FOUND.getCode(), "请求的资源不存在");
    }
    
    /**
     * 处理空指针异常
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
        logger.error("空指针异常 - URL: {}", request.getRequestURL(), e);
        
        return Result.error(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), "系统内部错误");
    }
    
    /**
     * 处理运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
        logger.error("运行时异常 - URL: {}", request.getRequestURL(), e);
        
        return Result.error(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), "系统内部错误");
    }
    
    /**
     * 处理所有未捕获的异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleException(Exception e, HttpServletRequest request) {
        logger.error("未知异常 - URL: {}", request.getRequestURL(), e);
        
        return Result.error(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), "系统异常,请联系管理员");
    }
}

5.2 改进的Result类

package com.example.common;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;

import java.time.LocalDateTime;

/**
 * 改进的统一响应结果类
 */
@JsonInclude(JsonInclude.Include.NON_NULL) // 空值不序列化
public class Result<T> {
    
    private Integer code;
    private String message;
    private T data;
    private Boolean success;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime timestamp;
    
    public Result() {
        this.timestamp = LocalDateTime.now();
    }
    
    public Result(Integer code, String message, T data, Boolean success) {
        this();
        this.code = code;
        this.message = message;
        this.data = data;
        this.success = success;
    }
    
    // 成功响应
    public static <T> Result<T> success() {
        return new Result<>(200, "操作成功", null, true);
    }
    
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data, true);
    }
    
    public static <T> Result<T> success(String message) {
        return new Result<>(200, message, null, true);
    }
    
    public static <T> Result<T> success(String message, T data) {
        return new Result<>(200, message, data, true);
    }
    
    // 失败响应
    public static <T> Result<T> error() {
        return new Result<>(500, "操作失败", null, false);
    }
    
    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null, false);
    }
    
    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null, false);
    }
    
    public static <T> Result<T> error(Integer code, String message, T data) {
        return new Result<>(code, message, data, false);
    }
    
    // 支持链式调用
    public Result<T> setData(T data) {
        this.data = data;
        return this;
    }
    
    public Result<T> setMessage(String message) {
        this.message = message;
        return this;
    }
    
    // getter和setter方法
    public Integer getCode() {
        return code;
    }
    
    public void setCode(Integer code) {
        this.code = code;
    }
    
    public String getMessage() {
        return message;
    }
    
    public void setMessage(String message) {
        this.message = message;
    }
    
    public T getData() {
        return data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
    
    public Boolean getSuccess() {
        return success;
    }
    
    public void setSuccess(Boolean success) {
        this.success = success;
    }
    
    public LocalDateTime getTimestamp() {
        return timestamp;
    }
    
    public void setTimestamp(LocalDateTime timestamp) {
        this.timestamp = timestamp;
    }
}

6. 实际应用示例

6.1 用户服务示例

package com.example.entity;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

/**
 * 用户实体类
 */
public class User {
    
    private Long id;
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
    private String password;
    
    // 构造方法
    public User() {}
    
    public User(Long id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }
    
    // getter和setter方法
    public Long getId() {
        return id;
    }
    
    public void setId(Long id) {
        this.id = id;
    }
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = email;
    }
    
    public String getPassword() {
        return password;
    }
    
    public void setPassword(String password) {
        this.password = password;
    }
}

6.2 用户服务类

package com.example.service;

import com.example.entity.User;
import com.example.enums.ErrorCode;
import com.example.exception.BizException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 用户服务类
 * 演示异常处理的使用
 */
@Service
public class UserService {
    
    // 模拟数据库
    private final ConcurrentHashMap<Long, User> userDatabase = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);
    
    /**
     * 根据ID查询用户
     */
    public User getUserById(Long id) {
        if (id == null || id <= 0) {
            throw new BizException(ErrorCode.BAD_REQUEST, "用户ID不能为空或小于等于0");
        }
        
        User user = userDatabase.get(id);
        if (user == null) {
            throw new BizException(ErrorCode.USER_NOT_FOUND);
        }
        
        return user;
    }
    
    /**
     * 根据用户名查询用户
     */
    public User getUserByUsername(String username) {
        if (username == null || username.trim().isEmpty()) {
            throw new BizException(ErrorCode.BAD_REQUEST, "用户名不能为空");
        }
        
        User user = userDatabase.values().stream()
                .filter(u -> username.equals(u.getUsername()))
                .findFirst()
                .orElse(null);
        
        if (user == null) {
            throw new BizException(ErrorCode.USER_NOT_FOUND, "用户不存在:" + username);
        }
        
        return user;
    }
    
    /**
     * 创建用户
     */
    public User createUser(User user) {
        // 检查用户名是否已存在
        boolean usernameExists = userDatabase.values().stream()
                .anyMatch(u -> user.getUsername().equals(u.getUsername()));
        
        if (usernameExists) {
            throw new BizException(ErrorCode.USER_ALREADY_EXISTS, "用户名已存在:" + user.getUsername());
        }
        
        // 检查邮箱是否已存在
        boolean emailExists = userDatabase.values().stream()
                .anyMatch(u -> user.getEmail().equals(u.getEmail()));
        
        if (emailExists) {
            throw new BizException(ErrorCode.DATA_ALREADY_EXISTS, "邮箱已存在:" + user.getEmail());
        }
        
        // 设置ID并保存
        user.setId(idGenerator.getAndIncrement());
        userDatabase.put(user.getId(), user);
        
        return user;
    }
    
    /**
     * 更新用户
     */
    public User updateUser(Long id, User user) {
        User existingUser = getUserById(id); // 这里会抛出用户不存在异常
        
        // 检查用户名是否被其他用户占用
        boolean usernameConflict = userDatabase.values().stream()
                .anyMatch(u -> !u.getId().equals(id) && user.getUsername().equals(u.getUsername()));
        
        if (usernameConflict) {
            throw new BizException(ErrorCode.CONFLICT, "用户名已被其他用户占用:" + user.getUsername());
        }
        
        // 更新用户信息
        existingUser.setUsername(user.getUsername());
        existingUser.setEmail(user.getEmail());
        
        return existingUser;
    }
    
    /**
     * 删除用户
     */
    public void deleteUser(Long id) {
        User user = getUserById(id); // 这里会抛出用户不存在异常
        userDatabase.remove(id);
    }
    
    /**
     * 获取所有用户
     */
    public List<User> getAllUsers() {
        return new ArrayList<>(userDatabase.values());
    }
    
    /**
     * 用户登录
     */
    public User login(String username, String password) {
        if (username == null || username.trim().isEmpty()) {
            throw new BizException(ErrorCode.BAD_REQUEST, "用户名不能为空");
        }
        
        if (password == null || password.trim().isEmpty()) {
            throw new BizException(ErrorCode.BAD_REQUEST, "密码不能为空");
        }
        
        User user = getUserByUsername(username);
        
        if (!password.equals(user.getPassword())) {
            throw new BizException(ErrorCode.INVALID_PASSWORD);
        }
        
        return user;
    }
    
    /**
     * 模拟系统异常
     */
    public void simulateSystemError() {
        throw new RuntimeException("模拟系统异常");
    }
    
    /**
     * 模拟空指针异常
     */
    public String simulateNullPointer() {
        String str = null;
        return str.length() + ""; // 这里会抛出NullPointerException
    }
}

6.3 用户控制器

package com.example.controller;

import com.example.common.Result;
import com.example.entity.User;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.List;

/**
 * 用户控制器
 * 演示统一异常处理的效果
 */
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
    
    @Autowired
    private UserService userService;
    
    /**
     * 根据ID查询用户
     */
    @GetMapping("/{id}")
    public Result<User> getUserById(@PathVariable @Min(value = 1, message = "用户ID必须大于0") Long id) {
        User user = userService.getUserById(id);
        return Result.success("查询成功", user);
    }
    
    /**
     * 根据用户名查询用户
     */
    @GetMapping("/username/{username}")
    public Result<User> getUserByUsername(@PathVariable String username) {
        User user = userService.getUserByUsername(username);
        return Result.success("查询成功", user);
    }
    
    /**
     * 创建用户
     */
    @PostMapping
    public Result<User> createUser(@Valid @RequestBody User user) {
        User createdUser = userService.createUser(user);
        return Result.success("用户创建成功", createdUser);
    }
    
    /**
     * 更新用户
     */
    @PutMapping("/{id}")
    public Result<User> updateUser(@PathVariable @Min(value = 1, message = "用户ID必须大于0") Long id,
                                  @Valid @RequestBody User user) {
        User updatedUser = userService.updateUser(id, user);
        return Result.success("用户更新成功", updatedUser);
    }
    
    /**
     * 删除用户
     */
    @DeleteMapping("/{id}")
    public Result<Void> deleteUser(@PathVariable @Min(value = 1, message = "用户ID必须大于0") Long id) {
        userService.deleteUser(id);
        return Result.success("用户删除成功");
    }
    
    /**
     * 获取所有用户
     */
    @GetMapping
    public Result<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return Result.success("查询成功", users);
    }
    
    /**
     * 用户登录
     */
    @PostMapping("/login")
    public Result<User> login(@RequestParam String username, @RequestParam String password) {
        User user = userService.login(username, password);
        return Result.success("登录成功", user);
    }
    
    /**
     * 模拟系统异常
     */
    @GetMapping("/error/system")
    public Result<Void> simulateSystemError() {
        userService.simulateSystemError();
        return Result.success();
    }
    
    /**
     * 模拟空指针异常
     */
    @GetMapping("/error/null-pointer")
    public Result<String> simulateNullPointer() {
        String result = userService.simulateNullPointer();
        return Result.success("操作成功", result);
    }
}

6.4 登录请求DTO

package com.example.dto;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

/**
 * 登录请求DTO
 */
public class LoginRequest {
    
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码长度不能少于6位")
    private String password;
    
    public LoginRequest() {}
    
    public LoginRequest(String username, String password) {
        this.username = username;
        this.password = password;
    }
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getPassword() {
        return password;
    }
    
    public void setPassword(String password) {
        this.password = password;
    }
}

改进登录接口:

/**
 * 用户登录(使用DTO)
 */
@PostMapping("/login-dto")
public Result<User> loginWithDto(@Valid @RequestBody LoginRequest request) {
    User user = userService.login(request.getUsername(), request.getPassword());
    return Result.success("登录成功", user);
}

7. 日志记录和监控

7.1 异常日志记录

package com.example.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

/**
 * 日志工具类
 */
public class LogUtil {
    
    private static final Logger logger = LoggerFactory.getLogger(LogUtil.class);
    
    /**
     * 记录异常详细信息
     */
    public static void logException(Exception e, HttpServletRequest request) {
        StringBuilder logMessage = new StringBuilder();
        logMessage.append("\n=== 异常详细信息 ===\n");
        logMessage.append("请求URL: ").append(request.getRequestURL()).append("\n");
        logMessage.append("请求方法: ").append(request.getMethod()).append("\n");
        logMessage.append("客户端IP: ").append(getClientIP(request)).append("\n");
        logMessage.append("User-Agent: ").append(request.getHeader("User-Agent")).append("\n");
        
        // 记录请求参数
        logMessage.append("请求参数: \n");
        Enumeration<String> paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = paramNames.nextElement();
            String paramValue = request.getParameter(paramName);
            logMessage.append("  ").append(paramName).append(" = ").append(paramValue).append("\n");
        }
        
        // 记录请求头
        logMessage.append("请求头: \n");
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            logMessage.append("  ").append(headerName).append(" = ").append(headerValue).append("\n");
        }
        
        logMessage.append("异常信息: ").append(e.getMessage()).append("\n");
        logMessage.append("=== 异常详细信息结束 ===");
        
        logger.error(logMessage.toString(), e);
    }
    
    /**
     * 获取客户端真实IP
     */
    private static String getClientIP(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

7.2 增强的异常处理器

package com.example.exception;

import com.example.common.Result;
import com.example.enums.ErrorCode;
import com.example.util.LogUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * 增强的全局异常处理器
 * 包含详细的日志记录和监控
 */
@RestControllerAdvice
public class EnhancedGlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(EnhancedGlobalExceptionHandler.class);
    
    @Value("${app.debug:false}")
    private boolean debug;
    
    /**
     * 处理业务异常
     */
    @ExceptionHandler(BizException.class)
    @ResponseStatus(HttpStatus.OK)
    public Result<Void> handleBizException(BizException e, HttpServletRequest request) {
        // 业务异常通常是可预期的,使用warn级别
        logger.warn("业务异常 - URL: {}, 错误码: {}, 消息: {}", 
                   request.getRequestURL(), e.getCode(), e.getMessage());
        
        return Result.error(e.getCode(), e.getMessage());
    }
    
    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Object> handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e, HttpServletRequest request) {
        
        logger.warn("参数校验失败 - URL: {}, 错误数量: {}", 
                   request.getRequestURL(), e.getBindingResult().getErrorCount());
        
        // 详细记录校验错误
        StringBuilder errorDetails = new StringBuilder();
        e.getBindingResult().getFieldErrors().forEach(error -> {
            errorDetails.append(String.format("[%s: %s] ", error.getField(), error.getDefaultMessage()));
        });
        logger.debug("校验错误详情: {}", errorDetails.toString());
        
        if (debug) {
            // 开发环境返回详细错误信息
            Map<String, String> errors = new HashMap<>();
            for (FieldError error : e.getBindingResult().getFieldErrors()) {
                errors.put(error.getField(), error.getDefaultMessage());
            }
            return Result.error(ErrorCode.VALIDATION_FAILED.getCode(), "参数校验失败", errors);
        } else {
            // 生产环境返回简化错误信息
            return Result.error(ErrorCode.VALIDATION_FAILED.getCode(), "参数校验失败");
        }
    }
    
    /**
     * 处理系统异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleException(Exception e, HttpServletRequest request) {
        // 记录详细的异常信息
        LogUtil.logException(e, request);
        
        // 发送告警(生产环境)
        if (!debug) {
            sendAlert(e, request);
        }
        
        // 返回用户友好的错误信息
        String message = debug ? e.getMessage() : "系统异常,请联系管理员";
        return Result.error(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), message);
    }
    
    /**
     * 发送告警通知
     */
    private void sendAlert(Exception e, HttpServletRequest request) {
        // 这里可以集成钉钉、企业微信、邮件等告警方式
        logger.error("系统异常告警 - URL: {}, 异常类型: {}", 
                    request.getRequestURL(), e.getClass().getSimpleName());
        
        // 示例:发送到监控系统
        // monitoringService.sendAlert("系统异常", e.getMessage(), request.getRequestURL().toString());
    }
}

7.3 异常统计和监控

package com.example.service;

import org.springframework.stereotype.Service;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 异常统计服务
 */
@Service
public class ExceptionStatisticsService {
    
    // 异常计数器
    private final ConcurrentHashMap<String, AtomicLong> exceptionCounts = new ConcurrentHashMap<>();
    
    // 异常发生时间记录
    private final ConcurrentHashMap<String, Long> lastOccurrenceTime = new ConcurrentHashMap<>();
    
    /**
     * 记录异常
     */
    public void recordException(String exceptionType, String url) {
        // 增加计数
        exceptionCounts.computeIfAbsent(exceptionType, k -> new AtomicLong(0)).incrementAndGet();
        
        // 记录最后发生时间
        lastOccurrenceTime.put(exceptionType, System.currentTimeMillis());
        
        // 记录URL相关的异常
        String urlKey = "URL:" + url;
        exceptionCounts.computeIfAbsent(urlKey, k -> new AtomicLong(0)).incrementAndGet();
    }
    
    /**
     * 获取异常统计信息
     */
    public Map<String, Object> getStatistics() {
        Map<String, Object> statistics = new HashMap<>();
        
        // 异常计数
        Map<String, Long> counts = new HashMap<>();
        exceptionCounts.forEach((key, value) -> counts.put(key, value.get()));
        statistics.put("exceptionCounts", counts);
        
        // 最后发生时间
        statistics.put("lastOccurrenceTime", new HashMap<>(lastOccurrenceTime));
        
        // 总异常数
        long totalExceptions = exceptionCounts.values().stream()
                .mapToLong(AtomicLong::get)
                .sum();
        statistics.put("totalExceptions", totalExceptions);
        
        return statistics;
    }
    
    /**
     * 清理统计数据
     */
    public void clearStatistics() {
        exceptionCounts.clear();
        lastOccurrenceTime.clear();
    }
}

8. 配置和优化

8.1 异常处理配置

# application.yml
app:
  # 是否开启调试模式
  debug: false
  
  # 异常处理配置
  exception:
    # 是否记录详细堆栈信息
    log-stack-trace: true
    # 是否发送告警
    send-alert: true
    # 告警阈值(每分钟异常数量)
    alert-threshold: 10
    # 是否返回详细错误信息给客户端
    return-details: false

# 日志配置
logging:
  level:
    com.example.exception: DEBUG
    com.example.util: DEBUG
  pattern:
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: logs/application.log
    max-size: 10MB
    max-history: 30

# 服务器错误配置
server:
  error:
    # 是否包含异常信息
    include-exception: false
    # 是否包含堆栈跟踪
    include-stacktrace: never
    # 是否包含消息
    include-message: never
    # 错误页面路径
    path: /error

8.2 配置属性类

package com.example.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 异常处理配置属性
 */
@Component
@ConfigurationProperties(prefix = "app.exception")
public class ExceptionProperties {
    
    /**
     * 是否记录详细堆栈信息
     */
    private boolean logStackTrace = true;
    
    /**
     * 是否发送告警
     */
    private boolean sendAlert = true;
    
    /**
     * 告警阈值(每分钟异常数量)
     */
    private int alertThreshold = 10;
    
    /**
     * 是否返回详细错误信息给客户端
     */
    private boolean returnDetails = false;
    
    // getter和setter方法
    public boolean isLogStackTrace() {
        return logStackTrace;
    }
    
    public void setLogStackTrace(boolean logStackTrace) {
        this.logStackTrace = logStackTrace;
    }
    
    public boolean isSendAlert() {
        return sendAlert;
    }
    
    public void setSendAlert(boolean sendAlert) {
        this.sendAlert = sendAlert;
    }
    
    public int getAlertThreshold() {
        return alertThreshold;
    }
    
    public void setAlertThreshold(int alertThreshold) {
        this.alertThreshold = alertThreshold;
    }
    
    public boolean isReturnDetails() {
        return returnDetails;
    }
    
    public void setReturnDetails(boolean returnDetails) {
        this.returnDetails = returnDetails;
    }
}

8.3 异常处理配置类

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;

import java.util.List;

/**
 * 异常处理配置类
 */
@Configuration
public class ExceptionConfig {
    
    /**
     * 配置异常解析器的优先级
     */
    @Bean
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        // 确保ExceptionHandlerExceptionResolver优先级最高
        resolvers.stream()
                .filter(resolver -> resolver instanceof ExceptionHandlerExceptionResolver)
                .findFirst()
                .ifPresent(resolver -> {
                    resolvers.remove(resolver);
                    resolvers.add(0, resolver);
                });
    }
}

9. 测试异常处理

9.1 异常处理单元测试

package com.example.exception;

import com.example.enums.ErrorCode;
import com.example.exception.BizException;
import com.example.exception.CompleteGlobalExceptionHandler;
import com.example.common.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 异常处理器单元测试
 */
class GlobalExceptionHandlerTest {
    
    private CompleteGlobalExceptionHandler exceptionHandler;
    private MockHttpServletRequest request;
    
    @BeforeEach
    void setUp() {
        exceptionHandler = new CompleteGlobalExceptionHandler();
        request = new MockHttpServletRequest();
        request.setRequestURI("/api/test");
        request.setMethod("GET");
    }
    
    @Test
    void testHandleBizException() {
        // 准备测试数据
        BizException exception = new BizException(ErrorCode.USER_NOT_FOUND);
        
        // 执行测试
        Result<Void> result = exceptionHandler.handleBizException(exception, request);
        
        // 验证结果
        assertNotNull(result);
        assertEquals(ErrorCode.USER_NOT_FOUND.getCode(), result.getCode());
        assertEquals(ErrorCode.USER_NOT_FOUND.getMessage(), result.getMessage());
        assertFalse(result.getSuccess());
    }
    
    @Test
    void testHandleNullPointerException() {
        // 准备测试数据
        NullPointerException exception = new NullPointerException("测试空指针异常");
        
        // 执行测试
        Result<Void> result = exceptionHandler.handleNullPointerException(exception, request);
        
        // 验证结果
        assertNotNull(result);
        assertEquals(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), result.getCode());
        assertFalse(result.getSuccess());
    }
    
    @Test
    void testHandleGeneralException() {
        // 准备测试数据
        RuntimeException exception = new RuntimeException("测试运行时异常");
        
        // 执行测试
        Result<Void> result = exceptionHandler.handleException(exception, request);
        
        // 验证结果
        assertNotNull(result);
        assertEquals(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), result.getCode());
        assertFalse(result.getSuccess());
    }
}

9.2 集成测试

package com.example.controller;

import com.example.enums.ErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * 异常处理集成测试
 */
@WebMvcTest(UserController.class)
class ExceptionHandlingIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @MockBean
    private UserService userService;
    
    @Test
    void testUserNotFoundExceptionHandling() throws Exception {
        // 测试用户不存在异常
        mockMvc.perform(get("/api/users/999"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.code").value(ErrorCode.USER_NOT_FOUND.getCode()))
                .andExpect(jsonPath("$.message").exists());
    }
    
    @Test
    void testValidationExceptionHandling() throws Exception {
        // 测试参数校验异常
        String invalidUserJson = """
            {
                "username": "",
                "email": "invalid-email",
                "password": "123"
            }
            """;
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidUserJson))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.code").value(ErrorCode.VALIDATION_FAILED.getCode()))
                .andExpect(jsonPath("$.data").exists());
    }
    
    @Test
    void testMethodArgumentTypeMismatchException() throws Exception {
        // 测试参数类型不匹配异常
        mockMvc.perform(get("/api/users/invalid-id"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST.getCode()));
    }
}

10. 最佳实践

10.1 异常处理设计原则

  1. 统一性:所有异常都通过全局异常处理器处理
  2. 安全性:不暴露敏感的系统信息
  3. 用户友好:提供清晰、有意义的错误消息
  4. 可监控:记录详细的异常信息用于问题排查
  5. 可配置:支持不同环境的不同配置

10.2 异常分类策略

/**
 * 异常分类处理策略
 */
public class ExceptionClassificationStrategy {
    
    /**
     * 业务异常(4xx)
     * - 用户输入错误
     * - 业务规则违反
     * - 资源不存在
     */
    public static boolean isBusinessException(Exception e) {
        return e instanceof BizException ||
               e instanceof ValidationException ||
               e instanceof UserNotFoundException;
    }
    
    /**
     * 系统异常(5xx)
     * - 数据库连接异常
     * - 网络异常
     * - 系统资源异常
     */
    public static boolean isSystemException(Exception e) {
        return e instanceof SQLException ||
               e instanceof IOException ||
               e instanceof OutOfMemoryError;
    }
    
    /**
     * 第三方服务异常
     * - 外部API调用失败
     * - 消息队列异常
     * - 缓存服务异常
     */
    public static boolean isExternalServiceException(Exception e) {
        return e instanceof HttpClientErrorException ||
               e instanceof RedisConnectionException ||
               e instanceof MessagingException;
    }
}

10.3 异常处理最佳实践

package com.example.exception;

import com.example.common.Result;
import com.example.config.ExceptionProperties;
import com.example.service.ExceptionStatisticsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * 最佳实践异常处理器
 */
@RestControllerAdvice
@Order(1) // 设置优先级
public class BestPracticeExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(BestPracticeExceptionHandler.class);
    
    @Autowired
    private ExceptionProperties exceptionProperties;
    
    @Autowired
    private ExceptionStatisticsService statisticsService;
    
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e, HttpServletRequest request) {
        
        // 1. 记录异常统计
        statisticsService.recordException(e.getClass().getSimpleName(), request.getRequestURI());
        
        // 2. 根据配置决定日志级别
        if (exceptionProperties.isLogStackTrace()) {
            logger.error("异常详情", e);
        } else {
            logger.error("异常: {} - {}", e.getClass().getSimpleName(), e.getMessage());
        }
        
        // 3. 根据异常类型选择不同的处理策略
        if (ExceptionClassificationStrategy.isBusinessException(e)) {
            return handleBusinessException(e, request);
        } else if (ExceptionClassificationStrategy.isSystemException(e)) {
            return handleSystemException(e, request);
        } else if (ExceptionClassificationStrategy.isExternalServiceException(e)) {
            return handleExternalServiceException(e, request);
        } else {
            return handleUnknownException(e, request);
        }
    }
    
    private Result<Void> handleBusinessException(Exception e, HttpServletRequest request) {
        logger.warn("业务异常 - URL: {}, 异常: {}", request.getRequestURI(), e.getMessage());
        
        String message = exceptionProperties.isReturnDetails() ? e.getMessage() : "业务处理失败";
        return Result.error(400, message);
    }
    
    private Result<Void> handleSystemException(Exception e, HttpServletRequest request) {
        logger.error("系统异常 - URL: {}", request.getRequestURI(), e);
        
        // 发送告警
        if (exceptionProperties.isSendAlert()) {
            sendSystemAlert(e, request);
        }
        
        return Result.error(500, "系统暂时不可用,请稍后重试");
    }
    
    private Result<Void> handleExternalServiceException(Exception e, HttpServletRequest request) {
        logger.error("外部服务异常 - URL: {}", request.getRequestURI(), e);
        
        return Result.error(503, "服务暂时不可用,请稍后重试");
    }
    
    private Result<Void> handleUnknownException(Exception e, HttpServletRequest request) {
        logger.error("未知异常 - URL: {}", request.getRequestURI(), e);
        
        // 未知异常需要特别关注
        if (exceptionProperties.isSendAlert()) {
            sendCriticalAlert(e, request);
        }
        
        return Result.error(500, "系统异常,请联系管理员");
    }
    
    private void sendSystemAlert(Exception e, HttpServletRequest request) {
        // 实现系统告警逻辑
        logger.warn("发送系统告警: {}", e.getMessage());
    }
    
    private void sendCriticalAlert(Exception e, HttpServletRequest request) {
        // 实现紧急告警逻辑
        logger.error("发送紧急告警: {}", e.getMessage());
    }
}

10.4 异常处理建议

  1. 错误消息国际化
@Component
public class MessageService {
    
    @Autowired
    private MessageSource messageSource;
    
    public String getMessage(String key, Object... args) {
        return messageSource.getMessage(key, args, LocaleContextHolder.getLocale());
    }
}

// 在异常处理器中使用
@Autowired
private MessageService messageService;

@ExceptionHandler(BizException.class)
public Result<Void> handleBizException(BizException e) {
    String message = messageService.getMessage("error.business", e.getMessage());
    return Result.error(e.getCode(), message);
}
  1. 异常重试机制
@Service
public class RetryableService {
    
    @Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public String processWithRetry() throws Exception {
        // 可能失败的操作
        if (Math.random() > 0.7) {
            throw new RuntimeException("随机失败");
        }
        return "处理成功";
    }
    
    @Recover
    public String recover(Exception e) {
        logger.error("重试后仍然失败", e);
        throw new BizException(ErrorCode.EXTERNAL_SERVICE_ERROR, "服务暂时不可用");
    }
}
  1. 异常降级处理
@Service
public class FallbackService {
    
    public String getDataWithFallback(String key) {
        try {
            return externalService.getData(key);
        } catch (Exception e) {
            logger.warn("外部服务调用失败,使用降级方案", e);
            return getCachedData(key);
        }
    }
    
    private String getCachedData(String key) {
        // 从缓存获取数据
        return "缓存数据";
    }
}

11. 总结

11.1 核心知识点回顾

  1. 异常处理机制:@ExceptionHandler、@ControllerAdvice、@RestControllerAdvice
  2. 异常分类:业务异常、系统异常、第三方服务异常
  3. 统一响应:使用统一的Result类格式化响应
  4. 日志记录:详细记录异常信息用于问题排查
  5. 监控告警:异常统计和实时告警机制

11.2 应用场景

  • API接口异常处理:统一处理REST接口的各种异常
  • 参数校验异常:处理@Valid校验失败的情况
  • 业务逻辑异常:处理业务规则验证失败
  • 系统异常处理:处理数据库连接、网络异常等
  • 第三方服务异常:处理外部API调用失败

11.3 学习建议

  1. 理解原理:掌握Spring异常处理的底层机制
  2. 分类处理:根据异常类型采用不同的处理策略
  3. 用户友好:始终考虑用户体验,提供有意义的错误信息
  4. 安全考虑:不暴露敏感的系统信息
  5. 监控完善:建立完善的异常监控和告警机制

SpringBoot的统一异常处理是构建健壮Web应用的重要基础。通过合理的异常处理机制,可以提升用户体验、保证系统安全、便于问题排查和系统维护。记住异常处理不仅仅是技术实现,更是用户体验和系统稳定性的重要保障!


网站公告

今日签到

点亮在社区的每一天
去签到