Spring Boot 参数校验异常与错误编码映射方案
一、错误码定义与配置
-
错误码结构
采用分层编码格式:[模块][错误类型][序号]
(如1001
表示公共模块参数校验错误中的第一个错误) -
配置文件(application.yml)
yaml
复制
error: codes: # 公共模块错误码 common: param_invalid: 1000 # 校验子错误码 constraints: NotNull: 1001 Size: 1002 Pattern: 1003 Email: 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 { @Autowired private 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("用户名") @NotNull private 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.NotNull code: 1001 messageKey: not.null - constraint: org.hibernate.validator.constraints.Length code: 1002 messageKey: invalid.length
七、方案优势
-
统一错误格式:标准化响应结构,前端处理更简单
-
精准错误定位:字段级错误码和提示信息
-
国际化支持:轻松扩展多语言版本
-
动态配置:通过YAML文件管理错误码映射
-
强类型校验:结合Java Validation规范
-
可维护性:错误码集中管理,避免散落各处
八、注意事项
-
校验顺序处理:多个校验注解同时失败时的处理顺序
-
敏感信息过滤:避免在错误信息中暴露敏感数据
-
性能优化:字段名反射获取需要缓存处理
-
文档同步:错误码列表需要与API文档保持同步
-
安全校验:结合Spring Security的权限校验体系