异常处理最佳实践代码
为什么需要全局异常处理和通用返回类
全局异常处理的设计目标是提供统一的异常捕获、处理机制,确保在应用中出现异常时能够进行集中管理并返回规范的错误信息。它不仅帮助开发者在不同地方减少重复代码,还提升了系统的可维护性和可扩展性。 以下是全局异常处理的几个优势:
- 一致性: 保证错误响应的格式和结构一致,无论是在前端还是后端。
- 集中管理: 所有异常都可以在一个地方处理,减少了分散管理的复杂性。
- 日志记录: 统一的异常捕获能更好地进行日志记录,方便后期排查问题。
- 用户体验: 提供了清晰且统一的错误信息,帮助前端进行更好的用户提示。
通用返回类(如 BaseResponse)用于封装请求结果的响应数据,并规范返回结构,确保每个接口的响应一致。
设计通用返回类的原因如下:
- 统一结构: 所有接口返回的数据都符合统一格式,使前端处理更加简单。
- 扩展性: 返回类支持链式调用,可以便捷地添加额外字段或修改已有字段。
- 简化代码: 使用通用返回类可以避免每次接口响应都编写重复代码。
错误码枚举 ErrorCode.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
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 <T> 数据泛型
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseResponse<T> 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
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
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> T throwIfNull(T obj, ErrorCode errorCode) {
if (obj == null) {
throw new BusinessException(errorCode);
}
return obj;
}
/**
* 对象为null时抛异常(支持自定义描述)
*/
public static <T> 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> 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> 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
package com.example.common.response;
import com.example.common.exception.ErrorCode;
/**
* 响应工具类
* 提供简洁的API用于构造各种响应对象
*/
public class ResultUtils {
/**
* 成功响应(返回数据)
*/
public static <T> BaseResponse<T> success(T data) {
return BaseResponse.<T>builder()
.code(ErrorCode.SUCCESS.getCode())
.message(ErrorCode.SUCCESS.getMessage())
.description(null)
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
/**
* 成功响应(无数据)
*/
public static <T> BaseResponse<T> success() {
return success(null);
}
/**
* 失败响应(使用错误码)
*/
public static <T> BaseResponse<T> error(ErrorCode errorCode) {
return BaseResponse.<T>builder()
.code(errorCode.getCode())
.message(errorCode.getMessage())
.description(errorCode.getDescription())
.timestamp(System.currentTimeMillis())
.build();
}
/**
* 失败响应(使用错误码和自定义描述)
*/
public static <T> BaseResponse<T> error(ErrorCode errorCode, String description) {
return BaseResponse.<T>builder()
.code(errorCode.getCode())
.message(errorCode.getMessage())
.description(description)
.timestamp(System.currentTimeMillis())
.build();
}
/**
* 失败响应(自定义code和message)
*/
public static <T> BaseResponse<T> error(int code, String message, String description) {
return BaseResponse.<T>builder()
.code(code)
.message(message)
.description(description)
.timestamp(System.currentTimeMillis())
.build();
}
/**
* 服务器错误响应(包含Trace ID)
*/
public static <T> BaseResponse<T> serverError(String traceId, String description) {
return BaseResponse.<T>builder()
.code(ErrorCode.SYSTEM_ERROR.getCode())
.message("System error, please contact support")
.description("系统错误,请联系管理员")
.timestamp(System.currentTimeMillis())
.traceId(traceId)
.build();
}
}
全局异常处理器 GlobalExceptionHandler.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;
/**
* 全局异常处理器
* <p>
* 功能:
* 1. 捕获所有异常,转换为统一响应格式
* 2. 记录详细的堆栈信息,精确定位代码行号
* 3. 区分客户端错误和服务端错误,进行不同处理
* 4. 服务端错误生成 TraceId 用于问题追踪
* </p>
*
* @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());
}
/**
* 处理运行时异常
* <p>
* 特殊处理:检查是否是包装的 BusinessException
* 场景:CompletableFuture 等异步操作会将 BusinessException 包装成 RuntimeException
* </p>
*/
@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, "系统异常");
}
// ==================== 响应生成 ====================
/**
* 根据错误类型生成不同响应
* <p>
* CLIENT 错误:返回详细信息供前端展示
* SERVER 错误:隐藏敏感信息,返回 TraceId 用于追踪
* </p>
*/
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(" | "));
}
/**
* 获取异常的最相关堆栈信息
* <p>
* 过滤规则:
* 1. 排除 ThrowUtils 工具类(因为异常是从工具类抛出的,需要找到真正调用处)
* 2. 排除 Spring 框架类
* 3. 排除 Java 反射相关类
* 4. 排除动态代理类
* </p>
*/
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 地址
* <p>
* 优先级:X-Forwarded-For > X-Real-IP > Proxy-Client-IP > WL-Proxy-Client-IP > RemoteAddr
* </p>
*/
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使用示例
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 使用示例
// ==================== 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<UserDTO> getUser(@PathVariable Long userId) {
UserDTO user = userService.getUserById(userId);
return ResultUtils.success(user);
}
/**
* 创建用户
* 自动处理参数验证异常
*/
@PostMapping
public BaseResponse<UserDTO> createUser(@RequestBody UserDTO dto) {
UserDTO user = userService.createUser(dto);
return ResultUtils.success(user);
}
/**
* 更新用户邮箱
*/
@PutMapping("/{userId}/email")
public BaseResponse<Void> updateEmail(@PathVariable Long userId,
@RequestParam String newEmail) {
userService.updateUserEmail(userId, newEmail);
return ResultUtils.success();
}
}
项目配置建议
pom.xml 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
application.yml 配置:
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-异常处理最佳实践指南 | 02-异常处理最佳实践代码 | ➡️ 03-Spring Boot 多模块自动装配
💬 评论