Appearance
SpringMVC教程 - 17 数据校验
在Web应用中,数据校验是非常重要的一环。在用户提交表单或发送请求时,我们需要确保输入的数据符合我们的要求,比如不能为空、长度不能超过限制、格式必须正确等。
在Web应用中,数据校验通常分为两种:
- 客户端校验:在浏览器端使用JavaScript进行校验,主要目的是提高用户体验,减少不必要的服务器请求
- 服务器端校验:在服务器端对数据进行校验,这是必不可少的,因为客户端校验可以被绕过,例如直接请求服务器接口。
SpringMVC主要关注的是服务器端校验。
在 Java EE 规范中,JSR-303(Bean Validation 1.0)和 JSR-349(Bean Validation 1.1)定义了一套用于 Java Bean 验证的标准 API。Hibernate Validator 是该 Bean Validation 规范的一个实现,可以方便地集成到 SpringMVC 框架中,用于对 Java Bean 进行验证。其主要使用方式是在 Java Bean 的属性或方法上添加校验注解,通过注解来声明属性的校验规则。
首先来看一下常用的校验注解,我们会将这些注解添加到 Java Bean 的属性上,并设置校验规则。
Bean Validation规范提供了一系列常用的校验注解,下面是一些最常用的:
| 注解 | 作用 | 示例 |
|---|---|---|
@NotNull | 不能为 null | @NotNull(message = "年龄不能为空") |
@NotEmpty | 不能为 null,且长度 > 0(字符串、集合、数组) | @NotEmpty(message = "标签列表不能为空") |
@NotBlank | 不能为 null,且去空格后长度 > 0(字符串) | @NotBlank(message = "用户名不能为空") |
@Null | 必须为 null | @Null(message = "此字段必须为空") |
@Size(min, max) | 字符串、集合、数组的长度必须在指定范围 | @Size(min=6, max=20, message="密码长度必须6-20位") |
@Min(value) | 数值必须 ≥ 最小值 | @Min(18, message="年龄不能小于18岁") |
@Max(value) | 数值必须 ≤ 最大值 | @Max(120, message="年龄不能大于120岁") |
@DecimalMin(value) | 小数值必须 ≥ 最小值 | @DecimalMin("0.01", message="金额不能为0") |
@DecimalMax(value) | 小数值必须 ≤ 最大值 | @DecimalMax("999.99", message="金额太大") |
@Digits(integer, fraction) | 限制小数的整数位与小数位数 | @Digits(integer=3, fraction=2, message="格式不正确,如123.45") |
@Pattern(regexp) | 必须匹配正则表达式 | @Pattern(regexp="^[a-zA-Z0-9_]{3,20}$", message="用户名格式非法") |
@Email | 必须是合法邮箱格式 | @Email(message="邮箱格式不正确") |
@Past | 必须是过去的日期 | @Past(message="生日必须是过去的日期") |
@PastOrPresent | 必须是过去或现在的日期 | @PastOrPresent(message="创建日期不能是未来") |
@Future | 必须是将来的日期 | @Future(message="预约时间必须是未来日期") |
@FutureOrPresent | 必须是未来或现在 | @FutureOrPresent(message="开始时间不能是过去") |
@Positive | 必须为正数 | @Positive(message="数量必须大于0") |
@PositiveOrZero | 必须为正数或 0 | @PositiveOrZero(message="库存不能是负数") |
@Negative | 必须为负数 | @Negative(message="值必须为负数") |
@NegativeOrZero | 必须为负数或 0 | @NegativeOrZero(message="温度不能高于0") |
@AssertTrue | 必须为 true | @AssertTrue(message="必须同意协议") |
@AssertFalse | 必须为 false | @AssertFalse(message="标志位必须为false") |
@Valid | 对嵌套对象执行级联校验 | @Valid private Address address; |
下面就来演示一下。
17.1 数据校验的使用
1 引入依赖
SpringMVC的数据校验需要引入以下依赖:
xml
<!-- 让 Jackson 支持 Java 8 日期时间类型(LocalDate、LocalDateTime 等) -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.20.1</version>
</dependency>
<!-- JSR-380 核心依赖,提供 Bean Validation 的标准接口和注解 -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>4.0.0-M1</version>
</dependency>
<!-- Hibernate Validator 实现(推荐) -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>9.1.0.Final</version>
</dependency>
<!-- 提供表达式语言支持,用于错误消息插值 -->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>5.0.0-M1</version>
</dependency>- Hibernate Validator是Bean Validation规范的一个优秀实现,提供了丰富的校验注解和扩展功能。
- 添加完依赖,
pom.xml 右键 —> Maven Reload —> Project。
2 配置校验器
在SpringMVC的配置文件中,我们需要配置校验器:
xml
<!-- 配置校验器 -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<!-- 指定验证器,使用Hibernate Validator -->
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<!-- 在处理器适配器中添加校验器 -->
<mvc:annotation-driven validator="validator"/>- 如果配置文件中已经有
<mvc:annotation-driven>标签了,在现有的标签上添加validator="validator"属性。
3 在实体类中使用校验注解
下面我们创建一个用户注册的实体类,并使用校验注解,后面会根据注解指定的内容进行校验:
java
package com.foooor.hellospringmvc.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.time.LocalDate;
@Data
public class UserDto {
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 6, max = 20, message = "用户名长度必须在6-20位之间")
private String username;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[0-9]{11}$", message = "手机号必须是11位数字")
private String phoneNumber;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String password;
@NotNull(message = "生日不能为空")
@Past(message = "生日必须是过去的日期")
@JsonFormat(pattern = "yyyy-MM-dd") // 告诉Jackson序列化时使用的日期格式
private LocalDate birthday;
}@Data是Lombok提供的注解,自动生成getter、setter等方法;- 每个属性都添加了校验的注解,并设置了自定义的错误提示信息;
4 RESTful API校验
对于RESTful API,我们可以使用@Validated注解配合@RequestBody来触发数据校验:
java
package com.foooor.hellospringmvc.controller;
import com.foooor.hellospringmvc.common.ErrorCode;
import com.foooor.hellospringmvc.common.Result;
import com.foooor.hellospringmvc.dto.UserDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/register")
public Result register(@Validated @RequestBody UserDto user, BindingResult bindingResult) {
// 检查是否有校验错误
if (bindingResult.hasErrors()) {
// 获取第一个错误信息
String errorMsg = bindingResult.getFieldError().getDefaultMessage();
return Result.error(ErrorCode.PARAM_ERROR.code, errorMsg);
}
// 打印注册信息
log.info("注册 user: {}", user);
// 校验通过,进行注册逻辑
return Result.success();
}
}@Validated注解用于触发数据校验,@RequestBody将请求体转换为Java对象,BindingResult参数用于接收校验结果,必须紧跟在被校验的参数后面。bindingResult.hasErrors()检查是否有校验错误;bindingResult.getFieldError().getDefaultMessage()获取第一个错误信息。
5 测试
此时请求接口,例如通过 Apifox 请求,如果少传递了参数,就会返回错误信息:

