文章目录
1. 异常处理基础概念
1.1 什么是异常处理
异常处理是程序在运行过程中遇到错误时的处理机制。在Web应用中,异常处理负责捕获和处理各种异常情况,并向客户端返回友好的错误信息。
形象比喻:
想象异常处理就像医院的急诊科:
- 分诊台(@ExceptionHandler):根据病情类型分配到不同科室
- 专科医生(具体异常处理器):针对特定问题进行专业处理
- 病历记录(日志记录):记录处理过程和结果
- 出院小结(统一响应格式):给病人明确的诊断和建议
1.2 为什么需要统一异常处理
- 用户体验:避免直接暴露技术错误信息给用户
- 安全性:防止敏感信息泄露(如数据库结构、系统路径等)
- 维护性:集中处理异常逻辑,便于维护和修改
- 一致性:确保所有错误响应格式统一
- 监控性:便于日志记录和错误追踪
1.3 Spring异常处理机制
Spring提供了多种异常处理方式:
- @ExceptionHandler:方法级异常处理
- @ControllerAdvice:全局异常处理
- @RestControllerAdvice:REST接口全局异常处理
- 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 异常处理设计原则
- 统一性:所有异常都通过全局异常处理器处理
- 安全性:不暴露敏感的系统信息
- 用户友好:提供清晰、有意义的错误消息
- 可监控:记录详细的异常信息用于问题排查
- 可配置:支持不同环境的不同配置
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 异常处理建议
- 错误消息国际化
@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);
}
- 异常重试机制
@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, "服务暂时不可用");
}
}
- 异常降级处理
@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 核心知识点回顾
- 异常处理机制:@ExceptionHandler、@ControllerAdvice、@RestControllerAdvice
- 异常分类:业务异常、系统异常、第三方服务异常
- 统一响应:使用统一的Result类格式化响应
- 日志记录:详细记录异常信息用于问题排查
- 监控告警:异常统计和实时告警机制
11.2 应用场景
- API接口异常处理:统一处理REST接口的各种异常
- 参数校验异常:处理@Valid校验失败的情况
- 业务逻辑异常:处理业务规则验证失败
- 系统异常处理:处理数据库连接、网络异常等
- 第三方服务异常:处理外部API调用失败
11.3 学习建议
- 理解原理:掌握Spring异常处理的底层机制
- 分类处理:根据异常类型采用不同的处理策略
- 用户友好:始终考虑用户体验,提供有意义的错误信息
- 安全考虑:不暴露敏感的系统信息
- 监控完善:建立完善的异常监控和告警机制
SpringBoot的统一异常处理是构建健壮Web应用的重要基础。通过合理的异常处理机制,可以提升用户体验、保证系统安全、便于问题排查和系统维护。记住异常处理不仅仅是技术实现,更是用户体验和系统稳定性的重要保障!