参数校验

参考博客链接:

https://blog.csdn.net/weixin_43376349/article/details/106065757?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&utm_relevant_index=2


https://www.cnblogs.com/mooba/p/11276062.html

导入依赖:

  <!--参数校验引入-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

接下来来进行相应的操作即可。

1、效验RequestParam

比如验证用户名不能为空,首先编写相关的校验规则,如@NotBlank,在message中填入验证不通过时的提示语;最后,在类上增加@Validated注解,使之生效。

代码如下所示:

@RestController
@Validated
public class UserController {    

	@GetMapping("/getUser")
    public String getUserStr(
                             @Max(value = 99, message = "不能大于99岁") Integer age,
                             @Max(value = 99, message = "不能大于99") Integer num,
                             // 对于发送过来的字符串默认使用的是"",不会为空
                             @NotBlank(message = "name 不能为空") String name) {
        return "name: " + name + " ,age:" + age;
    }   
}   

注意: @Validated 注解添加的位置,如果添加到了方法参数上将不会生效

可以校验多个参数:

不能大于99
name 不能为空

2、封装到实体类

但是参数大于等三个之后,建议使用类来对参数来进行封装。

    @PostMapping("/add1")
    public String add( @RequestBody @Validated UserDto dto) {
        return "SUCCESS";
    }

对应的实体类是:

@Data
public class UserDto implements Serializable {
	private static final long serialVersionUID = 5354063703280604542L;
	
	@NotNull(message = "用户名不能为空")
	@Length(min = 4, max = 10, message = "用户名长度只能为4至10位")
	private String username;

	@NotNull(message = "密码不能为空")
	@Length(min = 6, max = 14, message = "密码长度只能为6至14位")
	private String password;

	@NotNull(message = "启用状态不能为空")
	private Boolean enable;

}

注意:这里需要将@Validated注解放到参数位置上,也是现在使用的方式。

3、使用全局异常处理器来捕捉对应的异常

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handle(ValidationException exception) {
        if(exception instanceof ConstraintViolationException){
            ConstraintViolationException exs = (ConstraintViolationException) exception;

            Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
            for (ConstraintViolation<?> item : violations) {
                //打印验证不通过的信息
                System.out.println(item.getMessage());
            }
        }
        return "bad request" ;
    }
}

当参数校验异常的时候,该统一异常处理类在控制台打印信息的同时把bad request的字符串和HttpStatus.BAD_REQUEST所表示的状态码400返回给调用方(用@ResponseBody注解实现,表示该方法的返回结果直接写入HTTP response body 中)。其中:

  • @ControllerAdvice:控制器增强,使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到所有的 @RequestMapping注解的方法。
  • @ExceptionHandler:异常处理器,此注解的作用是当出现其定义的异常时进行处理的方法,此例中处理ValidationException异常。

对应的响应消息:

不能大于99
name 不能为空
2022-01-24 18:07:04.200  WARN 51408 --- [nio-8080-exec-4] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [javax.validation.ConstraintViolationException: getUserStr.num: 不能大于99, getUserStr.name: name 不能为空]

在上面的例子中,我们使用BindingResult验证不通过的结果集合,但是通常按顺序验证到第一个字段不符合验证要求时,就可以直接拒绝请求了。这就涉及到两种校验模式的配置:

4、校验模式

  1. 普通模式(默认是这个模式): 会校验完所有的属性,然后返回所有的验证失败信息
  2. 快速失败模式: 只要有一个验证失败,则返回
    如果想要配置第二种模式,需要添加如下配置类
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

@Configuration
public class ValidatorConf {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .failFast( true )
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        return validator;
    }
}

5、参数校验分组

在实际开发中经常会遇到这种情况:想要用一个实体类去接收多个controller的参数,但是不同controller所需要的参数又有些许不同,而你又不想为这点不同去建个新的类接收参数。比如有一个/setUser接口不需要id参数,而/getUser接口又需要该参数,这种时候就可以使用参数分组来实现。

  1. 定义表示组别的interface
public interface GroupA {
}

public interface GroupB {
}

2.在@Validated中指定使用哪个组;

@RestController
@Validated
public class UserController {  
	@PostMapping("/getUser")
    public String getUserStr(@RequestBody @Validated({GroupA.class}) UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
        return "name: " + user.getName() + ", age:" + user.getAge();
    }
}

=====================================================================================================================================

@RestController
@RequestMapping("hello")
public class HelloController {

    @PostMapping("getUser")
    public String getUserStr(@RequestBody @Validated({GroupB.class}) UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
        return "name: " + user.getName() + ", age:" + user.getAge();
    }

    // 统一异常来进行对应的参数校验,会将参数异常抛出到GlobalExceptionHandler
    private void validData(BindingResult bindingResult) {
        // 获取得到绑定的错误,然后将所有的错误绑定追加提取起来,像上面抛出
        if (bindingResult.hasErrors()) {
            StringBuffer sb = new StringBuffer();
            for (ObjectError error : bindingResult.getAllErrors()) {
                sb.append(error.getDefaultMessage());
            }
            throw new ValidationException(sb.toString());
        }
    }
}

其中Defaultjavax.validation.groups中的类,表示参数类中其他没有分组的参数;如果没有,/getUser接口的参数校验就只会有标记了GroupA的参数校验生效。

