本文看点
前言
验证数据是贯通所有应用程序层(从表示层到长久层)的常见工作。通常在每一层实现雷同的验证逻辑,这既费时又容易出错。为了防止反复这些验证,开发人员常常将验证逻辑间接捆绑到域模型中,将域类与验证代码混在一起,这些验证代码实际上是对于类自身的元数据,与业务逻辑不相干。
JSR 380——Bean Validation2.0——定义了用于实体和办法验证的元数据模型和API,将数据校验逻辑通过注解的模式封装在实体对象中。
1.对于JSR
JSR是Java Specification Requests的缩写,意思是Java 标准提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式申请。任何人都能够提交JSR,以向Java平台削减新的API和服务。JSR已成为Java界的一个重要规范。
JSR-303 是JAVA EE 6 中的一项子标准,起初的版本是Bean Validation 1.1(JSR-349),目前最新版本是Bean Validation 2.0(JSR-380),Hibernate Validator 是 Bean Validation 的参考实现 ,除了Jakarta Bean验证API定义的束缚之外,Hibernate Validator还有一些附加的 constraint;并且spring-boot-starter-web默认集成了Hibernate Validator。(springboot2.3版本曾经移除hibernate-validator的依赖,须要手动引入)
2.为什么应用Hibernate Validator
- 进步代码整洁度;
- 验证逻辑与业务逻辑之间进行了拆散,升高了程序耦合度;
- 对立且标准的验证形式,无需你再次编写反复的验证代码;
- 你将更专一于你的业务,将这些繁琐的事件通通丢在一边。
3.注解介绍
JSR 380内置罕用注解
注解 | 详细信息 |
---|---|
@Null | 被正文的元素必须为 null |
@NotNull | 被正文的元素必须不为 null |
@AssertTrue | 被正文的元素必须为 true |
@AssertFalse | 被正文的元素必须为 false |
@Min(value) | 被正文的元素能够是字符串、数值类型,如果元素是字符串类型,将值转为BigDecimal类型,并与value属性进行比对,值必须大于等于指定的value值 |
@Max(value) | 被正文的元素能够是字符串、数值类型,如果元素是字符串类型,将值转为BigDecimal类型,并与value属性进行比对,值必须小于等于指定的value值 |
@DecimalMin(value) | 被正文的元素能够是字符串、数值(能够带小数点),将注解内value的值转为BigDecimal类型,必须大于等于指定的最小值(能够配置是否等于value,默认是蕴含的) |
@DecimalMax(value) | 被正文的元素能够是字符串、数值(能够带小数点),将注解内value的值转为BigDecimal类型,其值必须小于等于指定的最大值(能够配置是否等于value,默认是蕴含的) |
@Size(max, min) | 被正文的元素的大小必须在指定的范畴内,可用于字符串、Collection、Map、数组等类型 |
@Digits (integer, fraction) | 被正文的元素必须是一个数字,其值必须在可承受的范畴内 |
@Past | 被正文的元素必须是一个过来的日期 |
@Future | 被正文的元素必须是一个未来的日期 |
@Pattern(value) | 被正文的元素必须合乎指定的正则表达式 |
@Email | 被正文的元素必须是电子邮箱地址 |
@NotBlank | 验证字符串非null,且trim后长度必须大于0 |
@NotEmpty | 实用于String、Collection、Map或者数组不能为Null且长度或元素个数必须大于0 |
@Valid | 具体作用上面会列举 |
Hibernate Validator 附加的 constraint
注解 | 详细信息 |
---|---|
@Length | 被正文的字符串的大小必须在指定的范畴内 |
@URL | 依据RFC2396规范校验正文的字符串必须是一个的无效的url |
@Range | 被正文的元素必须在适合的范畴内,利用于数值或字符串 |
@UniqueElements | 查看带正文的汇合是否只蕴含惟一的元素。相等性是应用equals()办法确定的。 |
@SafeHtml | 查看带正文的值是否蕴含潜在的歹意片段,如<script/> 。如用这个注解须要引入jsoup的依赖,用来解析html代码 |
留神
- @NotNull :实用于任何类型被注解的元素,必须不能为NULL
- @NotEmpty :实用于String、Collection、Map或者数组,不能为Null且长度或元素个数必须大于0
- @NotBlank:验证字符串非null,且trim后长度必须大于0
@Validated与@Valid的区别:
- @Validated注解是spring提供的,提供了一个分组性能,能够在入参验证时,依据不同的分组采纳不同的验证机制。没有增加分组属性时,默认验证没有分组的验证属性(Default分组);
- @Validated:能够用在类型、办法和办法参数上,然而不能用在成员属性(字段)上;
- @Validated: 用在办法入参上无奈独自提供嵌套验证性能,也无奈提醒框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
- @Valid:作为规范JSR-303标准,还没有排汇分组的性能;
- @Valid:能够用在办法、办法参数、构造函数、办法参数和成员属性(字段)上;
- @Valid加在办法参数时并不可能主动进行嵌套验证,而是用在须要嵌套验证类的相应字段上,来配合办法参数上@Validated或@Valid来进行嵌套验证。
4.应用
因为spring-boot-starter-web(springboot 2.3以下版本)依赖默认集成了Hibernate Validator,所以无需增加任何依赖和相干配置,只须要在我的项目中引入spring-boot-starter-web依赖即可(演示springboot版本为2.1.2.RELEASE),因为要用到@SafeHtml注解,这里须要加上jsoup的依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> <!-- 解析html片段--><dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.8.3</version></dependency>
Hibernate Validator有两种校验模式:
- 一般模式(会校验完所有的属性,而后返回所有的验证失败信息,默认是这个模式)
疾速失败返回模式(只有有一个字段验证失败,就返回后果)
在@Configuration Class中配置以下代码,将Validator设置为疾速失败返回模式
@Bean public Validator validator(){ ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class) .configure() .addProperty( "hibernate.validator.fail_fast", "true" ) .buildValidatorFactory(); Validator validator = validatorFactory.getValidator(); return validator; }
a.对象校验
1.在对象中增加注解
@Datapublic class User { //注解对动态变量不失效 @NotBlank(message = "性别不能为空") private static String sex; @NotBlank(message = "姓名不能为空") @Size(min = 2,max = 5,message = "姓名长度不标准") private String name; @NotNull(message = "年龄不能为空") @Max(value = 30,message = "年龄超过最大值30") @Range(min=30,max=60) private Integer age; @DecimalMax(value = "108.88",message = "超过最大108.88",inclusive = false) private Double price; @Past(message = "生日不能大于以后日期") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime birthday; @Email(message = "电子邮箱格局不正确") private String email; @SafeHtml(message = "非法申请参数") private String content;}
2.进入Controller对应办法,在须要校验的对象前增加@Valid注解即可(校验对动态变量不失效),在应用 @Valid
注解的参数后能够紧跟着一个 BindingResult
类型的参数,用于获取校验后果(将校验后果封装在BingdingResult对象中,不会抛出异样)
留神:@Valid 和 BindingResult 是一一对应的,如果有多个@Valid,那么每个@Valid前面跟着的BindingResult就是这个@Valid的验证后果,程序不能乱
//单个对象校验 @PostMapping("user") //校验参数后边跟BindingResult,spring不会抛出异样,将校验后果封装在这个对象中 public String person(@Valid User user,BindingResult bindingResult){ System.out.println(user); StringBuilder sb = new StringBuilder(); if(bindingResult.hasErrors()){ List<ObjectError> allErrors = bindingResult.getAllErrors(); for(ObjectError error:allErrors){ sb.append(error.getDefaultMessage()+","); } } return sb.toString(); }
3.如果此时去掉实体对象前面的BindingResult,如校验未通过会抛出BindException异样,须要在全局异样处理器中捕捉并对立解决
4.全局异样处理器配置
@RestControllerAdvice@Slfj@AutoConfigurationPackagepublic class GlobalExceptionHandler { //spring-context包外面的异样 //实体对象前不加@RequestBody注解,单个对象内属性校验未通过抛出的异样类型 @ExceptionHandler(BindingException.class) public ResponseEntity<ExceptionResponseVO> methodArguments(BindingException e){ log.warn("throw BindingException,{}",e); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(ExceptionResponseVO.error(NEError.INVALID_PARAMETER, e.getBindingResult().getFieldError().getDefaultMessage())); } //实体对象前不加@RequestBody注解,校验办法参数或办法返回值时,未校验通过时抛出的异样 //Validation-api包外面的异样 @ExceptionHandler(ValidationException.class) public ResponseEntity<ExceptionResponseVO> methodArguments(ValidationException e){ log.warn("throw ValidationException,{}",e); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(ExceptionResponseVO.error(NEError.INVALID_PARAMETER,e.getCause().getMessage())); } //spring-context包外面的异样,实体对象前加@RequestBody注解,抛出的异样为该类异样 //办法参数如果带有@RequestBody注解,那么spring mvc会应用RequestResponseBodyMethodProcessor //对参数进行序列化,并对参数做校验 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ExceptionResponseVO> methodArguments(MethodArgumentNotValidException e){ log.warn("throw MethodArgumentNotValidException,{}",e); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(ExceptionResponseVO.error(NEError.INVALID_PARAMETER, e.getBindingResult().getFieldError().getDefaultMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity methodArguments(Exception e){ log.warn("throw exception,{}",e); return ResponseEntity.badRequest().body(e.getMessage()); }}
b.级联校验
如果一个对象外部蕴含另一个对象作为属性,属性上加 @Valid,能够验证作为属性的对象外部的验证
@Datapublic class User2 { @NotBlank(message = "姓名不能为空") private String name; @Max(value = 50,message = "年龄不能为空") private Integer age; @Valid @NotNull(message = "商品不能为空") private Goods goods;}
@Datapublic class Goods{ @NotBlank(message = "商品名称不能为空") private String goodsName; @NotNull(message = "商品价格不能为空") private Double goodsPrice;}
如果级联校验内元素的属性校验未通过,抛出MethodArgumentNotValidException异样,留神在全局异样处理器捕捉该异样并解决
//级联校验 @PostMapping("cascade")public String cascade(@Valid @RequestBody User2 user2){ return "OK"; }
c.容器元素校验
用来校验实体对象内汇合中的元素,在容器泛型前加注解,可实现对容器单个元素的校验;如下:
@Datapublic class User3 { @NotBlank(message = "姓名不能为空") private String name; @Max(value = 50,message = "年龄不能为空") private Integer age; @Valid @NotEmpty(message = "商品列表不能为空") private List<@NotNull(message = "商品不能为空") Goods> goodsList;}
如果容器元素校验未通过,抛出异样MethodArgumentNotValidException(与级联校验抛出的一样)
//容器元素校验@PostMapping("container")public String container(@Valid @RequestBody User3 user3){ return "OK";}
d.办法的校验
JSR 303规范定义接口ExecutableValidator,用来校验办法参数,Hibernate Validator实现了该接口(ValidatorImpl.class),不仅对Object的属性进行校验,还能够对办法参数、返回值、结构函数参数等进行校验;Spring 在此基础上进行了扩大,增加了MethodValidationPostProcessor拦截器,通过AOP实现对办法的校验;此时抛出的异样是javax.validation.ConstraintViolationException
留神 :必须在Controller下面加上注解@Validated,否则校验规定有效
@RestController@RequestMapping("validator")@Validatedpublic class ValidatorController { @GetMapping("demo1") public String test1(@Range(min = 1,max = 999,message = "起始笔数超过区间范畴")@RequestParam int pageIndex, @Range(min = 1,max = 999,message = "查问笔数超过区间范畴")@RequestParam int pageSize){ return "ok"; }}
除了校验Controller办法外,也可校验Service(必须是单例的bean,否则不失效,因为办法参数校验逻辑底层用AOP来实现)等办法,用法如下:
@Service@Validatedpublic class UserService { //校验办法参数 public String queryUserName(@NotNull(message = "用户参数不能为空") User user){ return user.getName(); } //校验办法返回值 @NotNull(message = "用户信息不存在") public User queryUser(User user){ return null; }}
e.分组校验的实现
分组
同一个校验规定,不可能实用于所有的业务场景,对每一个业务场景去编写一个校验规定,又显得特地冗余。实际上咱们能够用到Hibernate-Validator的分组性能,达到对不同场景做出不同的校验逻辑,缩小DTO对象的创立。
比方一个User对象,新增的时候不须要测验id(系统生成),批改的时候须要测验id属性,要想复用Class,就能够应用Hibernate Validator的分组。
实例代码:
@Datapublic class UserGroup { @NotNull(message = "id不能为空",groups = UpdateUser.class) private Integer id; @NotBlank(message = "姓名不能为空",groups = AddUser.class) private String name; @NotNull(message = "年龄不能为空",groups = AddUser.class) private Integer age; public interface AddUser{} public interface UpdateUser{}}
增加用户:在须要校验的对象后面加@Validated注解(不能应用@Valid注解),并配置分组class,此时AddUser的分组校验规定失效。
//分组校验:增加用户 @PostMapping("addUser") public String addUser(@Validated(UserGroup.AddUser.class) UserGroup userGroup){ return "OK"; }
批改用户:配置UpdateUser分组
//分组校验:批改用户 @PostMapping("updateUser") public String updateUser(@Validated(UserGroup.UpdateUser.class) UserGroup userGroup){ return "OK"; }
应用分组能极大的复用须要验证的Class信息,而不是按业务反复编写冗余的类。
留神:如果指定了校验组,则该属性将不再属于默认的校验组Default.class,则在省略校验组参数的状况下,将不会校验自定义校验组的属性。
组序列
除了按组指定是否验证之外,还能够指定组的验证程序,后面组验证不通过的,前面组不进行验证;其中@GroupSequence提供组序列的模式进行程序式校验,即先校验@Save分组的,如果校验不通过就不进行后续的校验分组了。我认为程序化的校验,场景更多的是在业务解决类,例如联动的属性验证,值的有效性很大水平上不能从代码的枚举或常量类中来校验。
实例代码:
@Datapublic class UserDTO { @NotNull(message = "id不能为空",groups = {UpdateUser.class}) private Integer id; @NotBlank(message = "姓名不能为空",groups = {AddUser.class}) private String name; @NotNull(message = "年龄不能为空",groups = {AddUser.class}) private Integer age; @NotNull(message = "版本不能为空")//不配置goups,默认就是Default分组 private Integer version; @GroupSequence({AddUser.class, UpdateUser.class, Default.class}) public interface AddUpdateGroup{} public interface AddUser{} public interface UpdateUser{}}
首先校验AddUser分组的注解,如果AddUser校验不通过,就不会去校验UpdateUser和Default的分组
@PostMapping("user")public String saveUser(@Validated(UserDTO.AddUpdateGroup.class) UserDTO userDTO){ userMapper.addUser(userDTO); return "ok";}
5.自定义constraint
个别状况,自定义验证能够解决很多问题;某些业务场景下又须要做一些特地的参数校验,此时,咱们能够实现validator的接口,自定义验证器。
创立自定义注解@Sex,该注解是放在字段上的,也能够依据业务场景放在办法或者Class下面)用于判断性别是否合乎束缚
@Target({ ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Constraint(validatedBy = SexConstraintValidator.class)public @interface Sex { String message() default "性别参数有误"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default { };}
创立自定义验证器
public class SexConstraintValidator implements ConstraintValidator<Sex,String> { /** * 性别束缚逻辑 * @param value * @param constraintValidatorContext * @return */ @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { //如果value为null,那么该校验规定不失效;可搭配@NotNull注解应用,更加灵便 if(value == null){ return true; } return "男".equals(value) || "女".equals(value); }}
要验证的DTO对象
@Datapublic class UserDTO { @NotNull(message = "id不能为空") private Integer id; @NotBlank(message = "姓名不能为空") private String name; @NotNull(message = "年龄不能为空") private Integer age; @NotNull(message = "版本不能为空") private Integer version; @Sex private String sex;}
在UserDTO对象前加@Valid注解,可实现对性别字段的合法性校验,sex只能传入“男“或“女”。
这只是一个小例子,大家能够依据业务场景自定义参数校验器,例如敏感词校验、预防sql注入、js脚本攻打等等,都能够用自定义校验器来实现。
关注我理解更多