17.2 传统项目校验
上面是 Restful API 项目,在传统项目中,需要在 Controller 中校验,在页面上显示错误信息。
1 Controller中校验
在 Controller 也是使用 @Validated 注解和 BindingResult :
java
package com.foooor.hellospringmvc.controller;
import com.foooor.hellospringmvc.dto.UserDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Slf4j
@Controller
@RequestMapping("/user-form")
public class UserFormController {
/**
* 进入注册页面
*/
@GetMapping("/to-register")
public String toRegister(Model model) {
// 提供一个空的User对象给表单
model.addAttribute("user", new UserDto());
return "user/register";
}
/**
* 处理注册提交
*/
@PostMapping("/register")
public String register(@Validated @ModelAttribute("user") UserDto user, BindingResult bindingResult, Model model) {
// 检查是否有校验错误
if (bindingResult.hasErrors()) {
// 如果有错误,返回注册表单页面,并显示错误信息
return "user/register";
}
// TODO 校验通过,进行注册逻辑
// 注册成功,重定向到首页
model.addAttribute("message", "注册成功");
return "user/registerSuccess"; // 注册成功页面
}
}- 在校验失败时,SpringMVC 会往 Model 中放两个重要对象:表单绑定后的 user 对象和 user 的所有校验错误,在页面上就可以获取到校验错误信息。
@ModelAttribute("user")用来声明这个参数是一个“表单模型对象”,Spring 需要把它放进 Model,并为它配套创建BindingResult.user。
2 在页面显示错误信息
首先创建一个注册模板页面 user/register.html,然后在Thymeleaf模板中,使用 th:errors 标签来显示错误信息。
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:form="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
<style>
.error {
color: red;
font-size: 0.8em;
}
</style>
</head>
<body>
<div style="width: 400px; margin: 100px auto;">
<h1>用户注册</h1>
<!-- Spring表单 -->
<form th:object="${user}" action="/user-form/register" method="post">
<!-- 用户名 -->
<div style="margin-bottom: 10px;">
<label for="username">用户名:</label>
<input type="text" id="username" th:field="*{username}" style="width: 200px;">
<span class="error" th:errors="*{username}"></span>
</div>
<!-- 手机号 -->
<div style="margin-bottom: 10px;">
<label for="phoneNumber">手机号:</label>
<input type="text" id="phoneNumber" th:field="*{phoneNumber}" style="width: 200px;">
<span class="error" th:errors="*{phoneNumber}"></span>
</div>
<!-- 邮箱 -->
<div style="margin-bottom: 10px;">
<label for="email">邮箱:</label>
<input type="email" id="email" th:field="*{email}" style="width: 200px;">
<span class="error" th:errors="*{email}"></span>
</div>
<!-- 密码 -->
<div style="margin-bottom: 10px;">
<label for="password">密码:</label>
<input type="password" id="password" th:field="*{password}" style="width: 200px;">
<span class="error" th:errors="*{password}"></span>
</div>
<!-- 生日 -->
<div style="margin-bottom: 20px;">
<label for="birthday">生日:</label>
<input type="date" id="birthday" th:field="*{birthday}" style="width: 200px;">
<span class="error" th:errors="*{birthday}"></span>
</div>
<button type="submit">注册</button>
</form>
</div>
</body>
</html>- 当使用
th:errors="*{username}"时,会查找 Model 中找到 BindingResult.user,然后取出username字段的错误信息。
3 测试
使用 http://localhost:8080/user-form/register 访问页面,直接点击保存,会显示出校验的错误信息。
但是在页面显示校验错误信息的时候,会将一个字段所有的错误信息都显示出来:

