10、SpringBoot参数校验

1、概述

JSR相关的概念就不赘述了,网上一搜一大把。只要知道以下内容的区别即可:

  • Bean Validation,(javax.validation)包下的接口规范。
  • Hibernate Validation,Hibernate对于上述规范的具体实现。
  • Spring Validation,是对Hibernate的二次封装,在Spring环境中使用起来更为方便。

今天主要总结SpringBoot中进行参数校验的几种方案。

2、注解说明

常用的注解有:@NotNull、@NotEmpty、@NotBlank、@Email、@Pattern、@Past、@Size、@Min、@Max

  • @NotNull

可以注释在任何类型的变量上,而且不能是基本类型,必须是基本类型对应的包装类,否则不起作用
例如:

@NotNull(message = "不能为空")
private int times;

虽然程序可以正常运行,不报错,但是@NotNull注解并不起作用,因为times是基本类型,且是成员变量,默认值为0。

修改成:

@NotNull(message = "不能为空")
private Integer times;

即可生效。

  • @NotEmpty和@NotBlank

这两个注解需要放在一起理解,都是表示变量不为空
这两个注解都是架在字符串类型的变量上,且不能用于其他类型,否则运行时会报错。
区别在于:
@NotEmpty不会去掉空白字符, 例如变量为 “”会报错,而 “  ”就不会报错
@NotBlank会自动去掉空白字符,不管是“ ”还是“      ”,在它眼里都是“”,都会报错。

  • @Email、@Pattern、@Past、@Size、@Min 和 @Max

这几个注解可以放在一起看,在不同的场景下,使用不同的注解
1、@Email:加在字符串上,用来校验是否满足邮箱格式。
2、@Pattern:按照正则表达式来校验
3、@Past:加在日期格式上,用来表示不能是未来的日期,只能是过去的日期
4、@Size:表示集合的数量大小
例如
@Size(min = 1, message = "不能少于1个好友")
private List<UserInfo>friends;
5、@Min和@Max
加在整形上,表示数值的上下限,对于基本类型(int),也是有效的

注意点:
上述的几个注解并不能保证数据不为null,如果需要保证数据不为null,必须配合@NotNull标签
例如
@Past(message = "时间不能是未来的时间")
@NotNull(message = "日期不能不填")
private Date birthday;

  • 嵌套校验
  @Size(min = 1, message = "不能少于1个好友") //为空时不会报错
  private List<@Valid UserInfo>friends;

 

3、非Spring环境测试Demo

3.1、依赖

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>2.0.1.Final</version>
    </dependency>

    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.1.5.Final</version>
    </dependency>

    <dependency>
      <groupId>javax.el</groupId>
      <artifactId>javax.el-api</artifactId>
      <version>3.0.0</version>
    </dependency>

    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>javax.el</artifactId>
      <version>2.2.6</version>
    </dependency>

 

3.2、pojo类

public class UserInfo {

    public interface UpdateGroup {}

    public interface InsertGroup {}

    @NotNull(message = "用户id不能为空", groups = {UpdateGroup.class})
    private String userId;

    //依次验证每个组
    @GroupSequence({
            UpdateGroup.class,
            InsertGroup.class,
            Default.class
    })
    public interface group {}


    @NotEmpty(message = "用户名不能为空")  //不会自动去掉前后空格
    private String userName;

    //有多个条件时,如果都不满足,会打印所有信息
    @NotBlank(message = "用户密码不能为空")  //会自动去掉字符串中的空格
    @Length(min = 6, max = 20, message = "密码不能少于6位,多于20位")
    private String password;

    @Email(message = "必须为有效邮箱")  //为空时不会报错
    private String email;

    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
    private String phone;

    @Min(value = 18, message = "年龄必须大于18岁")
    @Max(value = 68, message = "年龄必须小于68岁") //为空时不会报错
    private Integer age;

    @Past(message = "时间不能是未来的时间")
    @NotNull(message = "日期不能不填")
    private Date birthday;

    @Size(min = 1, message = "不能少于1个好友") //为空时不会报错
    private List<UserInfo>friends;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public List<UserInfo> getFriends() {
        return friends;
    }

    public void setFriends(List<UserInfo> friends) {
        this.friends = friends;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "UserInfo{" +
                "userId='" + userId + '\'' +
                ", userName='" + userName + '\'' +
                ", password='" + password + '\'' +
                ", email='" + email + '\'' +
                ", phone='" + phone + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                ", friends=" + friends +
                '}';
    }
}

