本文将介绍Spring与Bean Validation的整合,内容还是蛮有干货的,大致分为以下几点:
-
Spring 数据校验
-
简单的数据校验
-
级联校验(层次性校验)
-
分组校验
-
构造器与方法参数,返回值的校验
-
-
自定义Constraints
-
国际化
-
数据校验的全局异常处理
-
属性取值
更多的请查看另外一篇文章 bean Validation
一、Spring数据校验
Spring牛逼一点在于,它提供了特别多的其他框架与功能的集成;数据校验也不例外,Spring提供了很好的与hibernate-validator的集成。需要引入的依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.7.3</version>
</dependency>
bean Validation的校验目标,无论什么校验,都可以大致分为以下三种分类:
- field or property 字段与属性
- container element 容器元素
- constructor or method 参数与返回值
下述的校验也都是围绕这三类来的。
1、@Validated与@Valid
javax.validation.Valid 与 org.springframework.validation.annotation.Validated, 不会吧,不会吧,不会还有人分不清楚这两个注解的作用吧😆😆
以下介绍两者的区别,同时下面的很多示例中都会用到:
- @Valid:
- 属于bean Validation标准中的标注注解
- 用于bean的级联验证
- 没有任何属性
- 可作用于方法、属性、构造器、参数、以及类型参数 上
介绍@Validated之前,我们可以想想,Spring与bean Validation的整合是怎样起作用的呢 ?
没错就是下面这个玩意 !!!
-
@Validated:
-
属于Spring提供的校验注解
-
用在SpringMvc的参数上开启校验
-
与方法级验证一起使用(可以指定组),当标注在特定类上时,该类中的某些标注了校验注解的方法将会被验证(作为相应验证拦截器的切入点);
注意: 不能再次将该注解标注在校验方法上,无效,如下错误示例:
@Validated public interface PhoneService { void insertPhone(@Validated Phone phone); }
-
存在一个value属性,进行group的指定,用于进行分组校验
-
可作用于类、接口、枚举、方法、参数上
-
不懂 ?没关系,看下面的示例再反过头来看应该就懂了 。
2、简单的数据校验
简单的数据校验仅仅介绍SpringMvc参数上的校验;
关于Service层的方法校验可以看下面:构造器|方法参数、返回值校验。
2.1 url 参数校验
用法:
- 在controller上添加@Validated注解;
- 在参数上标注校验注解;
示例如下:
@RestController
@RequestMapping("/simpleValid")
@Validated
public class SimpleValidController {
/**
* url参数校验
* @param orgId
* @param userId
* @return
*/
@GetMapping("/parameterValid")
public String parameterValid(@NotEmpty @RequestParam("orgId") String orgId,
@NotEmpty @RequestParam("userId") String userId){
return "success";
}
}
2.2 requestBody对象参数校验
用法:
- 在controller方法的参数上添加@Validated注解;
- 在参数实体bean的属性上添加校验注解;
示例如下:
bean:
@Data
public class SimpleValidBean {
@NotEmpty
private String orgId;
@NotEmpty
private String userId;
private int size;
}
controller:
@RestController
@RequestMapping("/simpleValid")
public class SimpleValidController {
/**
* body Bean实体校验
* @param bean
* @return
*/
@PostMapping("/beanValid")
public String beanValid(@Validated @RequestBody SimpleValidBean bean){
return "success";
}
}
注意:
对于url校验与requestBody校验,在同一个controller方法中,无法两者同时发生。如下的错误示例
:
@RestController
@RequestMapping("/simpleValid")
@Validated
public class SimpleValidController {
/**
* url参数校验
* @param orgId
* @param userId
* @return
*/
@GetMapping("/parameterValid")
public String parameterValid(@NotEmpty @RequestParam("orgId") String orgId, @Validated @RequestBody SimpleValidBean bean){
return "success";
}
}
2.3 容器元素校验
对于容器元素的校验bean Valiadtion也是支持的
用法:
- 在requestBody bean参数上添加@Validated注解
- 在<>内添加校验注解
示例如下:
bean:
@Data
public class CollectionBean {
private Optional<@NotEmpty(message = "Optional容器内元素不能为空") String> optional;
private Map<@NotEmpty(message = "Map容器Key不能为空") String, @Email(message = "Map容器value格式不符合email格式") String> map;
private List<@NotEmpty(message = "List容器内元素不能为空") String> list;
private Set<@NotEmpty(message = "Set容器内元素不能为空") String> set;
private Map<String, @NotNull List<@NotEmpty(message = "mapList的List内元素不能为空") String>> mapList;
}
controller:
@RestController
@RequestMapping("/simpleValid")
public class SimpleValidController {
/**
* 容器内元素数据校验
* @param bean
* @return
*/
@PostMapping("/collectionValid")
public String collectionValid(@RequestBody @Validated CollectionBean bean){
return "success";
}
}
2.4 容器元素校验的隐式解包
用法:
-
在requestBody bean参数上添加 @Validated
-
在bean的容器类型参数上添加校验注解,通过指定payload是作用容器本身还是容器内的元素。
-
Unwrapping.Skip.class 校验容器本身
-
Unwrapping.Unwrap.class 校验容器内的元素
-
示例如下:
bean:
@Data
public class HiddPackupCollectionBean {
@NotEmpty(payload = Unwrapping.Skip.class, message = "skipList不能为空, 校验List")
private List<String> skipList;
@NotEmpty(payload = Unwrapping.Unwrap.class, message = "unwrapList不能为空, 校验List内的元素")
private List<String> unwrapList;
}
controller:
@RestController
@RequestMapping("/simpleValid")
@Validated
public class SimpleValidController {
/**
* 容器内元素数据校验的隐式解包
* @param bean
* @return
*/
@PostMapping("/collectionHiddrenPackupValid")
public String collectionHiddPackupValid(@RequestBody @Validated HiddPackupCollectionBean bean){
return "success";
}
}
3、级联校验
什么是级联校验 ?对象内套对象,一层又一层,此时也会校验内层的对象,这称之为级联校验。
如何开启级联校验 ? 此时就需要@Valid起作用啦 !
注意:
需要注意的是一旦内部bean类型字段为null,就会终止内部bean的校验,因此可以添加个@NotNull注解
通用bean:
@Data
public class Head {
@Min(value = 1, message = "head.size 大小为1")
private int size;
@NotEmpty(message = "head.name 不能为空")
private String name;
}
3.1 字段 bean类型校验
用法:
- requestBody bean参数添加@Validated注解
- bean内部的bean类型字段添加@Valid
示例如下:
bean:
@Data
public class SimplePeople {
@Valid
@NotNull(message = "head 不能为null")
private Head head;
}
cotroller:
@RestController
@RequestMapping("/objectAreaValid")
public class ObjectAreaValidController {
/**
* bean 对象校验
* @param bean
* @return
*/
@PostMapping("/filedValid")
public String filedValid(@Validated @RequestBody SimplePeople bean){
return "success";
}
}
3.2 字段 容器类型校验
3.2.1 list
用法:
- requestBody bean参数添加@Validated注解
- bean内部的List类型字段添加@Valid(可以选择放在<> 内,也可以放置在字段上)。
示例如下:
bean:
@Data
public class ListSimplePeople {
@Valid
private List<Head> headList2;
private List<@Valid Head> headList;
}
controller:
@RestController
@RequestMapping("/objectAreaValid")
public class ObjectAreaValidController {
/**
* 容器 - list校验
* @param bean
* @return
*/
@PostMapping("/listValid")
public String listValid(@Validated @RequestBody ListSimplePeople bean){
return "success";
}
}
3.2.2 set
用法:
-
requestBody bean参数添加@Validated注解
-
bean内部的Set类型字段添加@Valid(可以选择放在<> 内,也可以放置在字段上)。
示例如下:
bean:
@Data
public class SetSimplePeople {
@Valid
private Set<Head> set2;
private Set<@Valid Head> set;
}
controller:
@RestController
@RequestMapping("/objectAreaValid")
public class ObjectAreaValidController {
/**
* 容器 - set校验
* @param bean
* @return
*/
@PostMapping("/setValid")
public String setValid(@Validated @RequestBody SetSimplePeople bean){
return "success";
}
}
3.2.3 map
map有点特殊,因为它分为key,value部分,你可以选择对key进行层次性校验(不过似乎没法传这样的json),也可以选择对value进行层次性校验。
如果@Valid直接标注在字段上,校验的是其value值。
用法:
- requestBody bean参数添加@Validated注解
- bean内部的Map类型字段的value添加@Valid(可以选择放在<> 内,也可以放置在字段上)。
示例如下:
bean:
@Data
public class MapSimplePeople {
private Map<@NotEmpty String, @Valid Head> headMap;
@Valid
private Map<@NotEmpty String, Head> headMap1;
}
controller:
@RestController
@RequestMapping("/objectAreaValid")
public class ObjectAreaValidController {
/**
* 容器 - map校验
* @param bean
* @return
*/
@PostMapping("/mapValid")
public String mapValid(@Validated @RequestBody MapSimplePeople bean) {
return "success";
}
}
3.2.4 容器内套容器
用法:
- requestBody bean参数添加@Validated注解
- bean内部的容器类型字段的内部容器的<>里面添加@Valid
示例如下:
bean:
@Data
public class NestedSimplePeople {
@NotEmpty
private List<@NotEmpty List<@Valid Head>> headList;
@NotEmpty
private Map<@NotEmpty String,@NotEmpty List<@Valid Head>> mapList;
}
controller:
@RestController
@RequestMapping("/objectAreaValid")
public class ObjectAreaValidController {
/**
* 容器内是容器的容器元素校验
* @param bean
* @return
*/
@PostMapping("/nestedcollectionValid")
public String nestedcollectionValid(@Validated @RequestBody NestedSimplePeople bean) {
return "success";
}
}
4、分组校验
bean validation支持分组校验,一个使用场景在于当你插入时需要校验的参数指定为Insert组;更新时所需要校验的参数指定为update组,按照接口进行不同组的校验。
在不指定的情况下,归属于Default组。
group被定义为接口的形式。
4.1 简单的分组校验
使用:
- requestBody的bean参数上添加@Validated,通过value属性指定校验组。
- 在bean的字段上添加校验注解,并通过groups指定校验组,如果没指定,则默认归属于Default组。
示例如下:
bean:
@Data
public class SimpleGroupBean {
@Size(groups = SimpleGroup.class,min = 2)
private String simpleGroupSize;
@Size(groups = Default.class, min = 2)
private String defaultGrSize;
}
controller:
@RestController
@RequestMapping("/groupValid")
public class GroupValidController {
/**
* 简单的分组校验
* @param bean
* @return
*/
@PostMapping("/simpleGroupValid")
public String simpleGroupValid(@Validated(value = SimpleGroup.class) @RequestBody SimpleGroupBean bean) {
return "success";
}
}
分析:
此时只会校验标注SimpleGroup组的字段。
4.2 多组校验
@Validated的value属性可以指定多个组进行多组校验。
用法:
- requestBody的bean参数上添加@Validated,通过value属性指定校验组。
- 在bean的字段上添加校验注解,并通过groups指定多个校验组。
示例如下:
bean:
@Data
public class SimpleGroupBean {
@Size(groups = SimpleGroup.class,min = 2)
private String simpleGroupSize;
@Size(groups = Default.class, min = 2)
private String defaultGrSize;
}
controller:
@RestController
@RequestMapping("/groupValid")
public class GroupValidController {
/**
* 多组校验
* @param bean
* @return
*/
@PostMapping("/mulitGroupValid")
public String mulitGroupValid(@Validated(value = {SimpleGroup.class, Default.class}) @RequestBody SimpleGroupBean bean) {
return "success";
}
}
解析:
此时SimpleGroup组, Default组都将会被校验。
4.3 组继承校验
既然group为接口,那么肯定会存在继承啦。
示例如下:
group:
public interface SuperGroup {
}
-----------
public interface ChildGroup extends SuperGroup {
}
bean:
@Data
public class ExtendGroupBean {
@Size(min = 2, groups = SuperGroup.class)
private String childSize;
@Size(min = 2, groups = Default.class)
private String defaultSize;
}
controller:
@RestController
@RequestMapping("/groupValid")
public class GroupValidController {
/**
* 组继承校验
* @param bean
* @return
*/
@PostMapping("/groupExtendValid")
public String groupExtendValid(@Validated(value = {Default.class, ChildGroup.class}) @RequestBody ExtendGroupBean bean) {
return "success";
}
}
解析:
由于ChildGroup继承了SuperGroup组,因此如果指定了ChildGroup组进行校验,此时也会校验标注SuperGroup组的字段。
4.4 组序校验
有时校验存在前后的关系,那么此时可以采用组序校验,这需要借助@GroupSequence注解。
一旦前一步组校验不满足,后续组将不会再进行校验。
使用:
- 定义一个group,上面标注@GroupSequence注解,并通过设置value属性指定顺序
- 定义bean,并在属性上标注校验注解,并指定组
- 在requestBody属性bean上标注@Validated,并通过value属性设置成第一步定义的group
示例如下:
GroupSequence:
@GroupSequence(value = {SimpleGroup.class,ChildGroup.class})
public interface SortGroup {
}
bean:
@Data
public class SortGroupBean {
@NotEmpty(groups = SimpleGroup.class)
private String simpleValue;
@NotEmpty(groups = SuperGroup.class)
private String superValue;
}
controller:
@RestController
@RequestMapping("/groupValid")
public class GroupValidController {
/**
* 组序校验
* @param bean
* 此时会先验证 SimpleGroup组,再验证ChildGroup以及父组
* 一旦前一步不满足,后续不再执行
* @return
*/
@PostMapping("/groupSortValid")
public String groupSortValid(@Validated(value = SortGroup.class) @RequestBody SortGroupBean bean){
return "success";
}
}
分析:
此时会先验证 SimpleGroup组,再验证ChildGroup以及父组,一旦前一步组校验不满足,后续不再执行。
4.5 重定义默认组
什么叫重定义默认组 ?使用上表现为,当你使用Default组进行校验时,会校验其他组,而且还有顺序。
使用:
- 定义一个校验bean,在属性上添加校验注解,并设置组
- 在bean上添加@GroupSequence注解,设置value属性值为value = {xx.class, Groupxx.class, Groupxxo.class},其中第一个值为bean的类名,后面的,后续值为指定的组(顺序校验哦)。
- 在requestBody bean参数上直接添加@Validated注解,不用指定组。
示例如下:
bean:
@Data
@GroupSequence(value = {RedirectDefaultGroupBean.class, RedirectDefaultGroupOne.class, RedirectDefaultGroupTwo.class})
public class RedirectDefaultGroupBean {
@NotEmpty(groups = Default.class, message = "defaultValue不能呢为空")
private String defaultValue;
@NotEmpty(groups = RedirectDefaultGroupOne.class, message = "redirectOneValue不能呢为空")
private String redirectOneValue;
@NotEmpty(groups = RedirectDefaultGroupTwo.class, message = "redirectTwoValue不能呢为空")
private String redirectTwoValue;
}
controller:
@RestController
@RequestMapping("/groupValid")
public class GroupValidController {
/**
* 重定义默认组校验
* @param bean
* @return
*/
@PostMapping("/defaultCovertValid")
public String defaultCovertValid(@Validated @RequestBody RedirectDefaultGroupBean bean) {
return "success";
}
}
分析:
此时会依次的校验Default组 -》RedirectDefaultGroupOne组 -》RedirectDefaultGroupTwo组,一旦上一个组的校验不满足,后续组的校验将不会再执行。
4.6 组转换
在级联校验中,对校验的组进行转换。
在不进行组转换的情况下, group具有传递性,即内部bean的校验也会遵从外部传递过来的group。
使用:
- 在bean的内部属性类型为bean的属性上添加@ConvertGroup注解,from属性为待转换的group, 默认为Default;to属性为转换成的group。
- 接下来就是正常的使用啦
示例如下:
bean:
@Data
public class CovertGroupBean {
@Valid
@ConvertGroup(from = Default.class, to = SuperCovertGroup.class)
private CovertValueGroupBean covertValueGroupBean;
@NotEmpty
private String defaultValue;
}
----------------
@Data
public class CovertValueGroupBean {
@NotEmpty(groups = SuperCovertGroup.class)
private String superCovertGroupValue;
@NotEmpty
private String defaultGroupValue;
}
controller:
@RestController
@RequestMapping("/groupValid")
public class GroupValidController {
/**
* 组转换
* @param bean
* @return
*/
@PostMapping("/covertValid")
public String covertValid(@Validated(value = Default.class) @RequestBody CovertGroupBean bean) {
return "success";
}
}
分析:
刚开始指定的校验组为Default,当校验内部covertValueGroupBean属性bean时,由于指定了@ConvertGroup(from = Default.class, to = SuperCovertGroup.class),此时校验CovertValueGroupBean的组会转成SuperCovertGroup;
5、方法参数、返回值校验
此处仅仅介绍方法参数、返回值的校验,以Service层方法为例;
使用很简单,直接在Service上添加@Validated;
- 参数校验将校验注解标注在参数上;
- 返回值校验将参数标注在方法上。
注意:对于构造器的参数,返回值校验通常是不必要的,而且Spring提供的@Validated注解也不支持,如果你硬要校验的话,需要采用手动编码的方式,如下:
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Driver driver = new Driver();
driver.setAge(16);
Car porsche = new Car();
driver.setCar(porsche);
Set<ConstraintViolation<Driver>> violations = validator.validate(driver);
assert violations.size() == 2;
通用bean:
@Data
public class SimpleMethodParamValidBean {
@NotEmpty(message = "username不能为空")
private String userName;
@NotEmpty(message = "passWord不能为空")
private String passWord;
}
@Data
public class AreaSimpleMethodValidBean {
@Valid
private SimpleMethodParamValidBean simpleMethodParamValidBean;
@NotEmpty(message = "username不能为空")
private String userName;
@NotEmpty(message = "passWord不能为空")
private String passWord;
}
5.1 方法参数校验
5.1.1 方法普通参数校验
service:
@Service
@Validated
public class MethodServiceImpl {
/**
* 方法参数校验
* @param username
* @param passWord
* @return
*/
public String paramValid(@NotEmpty(message = "paramValid.username 不能为空") String username,
@NotEmpty(message = "paramValid.passWord 不能为空")String passWord){
return "username = " + username + ", passWord = " + passWord;
}
}
controller:
@RequestMapping("/constructParamAndReturnValue")
@RestController
public class MethodParamAndReturnValueController {
@Resource
private MethodServiceImpl methodService;
/**
* 方法普通参数校验
* @param userName
* @param passWord
* @return
*/
@PostMapping("/paramValid")
public String paramValid(@RequestParam("userName") String userName, @RequestParam("passWord") String passWord){
return methodService.paramValid(userName,passWord);
}
}
5.1.2 方法bean参数校验
service:
@Service
@Validated
public class MethodServiceImpl {
/**
* 方法参数bean校验
* @param bean
* @return
*/
public String paramBeanValid(@Valid SimpleMethodParamValidBean bean){
return "username = " + bean.getUserName() + ", passWord = " + bean.getPassWord();
}
}
controller:
@RequestMapping("/constructParamAndReturnValue")
@RestController
public class MethodParamAndReturnValueController {
@Resource
private MethodServiceImpl methodService;
/**
* 方法bean参数校验
* @return
*/
@PostMapping("/paramBeanValid")
public String paramBeanValid(@RequestBody SimpleMethodParamValidBean bean){
return methodService.paramBeanValid(bean);
}
}
5.1.3 方法层次性bean参数校验
service:
@Service
@Validated
public class MethodServiceImpl {
/**
* 方法参数层次性bean校验
* @param bean
* @return
*/
public String areaParamBeanValid(@NotNull @Valid AreaSimpleMethodValidBean bean){
return "username = " + bean.getUserName() + ", passWord = " + bean.getPassWord();
}
}
controller:
@RequestMapping("/constructParamAndReturnValue")
@RestController
public class MethodParamAndReturnValueController {
/**
* 方法层次性bean参数校验
* @return
*/
@PostMapping("/areaParamBeanValid")
public String areaParamBeanValid(@RequestBody AreaSimpleMethodValidBean bean){
return methodService.areaParamBeanValid(bean);
}
}
5.2 方法返回值校验
5.2.1 方法返回普通值校验
service:
@Service
@Validated
public class MethodServiceImpl{
/**
* 方法返回普通值校验
* @param bean
* @return
*/
@NotEmpty(message = "returnValid方法返回值参数不能为空")
public String returnValid(SimpleMethodParamValidBean bean){
if (bean.getPassWord() == null || bean.getPassWord().equals("")) {
return "";
}
return "username = " + bean.getUserName() + ", passWord = " + bean.getPassWord();
}
}
controller:
@RequestMapping("/constructParamAndReturnValue")
@RestController
public class MethodParamAndReturnValueController {
@Resource
private MethodServiceImpl methodService;
/**
* 方法返回普通值校验
* @return
*/
@PostMapping("/methodReturnValid")
public String methodReturnValid(@RequestBody SimpleMethodParamValidBean bean){
return methodService.returnValid(bean);
}
}
5.2.2 方法返回bean校验
service:
@Service
@Validated
public class MethodServiceImpl{
/**
* 方法返回值bean校验
* @param bean
* @return
*/
@Valid
public SimpleMethodParamValidBean returnBeanValid(SimpleMethodParamValidBean bean){
return bean;
}
}
controller:
@RequestMapping("/constructParamAndReturnValue")
@RestController
public class MethodParamAndReturnValueController {
/**
* 方法返回bean校验
* @return
*/
@PostMapping("/returnBeanValid")
public SimpleMethodParamValidBean returnBeanValid(@RequestBody SimpleMethodParamValidBean bean){
return methodService.returnBeanValid(bean);
}
}
5.2.3 方法返回值层次性bean校验
service:
@Service
@Validated
public class MethodServiceImpl{
/**
* 方法返回值层次性bean校验
* @param bean
* @return
*/
@Valid
public AreaSimpleMethodValidBean areaReturnBeanValid(AreaSimpleMethodValidBean bean){
return bean;
}
}
controller:
@RequestMapping("/constructParamAndReturnValue")
@RestController
public class MethodParamAndReturnValueController {
/**
* 方法返回值层次性bean校验
* @return
*/
@PostMapping("/areaReturnBeanValid")
public AreaSimpleMethodValidBean areaReturnBeanValid(@RequestBody AreaSimpleMethodValidBean bean){
return methodService.areaReturnBeanValid(bean);
}
}
5.3 继承层次的方法参数返回值校验
在继承层次结构中定义方法约束(即通过扩展基类的类继承和通过实现或扩展接口的接口继承)时,必须遵守Liskov 替换原则,该原则要求:
- 不能在子类型中加强方法的前提条件(由参数约束表示) - 即不能在子类的重写方法中给参数加校验注解
- 不能在子类型中削弱方法的后置条件(由返回值约束表示)- 即不能在父类的被重写方法中给返回值加校验注解
仅适用于一般方法,验证构造函数约束时不适用,因为构造函数不会相互重写。
示例如下:
interface:
public interface MethodService {
String methodExtendParamValid(@NotEmpty String userName, @NotEmpty @Email String passWord);
String methodExtendReturnValid(String value);
}
service:
@Service
@Validated
public class MethodServiceImpl implements MethodService {
@Override
public String methodExtendParamValid(String userName, String passWord) {
return "username = " + userName + ", passWord = " + passWord;
}
@Override
@NotEmpty
@Email
public String methodExtendReturnValid(String value) {
return value;
}
}
controller:
@RequestMapping("/constructParamAndReturnValue")
@RestController
public class MethodParamAndReturnValueController {
@Resource
private MethodServiceImpl methodService;
/**
* 方法继承参数校验
* @param userName
* @param passWord
* @return
*/
@PostMapping("/methodExtendParamValid")
public String methodExtendParamValid(@RequestBody SimpleMethodParamValidBean bean) {
return methodService.methodExtendParamValid(bean.getUserName(),bean.getPassWord());
}
/**
* 方法继承返回值校验
* @return
*/
@PostMapping("/methodExtendReturnValid")
public String methodExtendReturnValid(@RequestParam("value") String value){
return methodService.methodExtendReturnValid(value);
}
}
二、消息插值
很多时候我们需要对校验失败后的message进行更人性化(比如校验的范围是啥,当前值是啥)的处理,这就涉及到消息插值了。
bean Validation支持两种消息插值方式:
-
消息参数 - 使用
{}
,取值范围如下:-
ResourceBundle``ValidationMessages``/ValidationMessages.properties
内的值。 -
内置的ResourceBundle内的值。
-
Constraint的属性名对应的值。
-
-
消息表达式 - 使用
${}
,取值范围如下:- Constraint的属性名对应的值。
- 在名称validatedValue下映射的验证值。
特别特别注意:消息参数取值先于消息表达式。
2.1 消息参数
2.1.1 ResourceBundle
这与国际化有关,关于更加详细的看下面的国际化章节。
ValidationMessages.properties:
valid.email.message=邮箱格式错误
controller:
@RestController
@RequestMapping("/message")
@Validated
public class MessageController {
/**
* {} -> ResourceBundle - ValidationMessages/ValidationMessages.properties
* @param email
* @return
*/
@GetMapping("/resourceBundleMessage")
public String resourceBundleMessage(@RequestParam("email") @Email(message = "{valid.email.message}") String email){
return "success";
}
}
错误消息如下:
邮箱格式错误
2.1.2 Constraint的对应属性名称值
controller:
@RestController
@RequestMapping("/message")
@Validated
public class MessageController {
/**
* {} -> 校验注解的对应属性名称值
* @param length
* @return
*/
@GetMapping("/propertiesMessage")
public String propertiesMessage(@RequestParam("length") @Length(min = 1, max = 10, message = "length值需要在{min} - {max}之间") String length){
return "success";
}
}
错误消息如下:
length值需要在1 - 10之间
2.2 消息表达式
2.2.1 validatedValue
controller:
@RestController
@RequestMapping("/message")
@Validated
public class MessageController {
/**
* ${} -> validatedValue
* @param email
* @return
*/
@GetMapping("/validatedValueMessage")
public String validatedValueMessage(@RequestParam("email") @Email(message = "email格式不满足,当前值为${validatedValue}") String email){
return "success";
}
}
错误消息如下:
email格式不满足,当前值为321
2.2.2 Constraint的对应属性名称值
需要注意的是,由于消息参数优先于消息表达式的方式,因此取值时展现的效果可能是这样的:
controllr:
@RestController
@RequestMapping("/message")
@Validated
public class MessageController {
/**
* ${} -> 校验注解的对应属性名称值
* @param length
* @return
*/
@GetMapping("/expPropertiesMessage")
public String expPropertiesMessage(@RequestParam("length") @Length(min = 1, max = 10, message = "length值需要在${min} - ${max}之间") String length){
return "success";
}
}
错误消息如下:
length值需要在$1 - $10之间
出现这样的原因就是由于消息参数解析优先于消息表达式解析,所以取校验注解的对应属性名称值就不要用${}
,去用{}
吧
2.3 混合方式
alidationMessages.properties:
valid.length.blend.message=长度应该在{min}-{max}之间,当前值为${validatedValue}
controller:
@RestController
@RequestMapping("/message")
@Validated
public class MessageController {
/**
* ${}、{} -> 混合插值
* @param length
* @return
*/
@GetMapping("/blendMessage")
public String blendMessage(@RequestParam("length") @Length(min = 1, max = 10, message = "{valid.length.blend.message}") String length){
return "success";
}
}
错误消息如下:
长度应该在1-10之间,当前值为41412412414124124124
三、国际化
国际化与消息插值具有关系,因为我们需要根据消息参数的方式去ResourceBundle中拿到对应的值。
以下介绍两种方式:
3.1 方式一
定义一个名称为ValidationMessages的ResourceBundle,如下:
内容:
ValidationMessages_en_US.properties文件:
valid.size.message=size must in the {min} - {max}, currentValue is ${validatedValue}
ValidationMessages_zh_CN.properties文件:
valid.size.message=size 必须在{min} - {max}之间, 当前值为${validatedValue}
controller:
@RequestMapping("/valid")
@RestController
@Validated
public class HelloController {
@GetMapping("/helloWord")
public String helloWord(@RequestParam @Size(min = 0,
max = 100,
message = "{valid.size.message}") int value){
return "success";
}
}
3.2 方式二
此处介绍通过配置MessageResolver与LocaleResolver来自定义的国际化,并不需要国际化文件名必须为 ValidationMessages。
LocaleResolver:
解析Request中的语言标志参数或者head中的Accept-Language参数, 并将解析后的参数保存到指定的LocaleContextHolder域中。
SpringMVC默认提供了四种实现了接口的类:
- CookieLocaleResolver
- AcceptHeaderLocalResolver
- FixdLocaleResolver
- SessionLocaleResolver。
MessageResolver:
国际化消息处理的顶层接口,
存在两个实现类:ResourceBundleMessageResolver、PropertiesMessageResolver,当然你也可以实现AbstractMessageResolver来自定义MessageResolver
MessageSource:
以用于支持信息的国际化和包含参数的信息的替换。
配置流程:
主要流程是通过配置springboot的LocaleResolver解析器,当请求打到springboot的时候对请求的所需要的语言进解析,并保存在LocaleContextHolder中。
MessageResolver解析器从MessageSource中获取到key对应的message,而locale值是从LocaleContextHolder获取的。
1、定义LocaleResolver,并注册bean
@Configuration
public class LocaleResolverConfig {
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
acceptHeaderLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return acceptHeaderLocaleResolver;
}
}
2、定义 MessageResolver
@RequiredArgsConstructor
public class MessageResolverConfig extends AbstractMessageResolver {
private final MessageSource messageSource;
@Override
protected String getMessage(String key) {
return messageSource.getMessage(key,null, LocaleContextHolder.getLocale());
}
}
3、将自定义的MessageResolver注册为bean
@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final MessageSource messageSource;
/**
* 配置自定义的 Passay 消息解析器
* @return MessageResolver
*/
@Bean
public MessageResolver messageResolver() {
return new MessageResolverConfig(messageSource);
}
/**
* 配置 Java Validation 使用国际化的消息资源
*
* @return LocalValidatorFactoryBean
*/
@Bean
@Override
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
}
4、配置国际化文件的位置
spring.messages.basename=message # 多个文件用逗号分隔,使用/表示层级
5、像方式一那样使用就行啦
四、自定义Constraints
此处仅仅展示一个简单的示例,更多的请查看另外一篇文章。
大致可以分为两步:
- 定义校验注解
- 定义ConstraintValidator约束验证器
以下示例展示一个电话号码的验证
1、校验注解
@Target({TYPE,ANNOTATION_TYPE,FIELD,CONSTRUCTOR,PARAMETER,METHOD})
@Retention(RUNTIME)
@Constraint(validatedBy = {MobileConstraint.class})
@Repeatable(IsMobile.List.class)
public @interface IsMobile {
String message() default "{valid.mobile.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({TYPE, ANNOTATION_TYPE, FIELD, CONSTRUCTOR, PARAMETER, METHOD})
@Retention(RUNTIME)
@interface List {
IsMobile[] value();
}
}
@Constraint内的validatedBy属性用于指定约束验证器,其他的倒是没啥。
message的default设置的是ResourceBundle内设置的key值,bean validation会为我们进行消息参数与消息表达式的解析进行消息插值。
内容如下:
valid.mobile.message=电话号码错误,当前的电话号码为${validatedValue}
2、约束验证器
/**
*@Description 电话号码验证器
*@Version
**/
public class MobileConstraint implements ConstraintValidator<IsMobile,String> {
private final Pattern p = Pattern.compile("^((13\\d{9}$)|(15[0,1,2,3,5,6,7,8,9]\\d{8}$)|(18[0,2,5,6,7,8,9]\\d{8}$)|(147\\d{8})$)");
/**
* 获取注解内的属性值
* @param constraintAnnotation
*/
@Override
public void initialize(IsMobile constraintAnnotation) {
}
/**
* 是否通过校验
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
Matcher m = p.matcher(value);
return m.matches();
}
}
3、使用
@RestController
@RequestMapping("/constraint")
@Validated
public class ConstraintController {
@GetMapping("/isMobile")
public String isMobile(@RequestParam @IsMobile String mobile){
return "success";
}
}
错误如下:
电话号码错误,当前的电话号码为13443
五、数据校验的异常处理
5.1 controller方法校验异常捕获
对于错误消息的获取可以在方法参数中加入BindingResult参数
@GetMapping(value = "/simpleValid")
public ResponseData simpleValid(@Validated(value = {Default.class}) User user, BindingResult resultError){
if (resultError.hasErrors()) {
return ResponseData
.error(resultError.getAllErrors()
.stream()
.map(e -> e.getDefaultMessage())
.collect(Collectors.joining(",")));
}
phoneService.insertPhone(user.getPhone());
return ResponseData.success();
}
5.2 全局的异常处理
在一个一个的Controller方法中获取BindingResult对象,这样子不是太优雅,此时就可以使用全局的异常处理了。
关于数据校验绑定时的异常,为以下两个异常(其他的异常在开发时就应该处理了,比如你使用的方式并不符合规范):
-
BindException:对SpringMVC的参数进行校验是会出现该异常。
-
ValidationException:@Validated标注在特定类上,对某些方法上需要校验的Bean,出现校验失败时会出现该异常。
以下是一个简单的示例:
@RestControllerAdvice
public class ValidatorExceptionHandler {
/**
* 当绑定错误被认为是致命的时抛出。 -》 从 5.3 开始 MethodArgumentNotValidException 扩展了BindException
* @param bindException
* @return
*/
@ExceptionHandler(value = {BindException.class})
private ResponseData handlerError(BindException bindException) {
String errFields = bindException.getBindingResult().getFieldErrors().stream().map(e -> e.getField()).collect(Collectors.joining(","));
String errorMsg = bindException.getAllErrors().stream().map(e ->e.getDefaultMessage()).collect(Collectors.joining(","));
return ResponseData.error("属性 = " + errFields + ", errorMsg = " + errorMsg).status(false);
}
/**
* 所有Jakarta Bean验证“意外”问题的基本异常, -》 大部分情况下是抛出ConstraintViolationException,该异常包含更多的异常信息
* @param exception
* @return
*/
@ExceptionHandler(value = {ValidationException.class})
private ResponseData handlerError(ValidationException exception){
return ResponseData.error("数据校验错误:" + exception.getMessage()).status(false);
}
}