CC 咖啡猫的工作空间 Coding Space

异常处理实践

异常处理是系统健壮性的保障,全局异常管理让代码更清晰、问题可追溯。


1. 异常分类

1.1 异常层次结构

Throwable
├── Error(系统级错误,不应该捕获)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
├── Exception
│   ├── RuntimeException( unchecked,不强制声明)
│   │   ├── BusinessException    ← 业务异常,可捕获处理
│   │   ├── IllegalArgumentException
│   │   └── ...
│   └── IOException / SQLException( checked,强制声明)
└   └── 业务异常基类

1.2 项目中的异常定义

// 业务异常(用户可感知)
public class BusinessException extends RuntimeException {
    private String code;
    private String message;

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }
}

// 系统异常(需要记录日志)
public class SystemException extends RuntimeException {
    private String code;

    public SystemException(String code, String message) {
        super(message);
        this.code = code;
    }
}

2. 全局异常处理

2.1 Spring Boot 全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 业务异常
    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    // 参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining(", "));
        log.warn("参数校验异常: {}", message);
        return Result.error(400, message);
    }

    // 断言异常
    @ExceptionHandler(IllegalArgumentException.class)
    public Result handleIllegalArgumentException(IllegalArgumentException e) {
        log.warn("参数异常: {}", e.getMessage());
        return Result.error(400, e.getMessage());
    }

    // 系统异常
    @ExceptionHandler(SystemException.class)
    public Result handleSystemException(SystemException e) {
        log.error("系统异常: code={}, message={}", e.getCode(), e.getMessage(), e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }

    // 空指针异常
    @ExceptionHandler(NullPointerException.class)
    public Result handleNullPointerException(NullPointerException e) {
        log.error("空指针异常", e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }

    // 未知异常(兜底)
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("未知异常", e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }
}

2.2 统一响应结构

public class Result<T> {
    private int code;
    private String message;
    private T data;
    private long timestamp;

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        result.setTimestamp(System.currentTimeMillis());
        return result;
    }

    public static <T> Result<T> error(int code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(null);
        result.setTimestamp(System.currentTimeMillis());
        return result;
    }

    public static <T> Result<T> error(String code, String message) {
        Result<T> result = new Result<>();
        result.setCode(-1);
        result.setMessage(message);
        result.setData(null);
        result.setTimestamp(System.currentTimeMillis());
        return result;
    }
}

3. 异常页面处理

3.1 HTML 错误页面

// 在全局异常处理中,返回 JSON 或 HTML
@ExceptionHandler(NoHandlerFoundException.class)
public Result handleNotFoundException(NoHandlerFoundException e) {
    // 判断是否 AJAX 请求
    if (isAjaxRequest()) {
        return Result.error(404, "接口不存在");
    }
    // 返回 HTML 页面(可配置 error/404.html)
    return Result.error(404, "页面不存在");
}

3.2 错误页面配置

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

4. 异常日志规范

4.1 日志记录原则

异常类型 日志级别 记录内容
业务异常 WARN code + message
参数异常 WARN 参数校验信息
系统异常 ERROR 完整堆栈信息
未知异常 ERROR 完整堆栈信息

4.2 日志脱敏

@Aspect
@Component
public class ExceptionLogAspect {

    @Around("execution(* com.example..*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            return joinPoint.proceed();
        } catch (Exception e) {
            // 脱敏后记录日志
            String params = sanitizeParams(joinPoint.getArgs());
            log.error("方法: {}, 参数: {}, 异常: {}",
                joinPoint.getSignature(),
                params,
                e.getMessage());
            throw e;
        }
    }

    private String sanitizeParams(Object[] args) {
        // 脱敏手机号、身份证、密码等敏感信息
        return JSON.toJSONString(args);
    }
}

5. 自定义业务异常

5.1 错误码枚举

public enum ErrorCode {
    // 通用错误 1xxx
    PARAM_INVALID(1001, "参数错误"),
    DATA_NOT_FOUND(1002, "数据不存在"),
    DATA_DUPLICATE(1003, "数据重复"),

    // 业务错误 2xxx
    USER_NOT_FOUND(2001, "用户不存在"),
    USER_DISABLED(2002, "用户已被禁用"),
    PASSWORD_ERROR(2003, "密码错误"),

    // 系统错误 9xxx
    SYSTEM_ERROR(9001, "系统错误"),
    SERVICE_UNAVAILABLE(9002, "服务不可用");

    private final int code;
    private final String message;

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

5.2 业务异常工厂

public class BizException {

    public static BusinessException fail(ErrorCode errorCode) {
        return new BusinessException(String.valueOf(errorCode.getCode()), errorCode.getMessage());
    }

    public static BusinessException fail(ErrorCode errorCode, String message) {
        return new BusinessException(String.valueOf(errorCode.getCode()), message);
    }

    public static BusinessException fail(int code, String message) {
        return new BusinessException(String.valueOf(code), message);
    }
}

6. 生产环境异常处理

6.1 异常隔离

// 不同模块使用不同的异常处理器
@RestControllerAdvice(basePackages = {"com.example.user"})
public class UserExceptionHandler {
    // 处理用户模块异常
}

@RestControllerAdvice(basePackages = {"com.example.order"})
public class OrderExceptionHandler {
    // 处理订单模块异常
}

6.2 异常告警

@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
    // 发送告警(钉钉/企微/短信)
    if (isCriticalException(e)) {
        alertService.sendAlert("系统异常", e.getMessage());
    }
    return Result.error(500, "系统繁忙,请稍后重试");
}

private boolean isCriticalException(Exception e) {
    return e instanceof SystemException
        || e instanceof NullPointerException
        || e instanceof OutOfMemoryError;
}

6.3 异常链路追踪

@ExceptionHandler(Exception.class)
public Result handleException(Exception e, HttpServletRequest request) {
    // 生成异常唯一ID,便于追踪
    String traceId = UUID.randomUUID().toString().replace("-", "");
    log.error("异常ID: {}, 请求: {}, 异常: {}",
        traceId,
        request.getRequestURI(),
        e);

    // 返回异常ID给前端,便于反馈问题
    return Result.error(500, "系统繁忙,请稍后重试")
        .setTraceId(traceId);
}

7. 注意事项

  1. 不要捕获所有异常:Error 应该让 JVM 处理
  2. 不要只 throw 不 catch:业务异常要有明确的处理
  3. 异常要包含上下文:便于排查问题
  4. 生产环境不暴露堆栈:对外只返回 message
  5. 异常要有错误码:便于前端判断和处理

最后更新:2026/05/13