3.3 分组

以前一直有一个疑惑,validation参数校验确实很方便,但是某些情况下并不能满足开发需求。

比如添加用户信息和更新用户信息这两个业务场景下,添加用户信息不需要传入用户id,一般后台生成,而更新用户信息时必须传入用户id。这两种情况对userId这个字段的要求不一样。

validation校验框架提供了一种策略——分组,将不同的校验方式划分在不同的分组下。例如:

@NotNull(message = "用户id不能为空", groups = {UpdateGroup.class})
private String userId;

该标签只有在分组为 UpdateGroup.class时,才生效。这里的字节码仅仅时用来区分不同的分组、表示唯一性,没有特殊的含义。(可以联想到synchronized)。

因为没什么含义,为了方便,在上述pojo类里面添加了两个接口

public interface UpdateGroup {}

public interface InsertGroup {}

为了减少耦合,也可以将这两个接口定在外面。

其实validation已经的默认分组为Default.class,所以就算是不指定分组,也会被自动的划分到Default.class分组下(联想到Object类)。

在使用的时候,可以指定一个或者多个分组:

    @Test
    public void groupValidation(){
        set = validator.validate(userInfo, UserInfo.UpdateGroup.class, Default.class);
    }

完整的测试Demo见下方的测试类。这种写法表示按照 UpdateGroup和Default分组进行校验。

当涉及多个分组的时候,就会出现一个校验顺序的问题,我们可以指定分组顺序,让框架按照我们自定义的顺序依次校验:

    //依次验证每个组
    @GroupSequence({
            UpdateGroup.class,
            InsertGroup.class,
            Default.class
    })
    public interface group {}

 

3.4 测试类

/**
 * 验证测试类
 */
public class ValidationTest {

    private Validator validator;

    private UserInfo userInfo;

    private Set<ConstraintViolation<UserInfo>> set;

    /**
     * 初始化操作
     */
    @Before
    public void init() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
        userInfo = new UserInfo();
        userInfo.setUserId("123");
        userInfo.setUserName(" ");
        userInfo.setPassword("1234576");
        userInfo.setEmail("hello@gamil.com");
        userInfo.setAge(70);
        Calendar calendar = Calendar.getInstance();
        calendar.set(1968, Calendar.FEBRUARY,1);
        userInfo.setBirthday(calendar.getTime());
        userInfo.setFriends(new ArrayList() {{add(new UserInfo());}});
    }

    /**
     * 结果打印
     */
    @After
    public void print(){
        set.forEach(item -> {
            //输出错误信息
            System.out.println(item.getMessage());
        });
    }

    @Test
    public void nullValidation(){
        //用验证器对对象进行验证
        set = validator.validate(userInfo);
    }

    @Test
    public void groupValidation(){
        set = validator.validate(userInfo, UserInfo.UpdateGroup.class, Default.class);
    }
}

4、Spring环境Demo

4.1 环境说明

SpringBoot版本:

 <version>2.3.4.RELEASE</version>

依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>


<!--validation相关-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

4.2 pojo类

public class UserInfo {

@NotNull(message = "用户id不能为空", groups = {UpdateGroup.class})
private String userId;

@NotEmpty(message = "用户名不能为空") //不会自动去掉前后空格
private String userName;

//有多个条件时,如果都不满足,会打印所有信息
@NotBlank(message = "用户密码不能为空") //会自动去掉字符串中的空格
@Length(min = 6, max = 20, message = "密码不能少于6位,多于20位")
private String password;

@Email(message = "必须为有效邮箱") //为空时不会报错
@NotNull
private String email;

@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
private String phone;

@Min(value = 18, message = "年龄必须大于18岁")
@Max(value = 68, message = "年龄必须小于68岁") //为空时不会报错
private Integer age;

@Past(message = "时间不能是未来的时间") //为空时不会报错
private Date birthday;

@Size(min = 1, message = "不能少于1个好友") //为空时不会报错
private List<UserInfo>friends;

@GenderValue(message = "性别输入不合法", genders = {"男","女"})
private String gender;

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPhone() {
return phone;
}

public void setPhone(String phone) {
this.phone = phone;
}

public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}

public List<UserInfo> getFriends() {
return friends;
}

