SpringMVC 数据校验和BindingResult以及自定义校验注解
数据校验和BindingResult
- BindingResult
- 概念:
- 用法:
- 常用方法:
- 校验概述
- 标准注解
- 核心使用示例
- 在 Spring 中使用
- 用法总结
- 数据类型校验
- 一、字符串类型校验
- 基础验证
- 长度和格式验证
- 二、数值类型校验
- 基础数值验证-整数、浮点、正负数
- 三、日期时间类型校验
- 四、集合类型校验
- 五、布尔类型校验
- 六、枚举类型校验
- 七、嵌套对象校验
- 八、★ 自定义注解校验复杂逻辑
- 1. 自定义注解
- 2. 自定义注解校验器
- 3. 在实体类中使用
- 4. 访问并验证
- 九、完整综合示例
- 十、最佳实践总结
- 1. **按数据类型选择合适注解**
- 2. **验证层次**
- 3. **组合验证策略**
- 数据校验注解使用示例
- 1. 在POM文件中导入jar包
- 2. 创建一个实体类,并使用参数校验注解。
- 3. 在控制器中开启校验。
- 数据校验错误加入到全局统一异常
示例代码地址:https://gitee.com/hua5h6m/framework-java/tree/master/spring-validated-exception
BindingResult
BindingResult
是Spring MVC
中用于处理数据绑定的一个接口,它继承自Errors
接口。
在表单验证中,我们通常使用BindingResult
来存储和获取数据绑定 errors
和验证 errors
。
概念:
-
当我们在Controller的方法中使用
@Valid
或@Validated
注解对模型对象进行验证时,紧跟在被验证模型对象参数之后可以添加一个BindingResult
参数。 -
BindingResult
参数会包含验证结果,包括所有错误信息(如字段错误、全局错误等)。 -
如果不存在
BindingResult
参数,那么当验证失败时,Spring MVC
会抛出异常(如MethodArgumentNotValidException
)。而如果有BindingResult
,则异常不会被抛出,我们可以通过BindingResult
对象来自定义处理错误。
用法:
-
在Controller方法中,将
BindingResult
放在被验证的模型对象参数后面。 -
通过
BindingResult
的方法来判断是否有错误,并处理这些错误。
常用方法:
-
hasErrors()
: 判断是否有错误。 -
hasFieldErrors(String field)
: 判断指定字段是否有错误。 -
getFieldError(String field)
: 获取指定字段的错误。 -
getAllErrors()
: 获取所有错误(包括字段错误和全局错误)。
校验概述
在WEB应用三层架构体系中,表述层负责接口搜浏览器提交的数据,业务逻辑层负责数据的处理。为了能够让业务逻辑层基于正确的数据进行处理,我们需要在表述层对数据进行检查,将错误的信息隔绝在业务逻辑层之外。
JSR303是JAVA为Bean数据合法性校验提供的标准框架,他已经包含在JavaEE 6.0标准中。JSR303通过在Bean属性上标注类似于@NotNull
、@Max
等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
- Spring 4.0版本已经拥有自己独立的数据校验框架,同时支持JSR303标准的校验框架。Spring在进行数据绑定时,可同时调用校验框架完成数据校验工作。
- 在SpringMVC中,可直接通过注解驱动
mvc:annotation-driven
的方式进行数据校验。 - Spring的
LocalValidatorFactoryBean
,即可将其注入到需要数据校验的Bean中。 - Spring本身并没有提供JSR303的实现,所以必须将JSR303的实现者的jar包放到类路径下。
配置mvc:annotation-driven
后,SpringMVC会默认装配好一个LocalValidatorFactoryBean
,通过在处理方法的入参上标注@Validated
注解,即可让SpringMVC在完成数据绑定后执行数据校验的工作。
标准注解
这些是核心规范中包含的注解,适用于大多数场景。
注解 | 功能描述 | 示例 |
---|---|---|
@NotNull | 验证注解的元素值不能为 null。但可以是空字符串或 0。 | @NotNull private String name; |
@Null | 验证注解的元素值必须为 null。 | @Null private String unusedField; |
@NotBlank | 验证字符串不能为 null,且必须至少包含一个非空白字符(即修剪后长度 > 0)。 | @NotBlank private String username; |
@NotEmpty | 验证元素不能为 null 或空。支持 CharSequence, Collection, Map, Array。 | @NotEmpty private List<String> roles; |
@Size | 验证元素大小在指定范围内(包括边界)。支持字符串、集合、数组、Map。 | @Size(min=2, max=10) private String password; |
@Min | 验证数字值大于或等于指定的最小值。 | @Min(18) private Integer age; |
@Max | 验证数字值小于或等于指定的最大值。 | @Max(100) private Integer score; |
@DecimalMin | 验证 BigDecimal/BigInteger/字符串等大于或等于指定的最小值(值包含在字符串内,可指定是否包含边界)。 | @DecimalMin("0.0") private BigDecimal price; |
@DecimalMax | 验证 BigDecimal/BigInteger/字符串等小于或等于指定的最大值。 | @DecimalMax("10000.00") private BigDecimal budget; |
@Digits | 验证数字的整数部分和小数部分的位数是否符合要求。 | @Digits(integer=3, fraction=2) // 如 123.45 |
@Positive | 验证数字必须是正数(大于 0)。 | @Positive private Integer quantity; |
@PositiveOrZero | 验证数字必须是正数或零。 | @PositiveOrZero private Integer stock; |
@Negative | 验证数字必须是负数(小于 0)。 | @Negative private BigDecimal balance; |
@NegativeOrZero | 验证数字必须是负数或零。 | @NegativeOrZero private BigDecimal delta; |
@Email | 验证字符串是否是合法的电子邮件地址(默认正则表达式,可通过 regexp 覆盖)。 | @Email private String contactEmail; |
@Pattern | 验证字符串是否匹配指定的正则表达式。 | @Pattern(regexp = "^[a-zA-Z0-9_]+$") |
@Future | 验证日期或时间是否在当前时间之后。 | @Future private LocalDate startDate; |
@FutureOrPresent | 验证日期或时间是否在当前时间之后或等于当前时间。 | @FutureOrPresent private LocalDateTime eventTime; |
@Past | 验证日期或时间是否在当前时间之前。 | @Past private Date birthDate; |
@PastOrPresent | 验证日期或时间是否在当前时间之前或等于当前时间。 | @PastOrPresent private LocalDate reportDate; |
@AssertTrue | 验证布尔值必须为 true。常用于依赖其他字段的逻辑验证。 | @AssertTrue private Boolean isAgreedToTerms; |
@AssertFalse | 验证布尔值必须为 false。 | @AssertFalse private Boolean isSuspended; |
核心使用示例
以下是一个综合使用这些注解的实体类示例:
import jakarta.validation.constraints.*;
import org.hibernate.validator.constraints.Length;public class User {@NotNull(message = "用户ID不能为空")private Long id;@NotBlank(message = "用户名不能为空")@Length(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")private String username;@NotBlank(message = "邮箱不能为空")@Email(message = "邮箱格式不正确")private String email;@Min(value = 18, message = "年龄必须大于等于18岁")@Max(value = 100, message = "年龄必须小于等于100岁")private Integer age;@Future(message = "会员有效期必须是一个将来的日期")private LocalDate membershipExpiry;// ... 省略 getter 和 setter
}
在 Spring 中使用
在 Spring Boot 中,Bean Validation 被无缝集成。你只需要:
- 在依赖中引入
spring-boot-starter-validation
。 - 在 Controller 的方法参数上使用
@Valid
或@Validated
注解来触发验证。
@RestController
public class UserController {@PostMapping("/users")public ResponseEntity<User> createUser(@RequestBody @Valid User user) {// 如果 user 对象的验证失败,Spring 会自动抛出 MethodArgumentNotValidException// 业务逻辑...return ResponseEntity.ok(user);}
}
用法总结
- 基础三剑客:
@NotNull
,@NotBlank
,@NotEmpty
用于非空检查。 - 范围与大小:
@Size
,@Min
,@Max
,@Range
用于控制大小和范围。 - 格式与模式:
@Email
,@Pattern
用于验证特定格式。 - 时间相关:
@Future
(未来),@Past
(过去) 等用于日期验证。 - 逻辑验证:
@AssertTrue/False
用于复杂的业务逻辑验证。
数据类型校验
一、字符串类型校验
基础验证
public class StringValidation {@NotNull // 不能为nullprivate String requiredField;@NotBlank // 非null且至少一个非空白字符private String username;@NotEmpty // 非null且非空字符串private String notEmptyString;
}
长度和格式验证
public class StringFormatValidation {@Size(min = 2, max = 50) // 长度范围private String name;@Length(min = 8, max = 20) // Hibernate扩展,专用于字符串private String password;@Email // 邮箱格式private String email;@Pattern(regexp = "^[A-Za-z0-9+_.-]+@(.+)$") // 自定义邮箱正则private String customEmail;@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}$") // 密码强度private String strongPassword;@URL // URL格式验证private String websiteUrl;
}
二、数值类型校验
基础数值验证-整数、浮点、正负数
public class NumberValidation {// 整数验证@Min(0) @Max(100) // 整数范围private Integer percentage;@Range(min = 1, max = 150) // Hibernate扩展,整数范围private Integer age;// 小数/高精度验证@DecimalMin("0.0") // 最小值(字符串形式)@DecimalMax("9999.99")private BigDecimal price;@Digits(integer = 4, fraction = 2) // 位数控制private BigDecimal amount;// 正负验证@Positive // 必须为正数private Integer positiveNumber;@PositiveOrZero // 正数或零private Long nonNegativeId;@Negative // 必须为负数private Double negativeValue;@NegativeOrZero // 负数或零private Integer nonPositiveCount;
}
三、日期时间类型校验
public class DateTimeValidation {@Future // 将来时间private LocalDate startDate;@FutureOrPresent // 将来或现在private LocalDateTime eventTime;@Past // 过去时间private Date birthDate;@PastOrPresent // 过去或现在private LocalDateTime lastLogin;// 自定义日期范围验证(需要自定义注解或方法验证)// 例如:确保结束日期在开始日期之后
}
四、集合类型校验
public class CollectionValidation {@NotNull@NotEmpty // 集合不能为空private List<String> roles;@Size(min = 1, max = 10) // 集合大小范围private Set<Long> userIds;@NotEmpty@Size(max = 5) // Map不能为空且最多5个元素private Map<String, String> properties;@UniqueElements // Hibernate扩展,元素必须唯一private List<String> uniqueTags;// 数组验证@Size(min = 1) // 数组至少一个元素private String[] items;
}
五、布尔类型校验
public class BooleanValidation {@AssertTrue // 必须为trueprivate Boolean termsAccepted;@AssertFalse // 必须为falseprivate Boolean isExpired;@NotNull // 不能为nullprivate Boolean activeFlag;
}
六、枚举类型校验
public class EnumValidation {@NotNull // 枚举不能为nullprivate Status status;// 自定义枚举值验证(如果需要特定值)
}enum Status {PENDING, ACTIVE, INACTIVE, DELETED
}
七、嵌套对象校验
public class NestedValidation {@NotNull@Valid // 启用嵌套对象验证private Address address;@Valid // 集合内的对象也会被验证private List<@Valid Product> products;
}public class Address {@NotBlankprivate String street;@NotBlankprivate String city;@Size(min = 5, max = 10)private String postalCode;
}
八、★ 自定义注解校验复杂逻辑
自定义校验需要:自定义注解和自定义注解校验器。
1. 自定义注解
- 我们自定义一个性别校验注解,规定数据值只能是男和女。
- 重点在:
@Constraint(validatedBy = {GenderValidation.class})
约束注解,我们必须在这个地方填写我们自定义的校验器:GenderValidation.class
。
/*** 自定义数据校验注解*/
// 这个注解只能用在类的字段(成员变量)上
@Target(ElementType.FIELD)
// 表示注解在运行时保留,可以通过反射读取
@Retention(RetentionPolicy.RUNTIME)
// 元注解,这个注解应该被包含在JavaDoc中
@Documented
// 校验器,声明这是一个Bean Validation约束注解
@Constraint(validatedBy = {GenderValidation.class})
public @interface Gender {// 定义验证失败时的错误消息,默认提示信息String message() default "性别只能是男和女";// 用于分组验证,创建用户时验证A组字段,更新用户时验证B组字段Class<?>[] groups() default {};// 用于传递元数据给验证过程Class<? extends Payload>[] payload() default {};
}
2. 自定义注解校验器
步骤:
- 实现
ConstraintValidator
接口,接口参数类型必须为注解名称和准备接收的参数类型。注意:我们@Gender
注解,规定只能用在String类型的字段上,因此接口准备接收的参数类型为String
。 - 实现接口
isValid()
,在这个接口中编写判定逻辑。
/*** 自定义数据校验注解实现类*/
public class GenderValidation implements ConstraintValidator<Gender, String> {@Overridepublic void initialize(Gender constraintAnnotation) {ConstraintValidator.super.initialize(constraintAnnotation);}@Overridepublic boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {// 只有当参数s,为男或女时为真return "男".equals(s) || "女".equals(s);}
}
3. 在实体类中使用
@Gender
注解,是我们自定义的注解。只允许添加男和女。
@Data
public class User {@NotNull@Min(1)private Integer id;@NotBlank(message = "名称不能为空")private String name;@NotBlank@Size(min = 6, max = 12, message = "The password must be 6 to 12 characters in length.")private String password;@NotNull@AssertTrue(message = "必须同意注册协议!")private Boolean active;@Gender(message = "只允许为男性或女性!")private String gender;}
4. 访问并验证
-
- 准备JSON字符串:
{"id": 1,"name":"张三","password": "111111","active": true,"gender": "9999"
}
-
- 访问地址。
/*** 全局异常处理,捕获数据校验异常,查看数据异常类型。* @param user 实体类* @return*/@PostMapping("data2")public R data2(@Validated @RequestBody User user) {return R.success(user.toString());}
- 结果返回:自定义校验注解,成功生效!
{"code": 500,"msg": "数据校验异常","data": {"gender": "只允许为男性或女性!"}
}
九、完整综合示例
public class UserRegistrationDto {// 字符串验证@NotBlank(message = "用户名不能为空")@Size(min = 3, max = 20, message = "用户名长度3-20字符")@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字、下划线")private String username;@NotBlank@Email(message = "邮箱格式不正确")private String email;// 数值验证@Min(value = 18, message = "必须年满18岁")@Max(value = 100, message = "年龄不能超过100岁")private Integer age;@DecimalMin("0.0")@Digits(integer = 6, fraction = 2)private BigDecimal balance;// 日期验证@Past(message = "生日必须是过去日期")private LocalDate birthDate;@Futureprivate LocalDateTime membershipExpiry;// 集合验证@Size(min = 1, max = 5, message = "至少选择1个,最多5个兴趣")private List<String> interests;// 布尔验证@AssertTrue(message = "必须接受服务条款")private Boolean termsAccepted;// 嵌套对象验证@NotNull@Validprivate Address address;// 自定义验证@FieldMatch(first = "password", second = "confirmPassword")private String password;private String confirmPassword;
}
十、最佳实践总结
1. 按数据类型选择合适注解
- 字符串:
@NotBlank
,@Size
,@Pattern
,@Email
- 数值:
@Min
/@Max
(整数),@DecimalMin
/@DecimalMax
(小数) - 日期:
@Past
,@Future
系列 - 集合:
@NotEmpty
,@Size
- 布尔:
@AssertTrue
/@AssertFalse
2. 验证层次
public class ValidationHierarchy {@NotNull // 第一层:非空检查@NotBlank // 第二层:内容检查 @Size(max = 100) // 第三层:格式/大小检查@Email // 第四层:业务规则检查private String email;
}
3. 组合验证策略
// 基础数据完整性
@NotNull + @NotBlank/@NotEmpty// 格式和范围
@Size + @Pattern + @Min/@Max// 业务规则
@AssertTrue + 自定义注解 + 类级别验证
数据校验注解使用示例
若想校验用户传递过来的数据,必须使用
@Validated
或@Valid
注解开启校验功能。
1. 在POM文件中导入jar包
<!--导入java规范包,web包是一个精简包--><dependency><groupId>jakarta.platform</groupId><artifactId>jakarta.jakartaee-web-api</artifactId><version>10.0.0</version><scope>provided</scope></dependency><!--扩展规范--><!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator --><dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>8.0.2.Final</version></dependency><!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator-annotation-processor --><!--构建时会有报错警告--><dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator-annotation-processor</artifactId><version>8.0.2.Final</version></dependency>
2. 创建一个实体类,并使用参数校验注解。
@Data
public class User {@NotNull@Min(1)private Integer id;@NotBlank(message = "名称不能为空")private String name;@NotBlank@Size(min = 6, max = 12, message = "The password must be 6 to 12 characters in length.")private String password;@NotNull@AssertTrue(message = "必须同意注册协议!")private Boolean active;}
3. 在控制器中开启校验。
- 必须使用
@Validated
或@Valid
注解,放在模型对象的前面,方能开启校验对象功能。 - 必须放在最前面。
@RestController
@RequestMapping("user")
public class UserController {@PostMapping("info")public String info(@Validated @RequestBody User user) {return "user info信息: " + user.toString();}@PostMapping("data")public String data(@Validated @RequestBody User user, BindingResult result) {if (result.hasErrors()) {return result.getFieldError().toString();}return "user info信息: " + user.toString();}
}
- 不需要
@Valid
和@Validated
注解的情况,在方法参数中使用。
@PostMapping("param")public String param(@RequestParam(value = "name") @Size(min = 2) String name) {return "user info信息: " + name;}
数据校验错误加入到全局统一异常
- 当我们使用数据校验注解是,如果数据校验失败报错则会导致页面跳转到400页面,并不会返回我么之前定义的全局统一异常格式。
- 因此,若想将数据校验失败的异常错误加入到全局异常中,需要定义一个异常处理器。
我们通常将数据校验错误纳入全局统一异常处理,是为了统一处理校验失败的情况,避免在每个控制器方法中重复处理绑定结果(BindingResult
),从而提高代码的复用性和可维护性。
在Spring MVC中,我们通常使用@Valid
或@Validated
注解来触发数据校验。如果校验失败,默认会抛出MethodArgumentNotValidException
(对于@RequestBody
)或BindException
(对于@ModelAttribute
)。我们可以通过全局异常处理器(@ControllerAdvice
)来捕获这些异常,并统一返回错误信息。
这样做的好处:
- 避免在每个控制器方法中编写重复的
BindingResult
处理代码。 - 统一错误响应格式,便于前端处理。
- 将业务逻辑与参数校验错误处理分离,使代码更清晰。