范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

SpringBoot参数校验,高级特性,非常实用

  之前也写过一篇关于Spring Validation 使用的文章,不过自我感觉还是浮于表面,本次打算彻底搞懂Spring Validation 。
  本文会详细介绍Spring Validation 各种场景下的最佳实践及其实现原理,死磕到底! 简单使用
  Java API 规范 (JSR303 ) 定义了Bean 校验的标准validation-api ,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解如@Email 、@Length 等。Spring Validation 是对hibernate validation 的二次封装,用于支持spring mvc 参数自动校验。接下来,我们以spring-boot 项目为例,介绍Spring Validation 的使用。 引入依赖
  如果spring-boot 版本小于2.3.x ,spring-boot-starter-web 会自动传入hibernate-validator 依赖。如果spring-boot 版本大于2.3.x ,则需要手动引入依赖:        org.hibernate       hibernate-validator       6.0.1.Final   
  对于web 服务来说,为防止非法参数对业务造成影响,在Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式: POST 、PUT 请求,使用requestBody 传递参数; GET 请求,使用requestParam/PathVariable 传递参数。
  下面我们简单介绍下requestBody 和requestParam/PathVariable 的参数校验实战! requestBody参数校验
  POST 、PUT 请求一般会使用requestBody 传递参数,这种情况下,后端使用** DTO 对象进行接收。只要给 DTO 对象加上@Validated 注解就能实现自动参数校验**。比如,有一个保存User 的接口,要求userName 长度是2-10 ,account 和password 字段长度是6-20 。如果校验失败,会抛出MethodArgumentNotValidException 异常,Spring 默认会将其转为400(Bad Request) 请求。
  DTO 表示数据传输对象(Data Transfer Object),用于服务器和客户端之间交互传输使用的。在 spring-web 项目中可以表示用于接收请求参数的Bean 对象。 在 DTO 字段上声明约束注解 @Data   public class UserDTO {          private Long userId;          @NotNull       @Length(min = 2, max = 10)       private String userName;          @NotNull       @Length(min = 6, max = 20)       private String account;          @NotNull       @Length(min = 6, max = 20)       private String password;   }   在方法参数上声明校验注解  @PostMapping("/save")   public Result saveUser(@RequestBody @Validated UserDTO userDTO) {          return Result.ok();   }
  这种情况下,使用@Valid 和@Validated 都可以。 requestParam/PathVariable参数校验
  GET 请求一般会使用requestParam/PathVariable 传参。如果参数比较多 (比如超过 6 个),还是推荐使用DTO 对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,**必须在Controller 类上标注@Validated 注解,并在入参上声明约束注解 (如@Min 等)**。如果校验失败,会抛出ConstraintViolationException 异常。代码示例如下: @RequestMapping("/api/user")   @RestController   @Validated   public class UserController {          @GetMapping("{userId}")       public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {              UserDTO userDTO = new UserDTO();           userDTO.setUserId(userId);           userDTO.setAccount("11111111111111111");           userDTO.setUserName("xixi");           userDTO.setAccount("11111111111111111");           return Result.ok(userDTO);       }          @GetMapping("getByAccount")       public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {              UserDTO userDTO = new UserDTO();           userDTO.setUserId(10000000000000003L);           userDTO.setAccount(account);           userDTO.setUserName("xixi");           userDTO.setAccount("11111111111111111");           return Result.ok(userDTO);       }   }   统一异常处理
  前面说过,如果校验失败,会抛出MethodArgumentNotValidException 或者ConstraintViolationException 异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。比如我们系统要求无论发送什么异常,http 的状态码必须返回200 ,由业务码去区分系统的异常情况。 @RestControllerAdvice   public class CommonExceptionHandler {          @ExceptionHandler({MethodArgumentNotValidException.class})       @ResponseStatus(HttpStatus.OK)       @ResponseBody       public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {           BindingResult bindingResult = ex.getBindingResult();           StringBuilder sb = new StringBuilder("校验失败:");           for (FieldError fieldError : bindingResult.getFieldErrors()) {               sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");           }           String msg = sb.toString();          return Result.fail(BusinessCode.参数校验失败, msg);       }          @ExceptionHandler({ConstraintViolationException.class})       @ResponseStatus(HttpStatus.OK)       @ResponseBody       public Result handleConstraintViolationException(ConstraintViolationException ex) {           return Result.fail(BusinessCode.参数校验失败, ex.getMessage());       }   }   进阶使用分组校验
  在实际项目中,可能多个方法需要使用同一个DTO 类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在DTO 类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。还是上面的例子,比如保存User 的时候,UserId 是可空的,但是更新User 的时候,UserId 的值必须>=10000000000000000L ;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下: 约束注解上声明适用的分组信息 groups  @Data   public class UserDTO {          @Min(value = 10000000000000000L, groups = Update.class)       private Long userId;          @NotNull(groups = {Save.class, Update.class})       @Length(min = 2, max = 10, groups = {Save.class, Update.class})       private String userName;          @NotNull(groups = {Save.class, Update.class})       @Length(min = 6, max = 20, groups = {Save.class, Update.class})       private String account;          @NotNull(groups = {Save.class, Update.class})       @Length(min = 6, max = 20, groups = {Save.class, Update.class})       private String password;          public interface Save {       }          public interface Update {       }   }   @Validated 注解上指定校验分组 @PostMapping("/save")   public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {          return Result.ok();   }      @PostMapping("/update")   public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {          return Result.ok();   }   嵌套校验
  前面的示例中,DTO 类里面的字段都是基本数据类型 和String 类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验 。
  比如,上面保存User 信息的时候同时还带有Job 信息。需要注意的是,此时DTO 类的对应字段必须标记@Valid 注解。 @Data   public class UserDTO {          @Min(value = 10000000000000000L, groups = Update.class)       private Long userId;          @NotNull(groups = {Save.class, Update.class})       @Length(min = 2, max = 10, groups = {Save.class, Update.class})       private String userName;          @NotNull(groups = {Save.class, Update.class})       @Length(min = 6, max = 20, groups = {Save.class, Update.class})       private String account;          @NotNull(groups = {Save.class, Update.class})       @Length(min = 6, max = 20, groups = {Save.class, Update.class})       private String password;          @NotNull(groups = {Save.class, Update.class})       @Valid       private Job job;          @Data       public static class Job {              @Min(value = 1, groups = Update.class)           private Long jobId;              @NotNull(groups = {Save.class, Update.class})           @Length(min = 2, max = 10, groups = {Save.class, Update.class})           private String jobName;              @NotNull(groups = {Save.class, Update.class})           @Length(min = 2, max = 10, groups = {Save.class, Update.class})           private String position;       }          public interface Save {       }          public interface Update {       }   }
  嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验 会对集合里面的每一项都进行校验,例如List 字段会对这个list 里面的每一个Job 对象都进行校验。 集合校验
  如果请求体直接传递了json 数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection 下的list 或者set 来接收数据,参数校验并不会生效!我们可以使用自定义list 集合来接收参数: 包装 List 类型,并声明@Valid 注解 public class ValidationList implements List {          @Delegate       @Valid       public List list = new ArrayList<>();          @Override       public String toString() {           return list.toString();       }   }
  @Delegate 注解受lombok 版本限制,1.18.6 以上版本可支持。如果校验不通过,会抛出NotReadablePropertyException ,同样可以使用统一异常进行处理。
  比如,我们需要一次性保存多个User 对象,Controller 层的方法可以这么写: @PostMapping("/saveList")   public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList userList) {          return Result.ok();   }   自定义校验
  业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。自定义spring validation 非常简单,假设我们自定义加密id (由数字或者a-f 的字母组成,32-256 长度)校验,主要分为两步: 自定义约束注解  @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})   @Retention(RUNTIME)   @Documented   @Constraint(validatedBy = {EncryptIdValidator.class})   public @interface EncryptId {          String message() default "加密id格式错误";          Class<?>[] groups() default {};          Class<? extends Payload>[] payload() default {};   }   实现 ConstraintValidator 接口编写约束校验器 public class EncryptIdValidator implements ConstraintValidator {          private static final Pattern PATTERN = Pattern.compile("^[a-fd]{32,256}#34;);          @Override       public boolean isValid(String value, ConstraintValidatorContext context) {              if (value != null) {               Matcher matcher = PATTERN.matcher(value);               return matcher.find();           }           return true;       }   }
  这样我们就可以使用@EncryptId 进行参数校验了! 编程式校验
  上面的示例都是基于注解 来实现自动校验的,在某些情况下,我们可能希望以编程方式 调用验证。这个时候可以注入javax.validation.Validator 对象,然后再调用其api 。 @Autowired   private javax.validation.Validator globalValidator;      @PostMapping("/saveWithCodingValidate")   public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {       Set> validate = globalValidator.validate(userDTO, UserDTO.Save.class);          if (validate.isEmpty()) {          } else {           for (ConstraintViolation userDTOConstraintViolation : validate) {                  System.out.println(userDTOConstraintViolation);           }       }       return Result.ok();   }   快速失败 (Fail Fast)
  Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast 模式,一旦校验失败就立即返回。 @Bean   public Validator validator() {       ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)               .configure()                  .failFast(true)               .buildValidatorFactory();       return validatorFactory.getValidator();   }   @Valid和@Validated区别
  实现原理requestBody参数校验实现原理
  在spring-mvc 中,RequestResponseBodyMethodProcessor 是用于解析@RequestBody 标注的参数以及处理@ResponseBody 标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法resolveArgument() 中: public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {       @Override       public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,                                     NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {              parameter = parameter.nestedIfOptional();              Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());           String name = Conventions.getVariableNameForParameter(parameter);              if (binderFactory != null) {               WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);               if (arg != null) {                      validateIfApplicable(binder, parameter);                   if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {                       throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());                   }               }               if (mavContainer != null) {                   mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());               }           }           return adaptArgumentIfNecessary(arg, parameter);       }   }
  可以看到,resolveArgument() 调用了validateIfApplicable() 进行参数校验。 protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {          Annotation[] annotations = parameter.getParameterAnnotations();       for (Annotation ann : annotations) {              Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);              if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {               Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));               Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});                  binder.validate(validationHints);               break;           }       }   }
  看到这里,大家应该能明白为什么这种场景下@Validated 、@Valid 两个注解可以混用。我们接下来继续看WebDataBinder.validate() 实现。 @Override   public void validate(Object target, Errors errors, Object... validationHints) {       if (this.targetValidator != null) {           processConstraintViolations(                  this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);       }   }
  最终发现底层最终还是调用了Hibernate Validator 进行真正的校验处理。 方法级别的参数校验实现原理
  上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解 的校验方式,就是方法级别的参数校验。实际上,这种方式可用于任何Spring Bean 的方法上,比如Controller /Service 等。其底层实现原理就是AOP ,具体来说是通过MethodValidationPostProcessor 动态注册AOP 切面,然后使用MethodValidationInterceptor 对切点方法织入增强。 public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {       @Override       public void afterPropertiesSet() {              Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);              this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));       }          protected Advice createMethodValidationAdvice(@Nullable Validator validator) {           return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());       }   }
  接着看一下MethodValidationInterceptor : public class MethodValidationInterceptor implements MethodInterceptor {       @Override       public Object invoke(MethodInvocation invocation) throws Throwable {              if (isFactoryBeanMetadataMethod(invocation.getMethod())) {               return invocation.proceed();           }              Class<?>[] groups = determineValidationGroups(invocation);           ExecutableValidator execVal = this.validator.forExecutables();           Method methodToValidate = invocation.getMethod();           Set> result;           try {                  result = execVal.validateParameters(                   invocation.getThis(), methodToValidate, invocation.getArguments(), groups);           }           catch (IllegalArgumentException ex) {               ...           }              if (!result.isEmpty()) {               throw new ConstraintViolationException(result);           }              Object returnValue = invocation.proceed();              result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);              if (!result.isEmpty()) {               throw new ConstraintViolationException(result);           }           return returnValue;       }   }
  实际上,不管是requestBody参数校验 还是方法级别的校验 ,最终都是调用Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。
  本人花费2个月时间,整理了一套JAVA开发技术资料,内容涵盖java基础,分布式、微服务等主流技术资料,包含大厂面经,学习笔记、源码讲义、项目实战、讲解视频。
  java面试资料
  希望可以帮助一些想通过自学提升能力的朋友,领取资料,扫码关注一下
  记得转发+关注+私信
  私信回复【2022面试资料】
  领取更多学习资料