public void setFriends(List<UserInfo> friends) {
this.friends = friends;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "UserInfo{" +
"userId='" + userId + '\'' +
", userName='" + userName + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", phone='" + phone + '\'' +
", age=" + age +
", birthday=" + birthday +
", friends=" + friends +
'}';
}
}

4.3 分组

public interface InsertGroup {
}

 

public interface UpdateGroup {
}

注意:分组校验的时候,必须要用Hibernate提供的@Valided注解。

4.4 非全局异常处理

一般我们会在controller进行传参校验,校验不通过的时候会抛出异常,需要对异常信息进行有效的提取,并返回给前端。先看一种非全局的处理方法:

@RestController
public class UserController {

    @Autowired
    UserInfoService userInfoService;

    @RequestMapping("/register")
    public CommonRes register(@Valid UserInfo userInfo, BindingResult bindingResult){
        userInfoService.register(userInfo);
        if(bindingResult.hasErrors()){
            Map<String, String> map = new HashMap<>();
            bindingResult.getFieldErrors().forEach((item) -> {
                String message = item.getDefaultMessage();
                String filed = item.getField();
                map.put(filed, message);
            });
            return CommonRes.fail(map.toString());
        }
        return CommonRes.success(null);
    }

    @RequestMapping("/update")
    public CommonRes updateUserInfo(@Valid UserInfo userInfo, BindingResult bindingResult){
        userInfoService.update(userInfo);
        if(bindingResult.hasErrors()){
            return CommonRes.fail(bindingResult.getAllErrors().toString());
        }
        return CommonRes.success(null);
    }
}

优点:较为灵活

缺点:每次校验都需要写一遍bindingResult的处理代码,比较繁琐

4.5 全局异常处理

结合Spring中的全局异常处理方案,我们可以对校验出来的结果进行全局处理。

异常处理类:

@ControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
    public CommonRes handleValidException(Exception e) {
        BindingResult bindingResult = null;

        if(e instanceof MethodArgumentNotValidException) {
            bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();
        } else if (e instanceof BindException) {
            bindingResult = ((BindException)e).getBindingResult();
        }

        Map<String, String>errorMap = new HashMap<>(16);
        assert bindingResult != null;
        bindingResult.getFieldErrors().forEach(fieldError -> errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()));
        return CommonRes.fail(errorMap.toString());
    }
}

将Controller层代码改为

@RestController
public class UserController {

    @Autowired
    UserInfoService userInfoService;

    @RequestMapping("/register")
    public CommonRes register(UserInfo userInfo){
        userInfoService.register(userInfo);
        // if(bindingResult.hasErrors()){
        //     Map<String, String> map = new HashMap<>();
        //     bindingResult.getFieldErrors().forEach((item) -> {
        //         String message = item.getDefaultMessage();
        //         String filed = item.getField();
        //         map.put(filed, message);
        //     });
        //     return CommonRes.fail(map.toString());
        // }
        return CommonRes.success(null);
    }

    @RequestMapping("/update")
    public CommonRes updateUserInfo(UserInfo userInfo){
        userInfoService.update(userInfo);
        // if(bindingResult.hasErrors()){
        //     return CommonRes.fail(bindingResult.getAllErrors().toString());
        // }
        return CommonRes.success(null);
    }
}

优点:简化了编程,注释掉的代码就可以去掉了。

缺点:某些情况下不够灵活,比如不同参数的校验需要返回不同的状态码时,粗糙的全局处理就无法满足需求了。

但是一般来说,参数错误异常会使用统一的状态码,所以这种写法已经可以满足大多数人的校验需求了。

4.7 自定义注解

有时候现有的注解可能不能满足我们的需求,这个时候就用到自定义的注解了。

例如现在想要对gender字段进行校验,只能为“男”,或者为“女”。可以这么做

  • 定义注解
@Documented
@Constraint(validatedBy = {GenderValueConstraintValidator.class}) //交给哪个类校验
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface GenderValue {
    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String[] genders()default {};
}
  • 校验类
public class GenderValueConstraintValidator implements ConstraintValidator<GenderValue, String> {

    private Set<String> set = new HashSet<>();

