Spring Boot使用Hibernate validator进行参数校验
转载:https://blog.csdn.net/qq_34021712/article/details/87545287
一、概念
针对于参数校验仅在客户端进行校验不是安全可靠的,可以进行抓包直接调取接口。参数校验是基本上每个后端服务都需要进行的操作,但是在每个服务都加上if/else去判空,就显得代码有些冗余且不美观、不便于维护,例如下图所示:
于是就诞生了Hibernate validator,由于SpringBoot的Starters机制,只需要导入对应的Starters依赖就可以使用相关的API。Hibernate Validator 是Hibernate项目中的一个数据校验框架,是 JSR 380 参考实现。Hibernate Validator、Bean Validation API 和 TCK 都是使用了Apache Software License 2.0。Hibernate Validator 6 和 Bean Validation 2.0 需要 Java8 或更新版本。
二、基本使用
-
导入Pom依赖
org.springframework.boot spring-boot-starter-validation -
参数校验Demo
package com.example.demo.model; import lombok.Data; import javax.validation.constraints.*; import java.io.Serializable; /** * @Author xiaoMu * @Date 2021/8/11 20:23 * @Description 用户信息 */ @Data public class UserModelRequest implements Serializable { /** 姓名 */ @NotBlank(message = "用户姓名不能为空") private String name; /** 身份证号码 */ @Pattern(regexp = "^(\\d{18}|\\d{17}(\\d{1}|[X|x]))$",message = "身份证格式不正确") private String idCardNum; /** 年龄 */ @NotNull(message="用户年龄不能为空") @Size(max = 120,min = 1,message = "年龄不正确") private Integer age; /** 家庭住址 */ @NotBlank(message = "用户地址不能为空") private String address; /** 是否已确认 */ @NotNull(message="用户必须确认") @AssertTrue(message = "用户必须确认") private Boolean confirmResult; } -
Controller方法配置
package com.example.demo; import com.example.demo.model.UserModelRequest; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; /** * @Author helin * @Date 2021/5/17 11:21 * @Description NULL */ @RestController @RequestMapping("/demo") public class controller { @PostMapping("/createUser") public String demo(@RequestBody @Valid UserModelRequest userModelRequest, BindingResult result) { if (result.hasErrors()) { for (ObjectError error : result.getAllErrors()) { System.out.println(error.getDefaultMessage()); } } return "Hello World"; } }// Postman 传参
{"name":"","idCardNum":"","age":"","address":"","confirmResult":""}// 输出结果
用户姓名不能为空 用户地址不能为空 用户必须确认 身份证格式不正确 用户年龄不能为空
三、校验模式与实践
Hibernate Validator的校验模式有两种,第一种是普通模式(默认):普通模式会验证所有的属性,并打印所有匹配到不符合规定的验证失败信息;第二种是快速失败模式:顾名思义就是当匹配到不符合规定约束时会立即验证返回。下面介绍两种配置验证方式:
第一种 设置Hibernate Validator特定选项
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class ) .configure() .failFast( true ) .buildValidatorFactory(); Validator validator = validatorFactory.getValidator();
第二种 可以通过Configuration#addProperty()传递特定选项
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class ) .configure() .addProperty( "hibernate.validator.fail_fast", "true" ) .buildValidatorFactory(); Validator validator = validatorFactory.getValidator();
Spring Boot 配置hibernate Validator为快速失败模式:
package com.example.demo.config; 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; /** * @Author helin * @Date 2021/8/11 21:12 * @Description 配置hibernate Validator为快速失败模式 */ @Configuration public class ValidatorConfiguration { @Bean public Validator validator(){ ValidatorFactory factory = Validation .byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory(); return factory.getValidator(); } }
捕获全局参数校验异常并返回错误提示信息
在上述案例中,每个请求都加BindindResult参数,显然如果每个都去加一遍又是重复操作。BindindResult不加,再去请求时会抛出org.springframework.web.bind.MethodArgumentNotValidException。其次,上面的案例是用@RequestBody接收请求参数,如果换成@RequestParam,又会抛出异常:javax.validation.ConstraintViolationException,我们可以捕获这两个异常,在这里进行统一处理,并返回。
package com.example.demo.exception; import com.example.demo.common.ReturnResult; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.util.List; import java.util.Set; /** * @Author xiaoMu * @Date 2021/8/12 15:25 * @Description 参数校验异常捕获 */ @Component @ControllerAdvice public class GlobalExceptionHandlerAdvice { @ExceptionHandler @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public ReturnResult handleConstraintViolationException(ConstraintViolationException e){ Set> violations = e.getConstraintViolations(); StringBuilder stringBuilder = new StringBuilder(); for (ConstraintViolation<?> violation : violations) { stringBuilder.append(violation.getMessage()).append(" "); } ReturnResult result = new ReturnResult(); result.setCode(HttpStatus.BAD_REQUEST.value()); result.setMessage(stringBuilder.toString()); return result; } @ExceptionHandler @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public ReturnResult handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { BindingResult bindingResult = exception.getBindingResult(); List fieldErrors = bindingResult.getFieldErrors(); StringBuilder stringBuilder = new StringBuilder(); for (FieldError item : fieldErrors) { //将所有的异常信息封装成String stringBuilder.append(item.getDefaultMessage()).append(" "); } ReturnResult returnResult = new ReturnResult(); returnResult.setCode(HttpStatus.BAD_REQUEST.value()); returnResult.setMessage(stringBuilder.toString()); return returnResult; } }
捕获异常后,返回结果又SpringBoot自带的英文格式返回变成我们自己的返回格式:
@RequestParam注解使用Validator校验
配置MethodValidationPostProcessor
@Configuration public class ValidatorConfiguration { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); //设置validator模式为快速失败 postProcessor.setValidator(validator()); return postProcessor; } @Bean public Validator validator(){ ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class ) .configure() .failFast(true) .buildValidatorFactory(); Validator validator = validatorFactory.getValidator(); return validator; } }
Controller类上添加@Validated , 然后在方法参数内使用注解校验。
@Controller @Validated public class TestValidationController { @RequestMapping(value = "/demo2",method = RequestMethod.GET) @ResponseBody public void demo2(@NotBlank(message = "用户姓名不能为空") @RequestParam String name, @NotNull(message = "年龄不正确") @Range(min = 1,max = 150,message = "年龄不正确") @RequestParam Integer age){ System.out.println(name); System.out.println(age); } }
四、其他使用方式
- 在方法内对model类进行校验
package com.example.demo; import com.example.demo.model.UserModelRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.validation.ConstraintViolation; import javax.validation.Validator; import java.util.Set; /** * @Author xiaoMu * @Date 2021/5/17 11:21 * @Description NULL */ @RestController @RequestMapping("/demo") public class controller { @Autowired private Validator validator; @GetMapping("test") public void testModelMethod(){ UserModelRequest requestUser = new UserModelRequest(); requestUser.setName("小木"); requestUser.setAge(0); requestUser.setIdCardNum("123456781234567812345678"); Set
> validate = validator.validate(requestUser); for (ConstraintViolation model : validate) { System.out.println(model.getMessage()); } } } -
对象级联校验
1)对象的内部包含另一个对象作为属性,在该属性上添加@Valid,例如A类中 包含B类 在B属性上添加@Valid,校验A也可以校验Bpublic class A { @NotBlank(message = "用户姓名不能为空") private String name; @NotNull(message="用户年龄不能为空") @Min(value = 1,message = "年龄不正确") @Max(value = 150,message = "年龄不正确") private Integer age; @NotNull(message = "B不存在") @Valid private B b; } public class B { @Range(min = 1000000,message = "你不能没有钱") private double money; }
2)进行级联校验
@Autowired private Validator validator; @RequestMapping(value = "/demo4",method = RequestMethod.GET) @ResponseBody public void demo4(){ A a = new A(); a.setAge(8); a.setName("小木"); B b = new B(); a.setB(b); Set
> validate = validator.validate(a); for (ConstraintViolation model : validate) { System.out.println(model.getMessage()); } } - 分组校验
有时候需对一个实体类进行多重校验方式,在不同的情况下使用不同的验证方式,例如:新增和修改唯一的区别在于,新增没有ID而修改有
GroupA、GroupB:
public interface GroupA extends Default { } public interface GroupB extends Default { }
实体类
public class UserInfo { /** 主键 */ @NotBlank(message = "用户id不存在",groups = {GroupA.class}) private Integer id; /** 姓名 */ @NotBlank(message = "用户姓名不能为空",groups = {GroupA.class, GroupB.class}) private String name; /** 身份证号 */ @Pattern(regexp="^(\\d{18}|\\d{17}(\\d{1}|[X|x]))$",message="身份证格式不正确",groups = {GroupA.class, GroupB.class}) private String idCardNum; /** 年龄 */ @NotNull(message="用户年龄不能为空",groups = {GroupA.class, GroupB.class}) @Range(min = 1,max = 150,message = "年龄必须在[1,150]",groups = {GroupA.class, GroupB.class}) private Integer age; /** 家庭住址 */ @NotBlank(message = "用户地址不能为空",groups = {GroupA.class, GroupB.class}) private String address; /** 状态 0 可用 1 不可用 */ @NotBlank(message = "用户状态不能为空",groups = {Default.class}) private String state; }
根据Group存放的不同位置来判断这个组包含了哪些字段:GroupA验证字段id,name,idCardNum,age,address,state。GroupB验证字段name,idCardNum,age,address,state。Default验证字段state(Default是Validator自带的默认分组) - 自定义序列
默认情况下,无论它们属于哪个组,都不会按特定顺序计算约束。但是,在某些情况下,控制约束的评估顺序很有用。例如你要开车,首先你要通过驾照的检测,你才可以开车,为了实现这样的验证顺序,您只需要定义一个接口并对其添加@GroupSequence,定义必须验证组的顺序 ,这样前面组验证不通过的,后面组不进行验证
@GroupSequence({ GroupA.class, GroupB.class, Default.class }) public interface OrderedChecks { }controler代码
@RequestMapping(value = "/demo7",method = RequestMethod.POST) @ResponseBody public void demo7(@RequestBody @Validated( { OrderedChecks.class })UserInfo userInfo){ System.out.println(userInfo.toString()); }