文章目录
- 1. 异常处理基础概念
- 1.1 什么是异常处理
- 1.2 为什么需要统一异常处理
- 1.3 Spring异常处理机制
- 2. SpringBoot默认异常处理
- 2.1 默认错误页面
- 2.2 自定义错误页面
- 3. 全局异常处理器
- 3.1 基础全局异常处理器
- 3.2 统一响应格式
- 3.3 使用统一响应格式的异常处理器
- 4. 自定义异常
- 4.1 创建基础自定义异常
- 4.2 具体的业务异常类
- 4.3 错误码枚举
- 4.4 使用错误码的异常类
- 5. 完整的异常处理体系
- 5.1 完整的全局异常处理器
- 5.2 改进的Result类
- 6. 实际应用示例
- 6.1 用户服务示例
- 6.2 用户服务类
- 6.3 用户控制器
- 6.4 登录请求DTO
- 7. 日志记录和监控
- 7.1 异常日志记录
- 7.2 增强的异常处理器
- 7.3 异常统计和监控
- 8. 配置和优化
- 8.1 异常处理配置
- 8.2 配置属性类
- 8.3 异常处理配置类
- 9. 测试异常处理
- 9.1 异常处理单元测试
- 9.2 集成测试
- 10. 最佳实践
- 10.1 异常处理设计原则
- 10.2 异常分类策略
- 10.3 异常处理最佳实践
- 10.4 异常处理建议
- 11. 总结
- 11.1 核心知识点回顾
- 11.2 应用场景
- 11.3 学习建议
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; // 这里会抛出ArithmeticExceptionreturn result + "";}/*** 模拟参数类型错误*/@GetMapping("/users/{id}")public String getUserById(@PathVariable Long id) {return "User ID: " + id;}
}
当访问这些端点时,SpringBoot会返回默认的错误信息,但这些信息对用户不够友好。
2.2 自定义错误页面
可以通过配置文件定制默认错误页面:
# application.yml
server:error:include-message: alwaysinclude-binding-errors: alwaysinclude-stacktrace: on_paraminclude-exception: falsepath: /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;}@Overridepublic 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, "操作成功"),// 客户端错误 4xxBAD_REQUEST(400, "请求参数错误"),UNAUTHORIZED(401, "未授权"),FORBIDDEN(403, "权限不足"),NOT_FOUND(404, "资源不存在"),METHOD_NOT_ALLOWED(405, "请求方法不支持"),CONFLICT(409, "数据冲突"),VALIDATION_FAILED(422, "参数校验失败"),// 服务器错误 5xxINTERNAL_SERVER_ERROR(500, "系统内部错误"),SERVICE_UNAVAILABLE(503, "服务不可用"),// 业务错误 6xxxUSER_NOT_FOUND(6001, "用户不存在"),USER_ALREADY_EXISTS(6002, "用户已存在"),INVALID_PASSWORD(6003, "密码错误"),ACCOUNT_LOCKED(6004, "账户已锁定"),ACCOUNT_EXPIRED(6005, "账户已过期"),// 数据错误 7xxxDATA_NOT_FOUND(7001, "数据不存在"),DATA_ALREADY_EXISTS(7002, "数据已存在"),DATA_INTEGRITY_VIOLATION(7003, "数据完整性约束冲突"),// 第三方服务错误 8xxxEXTERNAL_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 {@Autowiredprivate 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);}/*** 创建用户*/@PostMappingpublic 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("用户删除成功");}/*** 获取所有用户*/@GetMappingpublic 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: DEBUGcom.example.util: DEBUGpattern:file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"file:name: logs/application.logmax-size: 10MBmax-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 {/*** 配置异常解析器的优先级*/@Beanpublic 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;@BeforeEachvoid setUp() {exceptionHandler = new CompleteGlobalExceptionHandler();request = new MockHttpServletRequest();request.setRequestURI("/api/test");request.setMethod("GET");}@Testvoid 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());}@Testvoid 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());}@Testvoid 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 {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@MockBeanprivate UserService userService;@Testvoid 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());}@Testvoid 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());}@Testvoid 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);@Autowiredprivate ExceptionProperties exceptionProperties;@Autowiredprivate 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 {@Autowiredprivate 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 "处理成功";}@Recoverpublic 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应用的重要基础。通过合理的异常处理机制,可以提升用户体验、保证系统安全、便于问题排查和系统维护。记住异常处理不仅仅是技术实现,更是用户体验和系统稳定性的重要保障!