可以使用下面的写法,只显示一条:
java
<span class="error"
th:if="${#fields.hasErrors('name')}"
th:text="${#fields.errors('name')[0]}"></span>17.3 单个参数校验
数据校验不仅可以用于复杂对象,还可以直接用于单个参数。
举个栗子:
java
package com.foooor.hellospringmvc.controller;
import com.foooor.hellospringmvc.common.ErrorCode;
import com.foooor.hellospringmvc.common.Result;
import com.foooor.hellospringmvc.dto.UserDto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
// @Validated // spring6以下需要在类上添加@Validated注解
@RequestMapping("/user")
public class UserController {
// @RequestParam参数校验
@GetMapping
public String getUsers(@RequestParam("page") @NotNull @Min(1) Integer page,
@RequestParam("size") @NotNull @Min(1) Integer size) {
return "分页查询用户,第" + page + "页,每页" + size + "条";
}
// @PathVariable参数校验
@GetMapping("/{id}")
public String getUserById(@PathVariable("id") @NotNull @Min(1) Long id) {
return "查询用户ID:" + id;
}
// 字符串长度校验
@GetMapping("/search")
public String search(@RequestParam("keyword") @Size(min = 2, max = 10) String keyword) {
return "搜索关键词:" + keyword;
}
// 其他代码...
}- 在方法的参数上添加相关的注解即可。
17.4 校验结果的详细处理
在上面 Restful API 的 例子中,通过 getDefaultMessage() 获取的是第一个错误信息:
java
// 获取第一个错误信息
String errorMsg = bindingResult.getFieldError().getDefaultMessage();如果要获取所有的错误信息,可以通过如下方式。
1 返回所有错误信息
java
@PostMapping("/register")
public Result register(@Validated @RequestBody UserDto user, BindingResult bindingResult) {
// 检查是否有校验错误
if (bindingResult.hasErrors()) {
// 获取所有错误信息
List<String> errorMessages = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
return Result.error(ErrorCode.PARAM_ERROR, String.join(",", errorMessages));
}
// 校验通过,进行注册逻辑
return Result.success();
}- 使用 Java 8 的Stream API将所有错误信息收集到一个列表中,当然你也可以使用普通的 for 循环遍历。
- 使用
String.join将所有错误信息用分号连接起来,返回结果如下:
返回的 JSON 格式如下:
json
{
"code": 1001,
"message": "生日不能为空,密码不能为空,手机号不能为空,用户名不能为空,邮箱不能为空",
"data": null
}2 按字段返回错误信息
有时候,我们需要按字段返回错误信息,以便前端能够更精确地在对应的输入框旁显示错误提示:
java
@PostMapping("/register")
public Result register(@Validated @RequestBody UserDto user, BindingResult bindingResult) {
// 检查是否有校验错误
if (bindingResult.hasErrors()) {
// 按字段收集错误信息
Map<String, String> errorMap = bindingResult.getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField, // 字段名
FieldError::getDefaultMessage // 错误信息
));
return Result.error(ErrorCode.PARAM_ERROR, errorMap);
}
// 校验通过,进行注册逻辑
return Result.success();
}这样返回的 JSON 格式如下:
json
{
"code": 1001,
"message": "参数错误",
"data": {
"birthday": "生日不能为空",
"password": "密码不能为空",
"phoneNumber": "手机号不能为空",
"email": "邮箱不能为空",
"username": "用户名不能为空"
}
}17.5 分组校验
在实际开发中,我们可能会遇到这样的情况:同一个实体类在不同的场景下需要不同的校验规则。比如,创建用户的时候不需要ID(自动生成),但更新用户信息时需要ID。
这时候,我们可以使用分组校验来解决这个问题。
1 定义分组接口
首先,我们需要定义一些分组接口:
java
package com.foooor.hellospringmvc.validation;
public class UserGroup {
// 创建分组
public interface CreateGroup {}
// 更新分组
public interface UpdateGroup {}
}- 为了简单,我将多个分组放在一个类中,也可以单独定义类文件;
- 分组接口只是一个标记接口,不需要定义任何方法。
- 另外分组是可以继承的,父类相当于囊括了所有子分组。
2 在实体类中指定分组
然后,我们在实体类的校验注解中指定分组:
java
package com.foooor.hellospringmvc.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.foooor.hellospringmvc.validation.UserGroup;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.time.LocalDate;
@Data
public class UserDto {
@NotNull(message = "学生ID不能为空", groups = UserGroup.UpdateGroup.class) // 使用更新分组
private Long id;
@NotBlank(message = "用户名不能为空", groups = UserGroup.CreateGroup.class) // 使用创建分组
@Size(min = 6, max = 20, message = "用户名长度必须在6-20位之间")
private String username;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[0-9]{11}$", message = "手机号必须是11位数字")
private String phoneNumber;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String password;
@NotNull(message = "生日不能为空")
@Past(message = "生日必须是过去的日期")
@JsonFormat(pattern = "yyyy-MM-dd") // 告诉Jackson序列化时使用的日期格式
private LocalDate birthday;
}id字段只在UpdateGroup分组中需要校验;username字段只在CreateGroup分组中需要校验;- 其他字段没有设置分组,Spring/Bean Validation 默认会使用
jakarta.validation.groups.Default分组 。
3 在Controller中指定使用的分组
最后,在Controller中使用 @Validated 注解时指定要使用的分组:
java
import jakarta.validation.groups.Default;
import com.foooor.hellospringmvc.validation.UserGroup;
@PostMapping("/register")
public Result register(@Validated({Default.class, UserGroup.CreateGroup.class}) @RequestBody UserDto user, BindingResult bindingResult) {
// 检查是否有校验错误
if (bindingResult.hasErrors()) {
// 获取所有错误信息
List<String> errorMessages = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
return Result.error(ErrorCode.PARAM_ERROR, String.join(",", errorMessages));
}
// 校验通过,进行注册逻辑
return Result.success();
}
@PutMapping
public Result update(@Validated({Default.class, UserGroup.UpdateGroup.class}) @RequestBody UserDto user, BindingResult bindingResult) {
// 检查是否有校验错误
if (bindingResult.hasErrors()) {
// 按字段收集错误信息
Map<String, String> errorMap = bindingResult.getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField, // 字段名
FieldError::getDefaultMessage // 错误信息
));
return Result.error(ErrorCode.PARAM_ERROR, errorMap);
}
// 更新逻辑...
// 校验通过,进行注册逻辑
return Result.success();
}@Validated({Default.class, UserGroup.CreateGroup.class})指定使用注册分组的校验规则,同时指定了默认的校验规则,这样可以对没有添加分组的属性也进行校验;@Validated({Default.class, UserGroup.UpdateGroup.class}指定使用更新分组的校验规则;- 这样在更新的时候才检查用户
id和其他默认分组的属性是否为空,创建的时候不检查。
17.6 自定义校验注解
有时候,系统提供的校验注解不能满足我们的需求,这时候我们可以自定义校验注解。
比如,我们需要校验手机号码的格式是否正确。
1 创建校验注解
先创建一个校验器注解,后面可以用在字段或参数上面。
java
package com.foooor.hellospringmvc.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PhoneValidator.class) // 指定校验器
@Target({ElementType.FIELD, ElementType.PARAMETER}) // 可以用在字段和参数上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
public @interface Phone {
// 默认错误信息
String message() default "手机号码格式不正确";
// 分组
Class<?>[] groups() default {};
// 负载
Class<? extends Payload>[] payload() default {};
}@Constraint(validatedBy = PhoneValidator.class)指定了该注解使用的校验器,待会创建;message()定义了默认的错误信息;groups()和payload()是Bean Validation规范要求的方法;
2 创建校验器
创建校验器,用来验证手机格式是否正确,这里使用正则表达式来验证。
java
package com.foooor.hellospringmvc.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class PhoneValidator implements ConstraintValidator<Phone, String> {
// 手机号码正则表达式
private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";
@Override
public void initialize(Phone constraintAnnotation) {
// 初始化方法,可以获取注解的属性
}
@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
// 如果手机号为null,返回true(由@NotNull等注解处理非空校验)
if (phone == null) {
return true;
}
// 校验手机号格式
return phone.matches(PHONE_REGEX);
}
}ConstraintValidator<Phone, String>表示该校验器用于校验Phone注解,校验的数据类型是String;initialize()方法用于初始化校验器,可以获取注解的属性;isValid()方法用于执行校验逻辑,返回true表示校验通过,false表示校验失败。
3 使用自定义校验注解
现在,我们可以在实体类中使用自定义的 @Phone 注解了:
java
@Data
public class UserDto {
// 其他字段...
@NotBlank(message = "手机号不能为空")
@Phone(message = "手机号码格式不正确")
private String phoneNumber;
}- 按自己的需要,看是不是要指定分组。
17.7 嵌套校验
当实体类中包含其他实体类作为属性时,我们需要使用嵌套校验来验证嵌套对象的属性。
比如,在 User类,包含一个 Book 对象作为属性:
java
package com.foooor.hellospringmvc.pojo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.foooor.hellospringmvc.validation.Phone;
import com.foooor.hellospringmvc.validation.UserGroup;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
public class UserDto {
@NotNull(message = "学生ID不能为空", groups = UserGroup.UpdateGroup.class)
private Long id;
// 其他字段...
@NotNull(message = "图书的信息不能为空")
@Valid // 嵌套校验
private Book book;
}@Valid注解用于触发嵌套对象的校验。
同样也可以在嵌套的对象中,对属性添加校验规则,例如 Book 类如下:
java
package com.foooor.hellospringmvc.pojo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class Book {
@NotNull(message = "图书ID不能为空")
private Long id;
@NotBlank(message = "图书名称不能为空")
@Size(min = 2, max = 20, message = "图书名称长度必须在2-20位之间")
private String name;
}在Controller中,和之前是一样的,还是对 User 正常使用@Validated注解即可,没有变化:
java
@PostMapping
public Result create(@Validated({Default.class, UserGroup.CreateGroup.class}) @RequestBody UserDto user, BindingResult bindingResult) {
}内容未完......