--- title: "02-异常处理最佳实践代码" created: 2025-12-01 tags: - 项目 aliases: - 异常处理最佳实践代码 --- # 异常处理最佳实践代码 ## **为什么需要全局异常处理和通用返回类** **全局异常处理**的设计目标是提供统一的异常捕获、处理机制,确保在应用中出现异常时能够进行集中管理并返回规范的错误信息。它不仅帮助开发者在不同地方减少重复代码,还提升了系统的可维护性和可扩展性。 以下是全局异常处理的几个优势: - **一致性:** 保证错误响应的格式和结构一致,无论是在前端还是后端。 - **集中管理:** 所有异常都可以在一个地方处理,减少了分散管理的复杂性。 - **日志记录:** 统一的异常捕获能更好地进行日志记录,方便后期排查问题。 - **用户体验:** 提供了清晰且统一的错误信息,帮助前端进行更好的用户提示。 **通用返回类**(如 `BaseResponse`)用于封装请求结果的响应数据,并规范返回结构,确保每个接口的响应一致。 设计通用返回类的原因如下: - **统一结构:** 所有接口返回的数据都符合统一格式,使前端处理更加简单。 - **扩展性:** 返回类支持链式调用,可以便捷地添加额外字段或修改已有字段。 - **简化代码:** 使用通用返回类可以避免每次接口响应都编写重复代码。 ## 错误码枚举 `ErrorCode.java` ```java package com.example.common.exception; /** * 错误码枚举 * 设计规范: * - 0: 成功 * - 4xxxx: 客户端错误(4开头) * - 5xxxx: 服务器错误(5开头) * - 1xxxx-3xxxx: 业务错误(按模块分类) */ public enum ErrorCode { // ============ 成功 ============ SUCCESS(0, "success", "请求成功", ErrorType.SUCCESS), // ============ 系统级错误 (4xxxx-5xxxx) ============ PARAMS_ERROR(40000, "request params error", "请求参数错误", ErrorType.CLIENT), NULL_ERROR(40001, "request data is null", "请求数据为空", ErrorType.CLIENT), UNAUTHORIZED(40100, "unauthorized", "未登录或令牌失效", ErrorType.CLIENT), FORBIDDEN(40300, "forbidden", "无权限访问", ErrorType.CLIENT), NOT_FOUND(40400, "not found", "资源不存在", ErrorType.CLIENT), SYSTEM_ERROR(50000, "system error", "系统内部错误", ErrorType.SERVER), DATABASE_ERROR(50100, "database error", "数据库错误", ErrorType.SERVER), EXTERNAL_SERVICE_ERROR(50200, "external service error", "外部服务调用失败", ErrorType.SERVER), CACHE_ERROR(50300, "cache error", "缓存操作失败", ErrorType.SERVER), // ============ 用户模块 (1xxxx) ============ USER_NOT_FOUND(10001, "user not found", "用户不存在", ErrorType.CLIENT), USER_ALREADY_EXISTS(10002, "user already exists", "用户已存在", ErrorType.CLIENT), PASSWORD_ERROR(10003, "password error", "密码错误", ErrorType.CLIENT), EMAIL_ALREADY_EXISTS(10004, "email already exists", "邮箱已存在", ErrorType.CLIENT), INVALID_TOKEN(10005, "invalid token", "无效的令牌", ErrorType.CLIENT), // ============ 作品模块 (2xxxx) ============ ARTWORK_NOT_FOUND(20001, "artwork not found", "作品不存在", ErrorType.CLIENT), ARTWORK_UPLOAD_FAILED(20002, "artwork upload failed", "作品上传失败", ErrorType.SERVER), ARTWORK_PERMISSION_DENIED(20003, "artwork permission denied", "无权操作该作品", ErrorType.CLIENT), // ============ 拍卖模块 (3xxxx) ============ AUCTION_NOT_FOUND(30001, "auction not found", "拍卖不存在", ErrorType.CLIENT), BID_PRICE_TOO_LOW(30002, "bid price too low", "出价过低", ErrorType.CLIENT), AUCTION_CLOSED(30003, "auction closed", "拍卖已结束", ErrorType.CLIENT); private final int code; private final String message; private final String description; private final ErrorType type; /** * 错误类型枚举 * CLIENT: 客户端错误(4xxxx),可返回给前端展示 * SERVER: 服务器错误(5xxxx),需要隐藏敏感信息,返回Trace ID用于追踪 * SUCCESS: 成功 */ public enum ErrorType { CLIENT, SERVER, SUCCESS } ErrorCode(int code, String message, String description, ErrorType type) { this.code = code; this.message = message; this.description = description; this.type = type; } public int getCode() { return code; } public String getMessage() { return message; } public String getDescription() { return description; } public ErrorType getType() { return type; } } ``` ## 统一响应格式 `BaseResponse.java` ```java package com.example.common.response; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * 统一响应格式 * @param 数据泛型 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class BaseResponse implements Serializable { private static final long serialVersionUID = 1L; /** * 状态码(必填) */ private int code; /** * 消息(必填,简短提示) */ private String message; /** * 描述(可选,详细说明) */ private String description; /** * 响应数据(可选,成功时返回) */ private T data; /** * 响应时间戳(必填,单位毫秒) */ private long timestamp; /** * Trace ID(可选,服务器错误时返回,用于问题追踪) */ private String traceId; } ``` ## 自定义业务异常 `BusinessException.java` ```java package com.example.common.exception; /** * 业务异常 * 用于表示业务逻辑中的各种异常情况 * 异常被全局处理器捕获后,会转换成统一的响应格式 */ public class BusinessException extends RuntimeException { private final int code; private final String description; private final ErrorCode.ErrorType errorType; /** * 使用错误码构造 */ public BusinessException(ErrorCode errorCode) { this(errorCode.getMessage(), errorCode.getCode(), errorCode.getDescription(), errorCode.getType()); } /** * 使用错误码和自定义描述构造 */ public BusinessException(ErrorCode errorCode, String description) { this(errorCode.getMessage(), errorCode.getCode(), description, errorCode.getType()); } /** * 完整构造(一般不直接使用) */ public BusinessException(String message, int code, String description, ErrorCode.ErrorType errorType) { super(message); this.code = code; this.description = description; this.errorType = errorType; } public int getCode() { return code; } public String getDescription() { return description; } public ErrorCode.ErrorType getErrorType() { return errorType; } @Override public String toString() { return String.format("BusinessException [code=%d, message=%s, description=%s, type=%s]", code, getMessage(), description, errorType); } } ``` ## 快速异常抛出工具 `ThrowUtils.java` ```java package com.canvaschain.common.exception; import com.canvaschain.common.response.ErrorCode; /** * 异常抛出工具类 * 简化异常抛出的语法,使代码更简洁 * * 使用示例: * ThrowUtils.throwIf(user == null, ErrorCode.USER_NOT_FOUND); * ThrowUtils.throwIf(price <= 0, ErrorCode.BID_PRICE_TOO_LOW, "价格必须大于0"); */ public class ThrowUtils { /** * 条件成立则抛异常(基础方法) */ public static void throwIf(boolean condition, RuntimeException exception) { if (condition) { throw exception; } } /** * 条件成立则抛业务异常 */ public static void throwIf(boolean condition, ErrorCode errorCode) { if (condition) { throw new BusinessException(errorCode); } } /** * 条件成立则抛业务异常(支持自定义描述) */ public static void throwIf(boolean condition, ErrorCode errorCode, String description) { if (condition) { throw new BusinessException(errorCode, description); } } /** * 对象为null时抛异常 */ public static T throwIfNull(T obj, ErrorCode errorCode) { if (obj == null) { throw new BusinessException(errorCode); } return obj; } /** * 对象为null时抛异常(支持自定义描述) */ public static T throwIfNull(T obj, ErrorCode errorCode, String description) { if (obj == null) { throw new BusinessException(errorCode, description); } return obj; } /** * 字符串为空时抛异常 */ public static String throwIfBlank(String str, ErrorCode errorCode) { if (str == null || str.trim().isEmpty()) { throw new BusinessException(errorCode); } return str; } /** * 字符串为空时抛异常(支持自定义描述) */ public static String throwIfBlank(String str, ErrorCode errorCode, String description) { if (str == null || str.trim().isEmpty()) { throw new BusinessException(errorCode, description); } return str; } /** * 集合为空时抛异常 */ public static T throwIfEmpty(T collection, ErrorCode errorCode) { if (collection == null || (collection instanceof java.util.Collection && ((java.util.Collection) collection).isEmpty())) { throw new BusinessException(errorCode); } return collection; } /** * 集合为空时抛异常(支持自定义描述) */ public static T throwIfEmpty(T collection, ErrorCode errorCode, String description) { if (collection == null || (collection instanceof java.util.Collection && ((java.util.Collection) collection).isEmpty())) { throw new BusinessException(errorCode, description); } return collection; } } ``` ## 响应构造工具 `ResultUtils.java` ```java package com.example.common.response; import com.example.common.exception.ErrorCode; /** * 响应工具类 * 提供简洁的API用于构造各种响应对象 */ public class ResultUtils { /** * 成功响应(返回数据) */ public static BaseResponse success(T data) { return BaseResponse.builder() .code(ErrorCode.SUCCESS.getCode()) .message(ErrorCode.SUCCESS.getMessage()) .description(null) .data(data) .timestamp(System.currentTimeMillis()) .build(); } /** * 成功响应(无数据) */ public static BaseResponse success() { return success(null); } /** * 失败响应(使用错误码) */ public static BaseResponse error(ErrorCode errorCode) { return BaseResponse.builder() .code(errorCode.getCode()) .message(errorCode.getMessage()) .description(errorCode.getDescription()) .timestamp(System.currentTimeMillis()) .build(); } /** * 失败响应(使用错误码和自定义描述) */ public static BaseResponse error(ErrorCode errorCode, String description) { return BaseResponse.builder() .code(errorCode.getCode()) .message(errorCode.getMessage()) .description(description) .timestamp(System.currentTimeMillis()) .build(); } /** * 失败响应(自定义code和message) */ public static BaseResponse error(int code, String message, String description) { return BaseResponse.builder() .code(code) .message(message) .description(description) .timestamp(System.currentTimeMillis()) .build(); } /** * 服务器错误响应(包含Trace ID) */ public static BaseResponse serverError(String traceId, String description) { return BaseResponse.builder() .code(ErrorCode.SYSTEM_ERROR.getCode()) .message("System error, please contact support") .description("系统错误,请联系管理员") .timestamp(System.currentTimeMillis()) .traceId(traceId) .build(); } } ``` ## 全局异常处理器 `GlobalExceptionHandler.java` ```java package com.zwnsyw.zwwwspringbootbasetemplate.exception; import com.zwnsyw.zwwwspringbootbasetemplate.common.response.BaseResponse; import com.zwnsyw.zwwwspringbootbasetemplate.common.response.ResultUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; import java.util.UUID; import java.util.stream.Collectors; /** * 全局异常处理器 *

