一、错误码定义与配置
-
错误码结构
采用分层编码格式:[模块][错误类型][序号]
(如1001
表示公共模块参数校验错误中的第一个错误) -
配置文件(application.yml)
yaml
复制
error:codes:# 公共模块错误码common:param_invalid: 1000# 校验子错误码constraints:NotNull: 1001Size: 1002Pattern: 1003Email: 1004# 用户模块错误码user:not_found: 2001
-
错误码枚举类
java
复制
@Getter @AllArgsConstructor public enum ErrorCode {// 公共错误码PARAM_INVALID(1000, "error.param.invalid"),NOT_NULL(1001, "error.validation.notNull"),SIZE(1002, "error.validation.size"),PATTERN(1003, "error.validation.pattern"),EMAIL(1004, "error.validation.email"),// 业务错误码USER_NOT_FOUND(2001, "error.user.notFound");private final int code;private final String msgKey; }
二、统一异常处理
-
全局异常处理器
java
复制
@RestControllerAdvice public class GlobalExceptionHandler {@Autowiredprivate MessageSource messageSource;// 处理参数校验异常@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex, Locale locale) {List<FieldErrorVO> fieldErrors = ex.getBindingResult().getFieldErrors().stream().map(fieldError -> convertToFieldErrorVO(fieldError, locale)).collect(Collectors.toList());String message = messageSource.getMessage(ErrorCode.PARAM_INVALID.getMsgKey(), null, locale);ErrorResponse response = new ErrorResponse(ErrorCode.PARAM_INVALID.getCode(),message,fieldErrors);return ResponseEntity.badRequest().body(response);}private FieldErrorVO convertToFieldErrorVO(FieldError fieldError, Locale locale) {// 获取校验注解类型(如 NotNull, Size)String constraintType = fieldError.getCode();ErrorCode errorCode = getErrorCodeByConstraint(constraintType);// 解析消息参数Object[] args = Stream.of(Optional.ofNullable(fieldError.getArguments()).orElse(new Object[]{})).map(arg -> {if (arg instanceof MessageSourceResolvable) {return messageSource.getMessage((MessageSourceResolvable) arg, locale);}return arg;}).toArray();// 获取字段显示名称String fieldName = getFieldDisplayName(fieldError.getField());// 生成错误信息String errorMsg = messageSource.getMessage(errorCode.getMsgKey(), new Object[]{fieldName, args}, locale);return new FieldErrorVO(fieldError.getField(),errorCode.getCode(),errorMsg);}private ErrorCode getErrorCodeByConstraint(String constraintType) {return switch (constraintType) {case "NotNull" -> ErrorCode.NOT_NULL;case "Size" -> ErrorCode.SIZE;case "Pattern" -> ErrorCode.PATTERN;case "Email" -> ErrorCode.EMAIL;default -> ErrorCode.PARAM_INVALID;};}private String getFieldDisplayName(String fieldName) {// 实现字段名到中文的转换(可通过注解或配置文件)return switch (fieldName) {case "username" -> "用户名";case "password" -> "密码";default -> fieldName;};} }
三、响应实体类
java
复制
@Data @NoArgsConstructor @AllArgsConstructor public class ErrorResponse {private int code;private String message;private List<FieldErrorVO> errors; }@Data @NoArgsConstructor @AllArgsConstructor public class FieldErrorVO {private String field;private int code;private String message; }
四、国际化配置
-
messages.properties
properties
复制
error.param.invalid=参数校验失败 error.validation.notNull={0}不能为空 error.validation.size={0}长度必须在{1}到{2}之间 error.validation.pattern={0}格式不正确 error.validation.email=邮箱格式无效 error.user.notFound=用户不存在
-
messages_en_US.properties
properties
复制
error.param.invalid=Parameter validation failed error.validation.notNull={0} cannot be empty error.validation.size={0} length must be between {1} and {2} error.validation.pattern={0} format is invalid error.validation.email=Invalid email format error.user.notFound=User not found
五、使用示例
-
DTO 类
java
复制
@Data public class UserDTO {@NotNull(message = "用户名不能为空")@Size(min = 3, max = 20, message = "用户名长度需在3到20个字符")private String username;@Email(message = "邮箱格式不正确")private String email; }
-
Controller 方法
java
复制
@PostMapping("/users") public ResponseEntity<?> createUser(@Valid @RequestBody UserDTO userDTO) {// 业务逻辑return ResponseEntity.ok().build(); }
-
错误响应示例
json
复制
{"code": 1000,"message": "参数校验失败","errors": [{"field": "username","code": 1001,"message": "用户名不能为空"},{"field": "email","code": 1004,"message": "邮箱格式无效"}] }
六、扩展配置
-
自定义字段显示名称
java
复制
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface FieldLabel {String value(); }// 在DTO中使用 public class UserDTO {@FieldLabel("用户名")@NotNullprivate String username; }// 修改字段名获取逻辑 private String getFieldDisplayName(FieldError fieldError) {try {Field field = fieldError.getField().getClass().getDeclaredField(fieldError.getField());FieldLabel annotation = field.getAnnotation(FieldLabel.class);return annotation != null ? annotation.value() : fieldError.getField();} catch (NoSuchFieldException e) {return fieldError.getField();} }
-
动态错误码配置
yaml
复制
error:mappings:- constraint: javax.validation.constraints.NotNullcode: 1001messageKey: not.null- constraint: org.hibernate.validator.constraints.Lengthcode: 1002messageKey: invalid.length
七、方案优势
-
统一错误格式:标准化响应结构,前端处理更简单
-
精准错误定位:字段级错误码和提示信息
-
国际化支持:轻松扩展多语言版本
-
动态配置:通过YAML文件管理错误码映射
-
强类型校验:结合Java Validation规范
-
可维护性:错误码集中管理,避免散落各处
八、注意事项
-
校验顺序处理:多个校验注解同时失败时的处理顺序
-
敏感信息过滤:避免在错误信息中暴露敏感数据
-
性能优化:字段名反射获取需要缓存处理
-
文档同步:错误码列表需要与API文档保持同步
-
安全校验:结合Spring Security的权限校验体系