新能源汽车成长的必然代价?每卖出14辆就有1辆被召回新能源汽车的普及改变的不仅仅是汽车产业的结构,以及人们的用车习惯,汽车技术和结构的改变,也使得在车辆可靠性以及安全性上,有了不同的风险界定。从汽车本身而言,燃油车相对少出现发动机变世界电信日临近,看5G如何引领大众移动通信生活跃迁5月17日是世界电信日,最近5G作为时下热词,行业聚焦,社会关注。5G在实际应用中为我们的移动互联网生活带来全新的便捷体验。越来越多的用户由4G转为5G,成功搭上5G极智生活的快车什么是新能源车?我想应该有很多人跟我一样,对新能源的认知最基础的就是看车牌颜色,当然这并没有错,还非常对,但是为啥有的绿牌是F有的是D呢?这个我想还是有很大一部分人是不清楚的,既然要说新能源车,那海尔和TCL冰箱我该买哪个?其实现在的冰箱TCL的质量比海尔的要好,现在海尔的质量远不如以前了海尔和TCL冰箱,都是家电知名品牌。关键是看你地域和关心点。一地域如果你是广东广西福建云南贵州等地的消费者,可以考微信要开始变了?取缔广告,还原纯净生态?微信要开始变了?取缔广告,还原纯净生态?腾讯公司作为现有的最大社交公司,拥有着两大神器,一个是让腾讯起家的QQ,一个是现在国民级的微信,QQ用来发展游戏,微信用来发展社交,也正是基applewatch值得购买吗?我觉得AppleWatch不值得购买,原因有三1外观设计太雷!AppleWatch从第一代开始就是方形的表盘,看起来就好像小孩子的卡通电子手表。这个问题一直都有用户在吐槽,原本一位天问一号传递回来的资料有多珍贵?那是人类首次登陆火星获得的资料,如果这些资料跟美国人的资料相互矛盾的话天问一号是我国第1个成功着陆火星的探测器,代表了我国深空探测的最高科技水平,其传递回来的资料弥足珍贵。可以从三现在滴滴顺风车是如何规定的,还属于非法营运吗?老百姓需要顺风车,顺风车符合民意,不要因为一两件事就全盘否定顺风车,这是极不公平的。现在的出租车可以说更不规范,请问以前全国出租车出事的还少吗?只是出租车出了事大家认为习以为常,我什么品牌的空调值得买?随着时代的发展变化,生活质量的提高,空调已经成为日常必备的家电产品之一,但在空调选购上,确实是很多人头疼的问题,怕质量不好售后不行等,之前我家里装空调时也遇到类似的问题,后面经由朋选iQOONeo5,还是红米K40?实测对比告诉你答案如今的手机市场,有一个很有意思的现象,产能并不会因为消费者需求强烈而有所改善,反而是竞品的出现,一定程度上能改善产能。像红米K40和iQOONeo5就是这样的一个例子。在iQOON华为鸿蒙再传好消息大家都知道华为鸿蒙系统大家都知道美帝长臂管辖制裁华为可是你们知道华为鸿蒙系统有多强大么?华为鸿蒙系统投入使用提振了中国人的士气,更是第三世界国家的福音,也是全世界的福音。为什么这么
工信部5G牌照很快发放今年流量费要降20以上2019年可以说是5G网络的元年。消费者手机厂商以及运营商都密切关注着我国5G网络建设的进度。今天,工信部部长苗圩在接受采访时透露了5G网络的最新动向以及工信部2019年的目标。在大众电动汽车模块化平台迎来首个外部合作伙伴环球网综合报道据英国路透社3月5日消息,大众集团近日与德国初创公司e。GOMobile签约,这是大众电动汽车模块化平台(MEB)迎来的首个外部合作伙伴,此次合作旨在简化多样化电动车去年我国研发支出近2万亿元,研发人员总量世界第一据新华社消息,国家统计局发布2018年国民经济和社会发展统计公报显示,2018年我国研究与试验发展(RampD)经费支出为19657亿元,比上年增长11。6,其中基础研究经费111Flyme7体验版3月5日更新你们更新了吗?今天,魅族Flyme体验版迎来了更新,主要是优化了16系列暗屏指纹解锁速度,解锁更顺畅,并且系统更新首页视觉改版,新增公告栏。与此同时,魅族16X首个体验版正式发布,可以通过报名获透过两会看通信业变局提速降费从个人到企业携号转网将提前一年实现每经记者刘春山每经编辑陈俊杰3月5日,国务院总理李克强在十三届全国人大二次会议上作政府工作报告。报告提出,今年中小企业宽带平均资费再降低15,移动网络流量平均资费再降低20以上,在机械物联监控管理云平台华筑科技完成新一轮融资,红杉资本中国投资猎云网北京3月5日报道猎云网今日获悉,机械物联监控管理云平台华筑科技宣布完成新一轮融资,红杉资本中国投资,具体金额暂未透露。2017年8月,华筑科技完成A轮融资,投资方为英诺天使基Lyft内忧外患资金短缺或难逃困境作者唐云来源电商报2019030510183月5日消息,lyft近日正式公布招股书,正式申请在纳斯达克上市,并计划在3月中旬进行持续两周的路演。Lyft近日正式向美国证券交易委员会量子计算机是人类的下一大步,但我们能够驾驭这个巨大的力量吗?计算机可以说是人类最伟大的发明之一,而计算机芯片(CPU)是最基本的组成部分之一,其中每个模块都有特定的功能,每个模块由晶体管构成,晶体管是0位或1位的开或关,一堆晶体管组成逻辑门华为或推MediaPadM5Pro后续平板配970处理器环球网科技综合报道据美国科技新闻网站AndroidHeadlines3月4日报道,在华为MediaPadM5Pro平板电脑推出一年后,XDA开发人员在今年年初华为发布的麒麟970内2018年三大运营商经营数据分析联通实现4G业务的大翻身中商情报网讯2018年工信部提出提速降费混改等多项任务,三大运营商均已超额完成,在5G网络建设我国也是在世界的前列。在过去的2018年,对于中国移动来说无疑是载入史册的一年,固网宽IBM量子计算完成里程碑式突破2020年可能实现量子优势IBM正取得量子计算的又一个里程碑进展创下了量子计量(QuantumVolume)的新高,IBM预计,按这种速度发展,用上所谓的量子优势(QuantumAdvantage)只需十年