看一下对应的DTO:

@Data
public class UserInfo {

    /**
     * A组才会来做参数校验的字段
     */
     @NotBlank( groups = {GroupA.class}, message = "id cannot be null")
    private Integer id;

    /**
     * B组才会来做参数校验
     */
    @NotBlank(groups = {GroupB.class},message = "username cannot be null")
    private String name;

    @NotBlank(message = "sex cannot be null")
    private String sex;

    @Max(value = 99L)
    private Integer age;
}

上面的controller层中的代码就意味着,校验A组合B组的标注了的属性;

如:在HelloController只会去校验GroupB组标注了的参数,而不会去校验其他的参数;在UserController只会取校验GroupA组标注了的参数,而不会去标注其他的参数。

对应的测试类如下所示:

测试B组参数校验:

{
    "id":null,
    "name":"liguang",
    "sex":null,
    "age":null
}

测试A组参数校验:

{
    "id":11,
    "name":null,
    "sex":null,
    "age":null
}

上面两种测试可以看到都是正常的。

但是还有一种情况,就是想如果没有标注了的,也需要来做参数校验,那么需要按照下面中使用方式来进行操作。

3.在实体类的注解中标记这个哪个组所使用的参数;

@Data
public class UserInfo {
    // A组才会使用到的参数校验属性
    @NotNull( groups = {GroupA.class}, message = "id cannot be null")
    private Integer id;

    @NotNull(message = "username cannot be null")
    private String name;

    @NotNull(message = "sex cannot be null")
    private String sex;

    @Max(value = 99L)
    private Integer age;
}

需要注意的是:在由@Validated标注了value属性后,实体类中未标注groups属性的字段校验将会失效。示例如下:

// 自定义接口
public interface Update {
}

// 实体类
public class Demo {

	@NotNull(groups = Update.class)
	private Integer id;
	
	@NotBlank
	private String name;

} 

// Controller 方法
public Object test(@RequestBody @Validated(Update.class)) {
	...
}

以上Demo类的id字段会被校验而name字段则不会处理。

原因:
校验属性默认的groups为Default.class,该类位于javax.validation.groups包下。如果自定义了接口,则只会处理该被接口标注的字段。

解决方法:
自定义接口继承Default类即可。

import javax.validation.groups.Default;
// 自定义接口
public interface Update extends Default{
}

那么即使在上面的name没有标注,那么也会来进行校验。

但是二者结合使用的话,我感觉还不如全部进行校验。

6、级联参数校验

当参数bean中的属性又是一个复杂数据类型或者是一个集合的时候,如果需要对其进行进一步的校验需要考虑哪些情况呢?

@Data
public class UserInfo {
    @NotNull( groups = {GroupA.class}, message = "id cannot be null")
    private Integer id;

    @NotNull(message = "username cannot be null")
    private String name;

    @NotNull(message = "sex cannot be null")
    private String sex;

    @Max(value = 99L)
    private Integer age;
   
    @NotEmpty
    private List<Parent> parents;
}

比如对于parents参数,@NotEmpty只能保证list不为空,但是list中的元素是否为空、User对象中的属性是否合格,还需要进一步的校验。这个时候我们可以这样写:

@NotEmpty
private List<@NotNull @Valid UserInfo> parents;

然后再继续在UserInfo类中使用注解对每个参数进行校验。

但是我们再回过头来看看,在controller中对实体类进行校验的时候使用的@Validated,在这里只能使用@Valid,否则会报错。关于这两个注解的具体区别可以参考@Valid 和@Validated的关系,但是在这里我想说的是使用@Valid就没办法对UserInfo进行分组校验。这种时候我们就会想,如果能够定义自己的validator就好了,最好能支持分组,像函数一样调用对目标参数进行校验,就像下面的validObject方法一样:

import javax.validation.Validator;

@RestController
public class PingController {
    @Autowired
    private Validator validator;

    @PostMapping("/setUser")
    public String setUser(@RequestBody @Validated UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
        Parent parent = user.getParent();
        validObject(parent, validator, GroupB.class, Default.class);
        return "name: " + user.getName() + ", age:" + user.getAge();
    }

    private void validData(BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            StringBuffer sb = new StringBuffer();
            for (ObjectError error : bindingResult.getAllErrors()) {
                sb.append(error.getDefaultMessage());
            }
            throw new ValidationException(sb.toString());
        }
    }

    /**
     * 实体类参数有效性验证
     * @param bean 验证的实体对象
     * @param groups 验证组
     * @return 验证成功:返回true;验证失败:将错误信息添加到message中
     */
    public void validObject(Object bean, Validator validator, Class<?> ...groups) {
        Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(bean, groups);
        if (!constraintViolationSet.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (ConstraintViolation violation: constraintViolationSet) {
                sb.append(violation.getMessage());
            }

            throw new ValidationException(sb.toString());
        }
    }
}


@Data
public class Parent {
    @NotEmpty(message = "parent name cannot be empty", groups = {GroupB.class})
    private String name;

    @Email(message = "should be email format")
    private String email;
}
posted @ 2022-01-24 19:30  写的代码很烂  阅读(347)  评论(0编辑  收藏  举报