Spring 数据校验

本文将介绍Spring与Bean Validation的整合,内容还是蛮有干货的,大致分为以下几点:

  • Spring 数据校验

    • 简单的数据校验

    • 级联校验(层次性校验)

    • 分组校验

    • 构造器与方法参数,返回值的校验

  • 自定义Constraints

  • 国际化

  • 数据校验的全局异常处理

  • 属性取值

更多的请查看另外一篇文章 bean Validation
 

一、Spring数据校验

Spring牛逼一点在于,它提供了特别多的其他框架与功能的集成;数据校验也不例外,Spring提供了很好的与hibernate-validator的集成。需要引入的依赖如下:
 

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-validationartifactId>
    <version>2.7.3version>
dependency>

bean Validation的校验目标,无论什么校验,都可以大致分为以下三种分类:

  • field or property 字段与属性
  • container element 容器元素
  • constructor or method 参数与返回值

下述的校验也都是围绕这三类来的。

 

1、@Validated与@Valid

javax.validation.Valid 与 org.springframework.validation.annotation.Validated, 不会吧,不会吧,不会还有人分不清楚这两个注解的作用吧
 
以下介绍两者的区别,同时下面的很多示例中都会用到:

  • @Valid:
    1. 属于bean Validation标准中的标注注解
    2. 用于bean的级联验证
    3. 没有任何属性
    4. 可作用于方法、属性、构造器、参数、以及类型参数 上

 
介绍@Validated之前,我们可以想想,Spring与bean Validation的整合是怎样起作用的呢 ?

没错就是下面这个玩意 !!!

  • @Validated:

    1. 属于Spring提供的校验注解

    2. 用在SpringMvc的参数上开启校验

    3. 与方法级验证一起使用(可以指定组),当标注在特定类上时,该类中的某些标注了校验注解的方法将会被验证(作为相应验证拦截器的切入点);

      注意: 不能再次将该注解标注在校验方法上,无效,如下错误示例:

      @Validated
      public interface PhoneService {
      
          void insertPhone(@Validated Phone phone);
      }
      
    4. 存在一个value属性,进行group的指定,用于进行分组校验

    5. 可作用于类、接口、枚举、方法、参数上

不懂 ?没关系,看下面的示例再反过头来看应该就懂了 。

 

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,如下:

Spring 数据校验_第1张图片

 
内容:

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。

img

 
MessageResolver:

国际化消息处理的顶层接口,

存在两个实现类:ResourceBundleMessageResolver、PropertiesMessageResolver,当然你也可以实现AbstractMessageResolver来自定义MessageResolver

 
MessageSource:

以用于支持信息的国际化和包含参数的信息的替换。

 
配置流程:

主要流程是通过配置springboot的LocaleResolver解析器,当请求打到springboot的时候对请求的所需要的语言进解析,并保存在LocaleContextHolder中。

MessageResolver解析器从MessageSource中获取到key对应的message,而locale值是从LocaleContextHolder获取的。

Spring 数据校验_第2张图片

 
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);
    }

}

你可能感兴趣的:(数据校验,spring,java,后端)