* 功能: * 1. 捕获所有异常,转换为统一响应格式 * 2. 记录详细的堆栈信息,精确定位代码行号 * 3. 区分客户端错误和服务端错误,进行不同处理 * 4. 服务端错误生成 TraceId 用于问题追踪 *

* * @author your-name */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // ==================== 业务异常处理 ==================== /** * 处理自定义业务异常 */ @ExceptionHandler(BusinessException.class) public BaseResponse handleBusinessException(BusinessException e, HttpServletRequest request) { logBusinessException(e, request); return handleByErrorType(e.getErrorType(), e.getCode(), e.getMessage(), e.getDescription()); } /** * 处理运行时异常 *

* 特殊处理:检查是否是包装的 BusinessException * 场景:CompletableFuture 等异步操作会将 BusinessException 包装成 RuntimeException *

*/ @ExceptionHandler(RuntimeException.class) public BaseResponse handleRuntimeException(RuntimeException e, HttpServletRequest request) { // 检查是否是包装的 BusinessException Throwable cause = e.getCause(); if (cause instanceof BusinessException) { return handleBusinessException((BusinessException) cause, request); } // 普通运行时异常按系统异常处理 String traceId = UUID.randomUUID().toString(); logUnexpectedException(e, request, traceId); return ResultUtils.serverError(traceId, "系统异常"); } // ==================== 参数校验异常处理 ==================== /** * 处理参数验证异常(@RequestBody + @Valid) */ @ExceptionHandler(MethodArgumentNotValidException.class) public BaseResponse handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) { String fieldErrors = extractFieldErrors(e.getBindingResult()); log.warn("Parameter validation failed at [{}] {} - Errors: {}", request.getMethod(), request.getRequestURI(), fieldErrors); return ResultUtils.error(ErrorCode.PARAMS_ERROR, fieldErrors); } /** * 处理参数绑定异常(@ModelAttribute / 表单提交) */ @ExceptionHandler(BindException.class) public BaseResponse handleBindException(BindException e, HttpServletRequest request) { String fieldErrors = extractFieldErrors(e.getBindingResult()); log.warn("Parameter binding failed at [{}] {} - Errors: {}", request.getMethod(), request.getRequestURI(), fieldErrors); return ResultUtils.error(ErrorCode.PARAMS_ERROR, fieldErrors); } /** * 处理非法参数异常 */ @ExceptionHandler(IllegalArgumentException.class) public BaseResponse handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) { logDetailedException(e, request, "warn"); return ResultUtils.error(ErrorCode.PARAMS_ERROR, e.getMessage()); } // ==================== 兜底异常处理 ==================== /** * 处理所有未捕获的异常(兜底) */ @ExceptionHandler(Exception.class) public BaseResponse handleException(Exception e, HttpServletRequest request) { String traceId = UUID.randomUUID().toString(); logUnexpectedException(e, request, traceId); return ResultUtils.serverError(traceId, "系统异常"); } // ==================== 响应生成 ==================== /** * 根据错误类型生成不同响应 *

* CLIENT 错误:返回详细信息供前端展示 * SERVER 错误:隐藏敏感信息,返回 TraceId 用于追踪 *

*/ private BaseResponse handleByErrorType(ErrorCode.ErrorType errorType, int code, String message, String description) { switch (errorType) { case CLIENT: return ResultUtils.error(code, message, description); case SERVER: String traceId = UUID.randomUUID().toString(); log.error("Server error occurred, TraceId: {}, Description: {}", traceId, description); return ResultUtils.serverError(traceId, description); default: return ResultUtils.error(code, message, description); } } // ==================== 日志记录 ==================== /** * 记录业务异常日志 */ private void logBusinessException(BusinessException e, HttpServletRequest request) { StackTraceElement element = getThrowingElement(e); // 客户端错误用 warn,服务端错误用 error if (e.getErrorType() == ErrorCode.ErrorType.CLIENT) { log.warn("Business exception at [{}] {} - Code: {}, Message: {}, Location: {}:{}#{}()", request.getMethod(), request.getRequestURI(), e.getCode(), e.getMessage(), element.getClassName(), element.getLineNumber(), element.getMethodName()); } else { log.error("Business exception at [{}] {} - Code: {}, Message: {}, Location: {}:{}#{}()", request.getMethod(), request.getRequestURI(), e.getCode(), e.getMessage(), element.getClassName(), element.getLineNumber(), element.getMethodName(), e); } } /** * 记录详细异常日志(带堆栈定位) */ private void logDetailedException(Exception e, HttpServletRequest request, String level) { StackTraceElement element = getThrowingElement(e); String logMessage = String.format("Exception at [%s] %s - Type: %s, Message: %s, Location: %s:%d#%s(), IP: %s", request.getMethod(), request.getRequestURI(), e.getClass().getSimpleName(), e.getMessage(), element.getClassName(), element.getLineNumber(), element.getMethodName(), getClientIp(request)); if ("warn".equals(level)) { log.warn(logMessage); } else { log.error(logMessage, e); } } /** * 记录未预期异常的详细日志(含完整堆栈) */ private void logUnexpectedException(Exception e, HttpServletRequest request, String traceId) { StackTraceElement element = getThrowingElement(e); log.error("Unexpected exception [TraceId: {}] at [{}] {} - Type: {}, Message: {}, Location: {}:{}#{}(), IP: {}", traceId, request.getMethod(), request.getRequestURI(), e.getClass().getSimpleName(), e.getMessage(), element.getClassName(), element.getLineNumber(), element.getMethodName(), getClientIp(request), e); // 最后一个参数打印完整堆栈 } // ==================== 工具方法 ==================== /** * 提取字段验证错误信息 */ private String extractFieldErrors(BindingResult bindingResult) { return bindingResult.getFieldErrors().stream() .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage())) .collect(Collectors.joining(" | ")); } /** * 获取异常的最相关堆栈信息 *

* 过滤规则: * 1. 排除 ThrowUtils 工具类(因为异常是从工具类抛出的,需要找到真正调用处) * 2. 排除 Spring 框架类 * 3. 排除 Java 反射相关类 * 4. 排除动态代理类 *

*/ private StackTraceElement getThrowingElement(Throwable e) { StackTraceElement[] stackTrace = e.getStackTrace(); for (StackTraceElement element : stackTrace) { String className = element.getClassName(); // 跳过需要过滤的类 if (shouldSkipClass(className)) { continue; } return element; } // 如果全部被过滤,返回第一个或默认值 return stackTrace.length > 0 ? stackTrace[0] : new StackTraceElement("Unknown", "unknown", "Unknown.java", -1); } /** * 判断是否应该跳过该类 */ private boolean shouldSkipClass(String className) { return className.contains(".ThrowUtils") || // 工具类 className.contains("org.springframework") || // Spring 框架 className.contains("java.lang.reflect") || // 反射 className.contains("sun.reflect") || // Sun 反射 className.contains("$Proxy") || // JDK 代理 className.contains("$$EnhancerBySpringCGLIB$$") || // CGLIB 代理 className.contains("$$FastClassBySpringCGLIB$$"); // CGLIB FastClass } /** * 获取客户端真实 IP 地址 *

* 优先级:X-Forwarded-For > X-Real-IP > Proxy-Client-IP > WL-Proxy-Client-IP > RemoteAddr *

*/ private String getClientIp(HttpServletRequest request) { String[] headerNames = { "X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR" }; for (String header : headerNames) { String ip = request.getHeader(header); if (isValidIp(ip)) { // X-Forwarded-For 可能包含多个 IP,取第一个 return ip.split(",")[0].trim(); } } return request.getRemoteAddr(); } /** * 判断 IP 是否有效 */ private boolean isValidIp(String ip) { return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip); } } ``` ## 使用示例 ### `ServiceImpl`使用示例 ```java package com.example.user.service.impl; import com.example.common.exception.ErrorCode; import com.example.common.exception.ThrowUtils; import com.example.user.entity.User; import com.example.user.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserServiceImpl { @Autowired private UserMapper userMapper; /** * 示例1:基础使用 */ public User getUserById(Long userId) { // 参数校验 ThrowUtils.throwIf(userId == null || userId <= 0, ErrorCode.PARAMS_ERROR); // 查询用户 User user = userMapper.selectById(userId); // 业务检查 ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND); return user; } /** * 示例2:创建用户(用户名冲突检查) */ public User createUser(String username, String password, String email) { // 参数校验 ThrowUtils.throwIfBlank(username, ErrorCode.PARAMS_ERROR); ThrowUtils.throwIfBlank(password, ErrorCode.PARAMS_ERROR); ThrowUtils.throwIfBlank(email, ErrorCode.PARAMS_ERROR); // 业务校验:用户名重复 User existing = userMapper.selectByUsername(username); ThrowUtils.throwIf(existing != null, ErrorCode.USER_ALREADY_EXISTS, "Username '" + username + "' already exists"); // 创建用户 User user = new User(); user.setUsername(username); user.setPassword(password); user.setEmail(email); userMapper.insert(user); return user; } /** * 示例3:复杂业务逻辑 */ public void updateUserEmail(Long userId, String newEmail) { User user = getUserById(userId); // 检查邮箱是否已被占用 User existing = userMapper.selectByEmail(newEmail); ThrowUtils.throwIf(existing != null && !existing.getId().equals(userId), ErrorCode.EMAIL_ALREADY_EXISTS, "Email '" + newEmail + "' is already in use"); user.setEmail(newEmail); userMapper.updateById(user); } } ``` ### `Controller` 使用示例 ```java // ==================== 8. ==================== package com.example.user.controller; import com.example.common.response.BaseResponse; import com.example.common.response.ResultUtils; import com.example.user.dto.UserDTO; import com.example.user.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; /** * 获取用户信息 * 异常处理: * - USER_NOT_FOUND → 400 客户端错误 * - 系统异常 → 500 服务器错误 + Trace ID */ @GetMapping("/{userId}") public BaseResponse getUser(@PathVariable Long userId) { UserDTO user = userService.getUserById(userId); return ResultUtils.success(user); } /** * 创建用户 * 自动处理参数验证异常 */ @PostMapping public BaseResponse createUser(@RequestBody UserDTO dto) { UserDTO user = userService.createUser(dto); return ResultUtils.success(user); } /** * 更新用户邮箱 */ @PutMapping("/{userId}/email") public BaseResponse updateEmail(@PathVariable Long userId, @RequestParam String newEmail) { userService.updateUserEmail(userId, newEmail); return ResultUtils.success(); } } ``` ## 项目配置建议 `pom.xml 依赖:` ```xml org.springframework.boot spring-boot-starter-web org.projectlombok lombok 1.18.30 provided ``` `application.yml 配置:` ```yaml server: servlet: encoding: charset: UTF-8 force: true logging: level: root: INFO com.example: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" ``` --- **项目分区导航**:⬅️ [[01-异常处理最佳实践指南|01-异常处理最佳实践指南]] | 02-异常处理最佳实践代码 | ➡️ [[03-Spring Boot 多模块自动装配|03-Spring Boot 多模块自动装配]]