3. 使用JSR 303实现请求参数校验与统一错误响应
为什么需要 JSR 303 校验
在用户注册接口中,请求体字段(用户名、密码、邮箱、年龄)需要满足长度、格式、范围等约束。如果把这些判断全部写在 Controller 方法里,代码会迅速膨胀,且错误响应格式难以统一。JSR 303(Bean Validation)允许把约束声明在 DTO 字段上,由框架在参数绑定后自动执行校验,校验失败时抛出 MethodArgumentNotValidException,再交给全局异常处理器统一转换为结构化 JSON。
依赖与 DTO 约束声明
Spring Boot 2.3 之后需要显式引入校验实现。在 pom.xml 中加入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
定义注册请求 DTO,把约束直接标注在字段上:
public class RegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 4, max = 20, message = "用户名长度需在4到20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 32, message = "密码长度需在8到32之间")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$",
message = "密码必须同时包含字母和数字")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于18")
@Max(value = 120, message = "年龄不能大于120")
private Integer age;
// getter / setter 省略
}
约束注解的
message 会作为字段级错误信息返回,建议写成对用户友好的中文提示,而不是默认的英文模板。在 Controller 上启用校验
在接口参数前加 @Valid(或 @Validated),Spring MVC 会在绑定完成后触发校验:
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping("/register")
public ResponseEntity<String> register(@Valid @RequestBody RegisterRequest request) {
// 校验通过后才会执行到这里
return ResponseEntity.ok("注册成功:" + request.getUsername());
}
}
统一错误响应结构
为了让前端稳定解析错误,先定义一个统一响应体:
public class ApiErrorResponse {
private int code;
private String message;
private List<FieldErrorItem> fieldErrors;
public ApiErrorResponse(int code, String message, List<FieldErrorItem> fieldErrors) {
this.code = code;
this.message = message;
this.fieldErrors = fieldErrors;
}
public static class FieldErrorItem {
private String field;
private String message;
public FieldErrorItem(String field, String message) {
this.field = field;
this.message = message;
}
// getter 省略
}
// getter 省略
}
用 @ControllerAdvice 捕获校验异常
校验失败时抛出的是 MethodArgumentNotValidException,其中 getBindingResult() 保存了所有字段错误。通过 @RestControllerAdvice 集中处理:
@RestControllerAdvice
public class GlobalValidationHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {
List<ApiErrorResponse.FieldErrorItem> items = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(fe -> new ApiErrorResponse.FieldErrorItem(
fe.getField(), fe.getDefaultMessage()))
.collect(Collectors.toList());
ApiErrorResponse body = new ApiErrorResponse(400, "参数校验失败", items);
return ResponseEntity.badRequest().body(body);
}
}
如果同一个字段同时触发多个约束(例如密码既太短又不含数字),
getFieldErrors() 可能返回多条记录。若前端只希望每个字段显示一条提示,可在流处理中按 field 去重,或使用 getFieldError() 只取第一条。校验链路与错误响应结构
下图展示从请求进入 Controller 到返回结构化错误的完整链路,以及 ApiErrorResponse 的字段组成:
验证与结果判读
启动应用后,用以下请求验证校验是否生效:
curl -X POST http://localhost:8080/api/users/register \
-H "Content-Type: application/json" \
-d '{"username":"ab","password":"123","email":"not-an-email","age":15}'
预期返回 HTTP 400,响应体形如:
{
"code": 400,
"message": "参数校验失败",
"fieldErrors": [
{"field": "username", "message": "用户名长度需在4到20之间"},
{"field": "password", "message": "密码长度需在8到32之间"},
{"field": "email", "message": "邮箱格式不正确"},
{"field": "age", "message": "年龄不能小于18"}
]
}
再发送一条全部合法的请求,应返回 HTTP 200 与 "注册成功:..."。若合法请求仍返回 400,优先检查:DTO 字段是否缺少 getter(Jackson 反序列化失败会先于校验报错)、@Valid 是否加在 @RequestBody 参数上、以及 spring-boot-starter-validation 是否已引入。
关键点:
@Valid 负责触发校验,@ControllerAdvice 负责把异常翻译成统一 JSON。两者缺一不可——只加注解不处理异常,前端拿到的仍是 Spring 默认错误页或默认错误结构。