    @Override
    public void initialize(GenderValue constraintAnnotation) {
        String[]genders = constraintAnnotation.genders();
        for (String gender : genders) {
            set.add(gender);
        }
    }

//校验的结果由这个类决定 @Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { return set.contains(value); } }
  • 使用方法
    @GenderValue(message = "性别输入不合法", genders = {"男","女"})
    private String gender;

 

4.8、参数校验放在controller层还是service层?

自从看到一个大神把所有的参数校验放在service层,我就有点怀疑人生了。参数校验究竟是放在哪一层呢?

花了点时间特地去查了下,最后得出的结论是:

controller层和service层都需要做校验,但是侧重点不同。

controller层偏向于参数形式上的合法性,与业务无关,比如这个字段是否能为空,邮箱格式是否正确,手机格式是否正确等等。

service层偏向于校验逻辑上的合法性,比如这个同名的用户是否已经存在,比如这个邮箱是不是已经被其他用户注册了等等。

 

4.9、其他方法

之前看到一种校验方式,将处理结果进行一定的封装,使用起来也比较方便:

  • 定义ValidationResult存放校验结果
/**
 * @author peng
 */
public class ValidationResult {

    /**
     * 结果校验是否有错
     */
    private boolean hasErrors;

    /**
     * 存放错误信息
     */
    private Map<String,String>errorMsgMap = new HashMap<>();

    public boolean isHasErrors() {
        return hasErrors;
    }

    public void setHasErrors(boolean hasErrors) {
        this.hasErrors = hasErrors;
    }

    public Map<String, String> getErrorMsgMap() {
        return errorMsgMap;
    }

    public void setErrorMsgMap(Map<String, String> errorMsgMap) {
        this.errorMsgMap = errorMsgMap;
    }

    /**
     * 实现通用的格式化字符串信息获取错误结果的msg方法。
     * @return 错误参数
     */
    public String getErrMsg(){
        return StringUtils.join(errorMsgMap.values().toArray(), ",");
    }
}
  • 校验类
/**
 * @author peng
 */
@Component
public class ValidatorImpl implements InitializingBean {

    private Validator validator;

    /**
     * 实现校验方法并返回校验结果
     */
    public ValidationResult validate(Object bean){
        ValidationResult result = new ValidationResult();
        Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(bean);
        if(constraintViolationSet.size() > 0 ){
            result.setHasErrors(true);
            constraintViolationSet.forEach(constraintViolation->{
                String errMsg = constraintViolation.getMessage();
                String propertyName = constraintViolation.getPropertyPath().toString();
                result.getErrorMsgMap().put(propertyName, errMsg);
            });
        }
        return result;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        //将hibernate validator通过工厂的初始化方式使其实例化
        this.validator = Validation.buildDefaultValidatorFactory().getValidator();
    }
}
  • 使用方法

注入验证类、返回结果

@Service
public class UserServiceImpl implements UserService, UserDetailsService {

    @Autowired
    UserDOMapper userDOMapper;

    @Autowired
    ValidatorImpl validator;

/** * * @param userModel 用户信息 * @return 0 失败,大于0 成功 * @throws BusinessException 管理员无法删除 */ @Override public int registerUser(UserModel userModel) throws BusinessException { ValidationResult validate = validator.validate(userModel); if(validate.isHasErrors()){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, validate.getErrMsg()); } UserDO userDO = userDOMapper.selectByLoginName(userModel.getLoginName()); if(userDO != null){ throw new BusinessException(EmBusinessError.USER_IS_EXIST); } return userDOMapper.insertSelective(convertFromModel(userModel)); } private UserDO convertFromModel(UserModel userModel){ if(userModel == null){ return null; } UserDO userDO = new UserDO(); BeanUtils.copyProperties(userModel, userDO); return userDO; } private UserModel convertFromDO(UserDO userDO){ if(userDO == null){ return null; } UserModel userModel = new UserModel(); BeanUtils.copyProperties(userDO, userModel); if(userDO.getUserId() != null){ List<RightModel> rightModelList = permissionService.getRightListByUserId(userDO.getUserId()); userModel.setRightModelList(rightModelList); } return userModel; } private List<UserModel> convertFromDOList(List<UserDO>userDOList){ if(userDOList == null){ return null; } return userDOList.stream().map(this::convertFromDO).collect(Collectors.toList()); } }

优缺点:算是一种比较这种的方案,可以自定义错误类型,使用起来也比较方便。

不过个人还是倾向于4.5的做法,已经可以满足大部分项目的需求了。

 

如有错误,欢迎批评指正!

posted @ 2020-10-12 10:14  小楼夜听雨QAQ  阅读(1131)  评论(0编辑  收藏  举报