spring mvc validation

作者: Alex的路 | 来源:发表于2018-12-23 15:40 被阅读17次

    是服务就需要对外提供接口,否则该服务就没有任何意义。接口需要指定具体的入参情况,以保证服务能够正常地运行。spring mvc通过controller中的method对外提供接口服务,本文就如何在spring mvc中对RequestParamPathVariableRequestBody三种类型的参数做参数校验做简单介绍。

    为了更好地介绍上诉三种类型参数校验的方式,本文将通过一个简单的接口需求来完成相应的接口校验,相关的代码在spring-demo 项目上。

    1 需求

    现在有一个学生管理系统(假设所有学生的姓名都是唯一的),我们需要对学生信息进行管理,即实现最常见的CURD操作。学生类(Student)如下所示:

    @JsonIgnoreProperties(ignoreUnknown = true)
    public class Student {
        private String studentName;
        private int age;
        private int gender;
        private LocalDate birthDay;
        
        // ... getter setter toString
    }
    
    

    现在需要提供CURD四个接口,接口的具体要求如下:

    1. 添加:采用POST的方式请求,姓名不能为空,年龄在1~200之间,性别用0和1表示,出生日期不能为空且只能是过去的时间。
    2. 删除:根据姓名删除学生,姓名不能为空
    3. 修改:修改指定学生的出生日期
    4. 查询:查询所有出生日期在指定范围内的学生

    默认情况下请求参数无法直接映射成LocalDate类型的,需要在spring中配置jacksonobjectmapper添加JavaTimeModule

    2 依赖项

    2.1 JSR 380

    JSR制定了许多Java开发的规范,其中JSR 380就制定Bean Validation的相关规范,可以在pom.xml中加入依赖引入相关API。

    <!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>2.0.1.Final</version>
    </dependency>
    

    JSR 380只是规范,并没有具体实现检验的方法,如果直接使用validation-api进行校验,会抛出javax.validation.NoProviderFoundException,提示需要提供实现JSR 380的校验器

    2.2 Hibernate Validator

    Hibernate ValidatorJSR 380规范的具体实现,并且除了JSR 380中的校验器,它还提供了更多的自定义的校验器。
    pom.xml中加入如下依赖引入Hibernate Validator

    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.0.13.Final</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>javax.el</artifactId>
        <version>3.0.1-b09</version>
    </dependency>
    

    实际上只需要添加2.2的依赖即可,2.1的依赖可以不用添加,因为2.2中已经包含了validation-api中的内容。

    3 应用Hibernate Validator

    Hibernate Validator实现了JSR 380的规范,提供了诸如@NotNull等的校验器,本文这里不具体介绍Hibernate Validator都提供了哪些校验器,感兴趣的话可以去Hibernate Validator官网查看相关的文档。

    3.1 添加学生信息

    1. 根据1中所述的需求,现在对StudentModel的字段添加校验规则,如下:

      public class StudentModel {
          @NotBlank(message = "studentName不能为空")
          private String studentName;
      
          @Min(value = 1, message = "参数age不能小于1")
          @Max(value = 200, message = "参数age不能大于200")
          @Range(min = 1, max = 200, message = "age只能在1到200之间")
          private int age;
          
          @Range(min = 0, max = 1, message = "gender只能取0或者1")
          private int gender;
      
          @Past(message = "birthDay只能是过去的时间")
          private LocalDate birthDay; 
      }
      
    2. 在方法的参数中对StudentModel通过@Validation进行校验

      @PostMapping(value = "/add", consumes =      MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
      public Map<String, Object> add(@Valid @RequestBody StudentModel studentModel) {
        return OutPut.success(HttpStatusWrapper.OK,"成功", studentModel);
      }
      

      @ValidHibernate Validator中用来校验对象合法性的注解。
      请求运行并且请求add接口的时候,当post body中的数据不符合设置的校 验规则是,系统并没有返回对应的错误信息,而是输出下面的信息:

      Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.util.Map<java.lang.String, java.lang.Object> com.lianglei.spring.demo.controller.StudentController.add(com.lianglei.spring.demo.model.StudentModel): [Field error in object 'studentModel' on field 'gender': rejected value [2]; codes [Range.studentModel.gender,Range.gender,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [studentModel.gender,gender]; arguments []; default message [gender],1,0]; default message [gender只能取0或者1]] ]
      

      从上面的异常信息中可以看出,gender要求只能是0或者1,但是输入的2,被Hibernate Validatorrejected了。从中我们可以发现,校验不通过的时候,会抛出org.springframework.web.bind.MethodArgumentNotValidException异常。因此我们可以通过统一异常捕获的方式处理校验不通过的情况,给出友好的接口返回。

    3. 捕获MethodArgumentNotValidException

      @RestControllerAdvice
      public class GlobalExceptionHandler {
          private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
          
          /**
           * 接口参数校验异常
           * @param e
           * @return
           */
          @ExceptionHandler(value = {MethodArgumentNotValidException.class})
          public Map<String, Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
              LOGGER.error(e.getMessage(), e);
              final String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
              return OutPut.failure(HttpStatusWrapper.ILLEGAL_REQUEST_PARAMETERS, message);
          }
      }
      

      添加异常捕获后的输出如下:

      {
          "status": "Illegal request parameters",
          "code": 460,
          "msg": "gender只能取0或者1"
      }
      

      如果想在GlobalExceptionHandler中处理MethodArgumentNotValidException.class异常的话,需要注意GlobalExceptionHandler不能继承ResponseEntityExceptionHandler否则会发生冲突。

      org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'handlerExceptionResolver' defined in org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerExceptionResolver]: Factory method 'handlerExceptionResolver' threw exception; nested exception is java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [class org.springframework.web.bind.MethodArgumentNotValidException]: {public java.util.Map com.lianglei.spring.demo.exception.GlobalExceptionHandler.handleMethodArgumentNotValidException(org.springframework.web.bind.MethodArgumentNotValidException), public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}ß 
      
    4. request body中参数名与StudentModel 不一致的情况
      在实际开发过程中,经常会出现入参的名字与请求类中的名字不一致的情况,比如说请求是student_name,而类中字段名为studentName

      因为是POST请求,采用的是JSON的方式,所以只需要在studentName上通过@JsonProperty注解一下即可,如下:

      @JsonProperty("student_name")
      @NotBlank(message = "student_name不能为空")
      private String studentName;
      

    3.2 删除学生信息

    采用DELETE删除指定姓名的学生,需要判断姓名不能为空,这里采用@RequestParam获取student_name参数。

    1. 直接通过@NotBlank校验

      @DeleteMapping(value = "/delete", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
      public Map<String, Object> delete(@NotBlank(message = "student_name不可以为空") @RequestParam(name = "student_name") String name) {
          return OutPut.success(HttpStatusWrapper.OK,"成功", name);
      }
      

      这里通过@NotBlank要求student_name不可以为空(null或者trim之后的""),运行后程序正常运行,但是@NotBlank并没有生效--student_name即使填了空白也没有报错

      这是因为不像@Valid可以直接作用在@RequestBody参数上,@NotBlank并不会直接在@RequestParam参数上生效。

    2. Controller上添加@Validated配合@NotBlank校验

      参考Validating RequestParams and PathVariables in Spring MVC这篇文章,了解到@RequestParam上的validation需要在类上标注@Validated注解(即在StudentController上注解)

      然而添加改注解运行后,@NotBlank仍然没有生效。原因是没有为@RequestParam配置注解器。

    3. spring mvc配置校验器

      @Configuration
      @EnableWebMvc
      @EnableAspectJAutoProxy
      @EnableScheduling
      @ComponentScan(basePackages = "com.lianglei.spring.demo")
      public class ApplicationConfig implements WebMvcConfigurer {
          @Bean
          public Validator validator() {
              ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                      .configure()
                      .failFast(true)
                      .buildValidatorFactory();
      
              return validatorFactory.getValidator();
          }
      
          @Bean
          public MethodValidationPostProcessor methodValidationPostProcessor() {
              MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
              methodValidationPostProcessor.setValidator(validator());
              return methodValidationPostProcessor;
          }
      
      }
      

      failFast的意思只要出现校验失败的情况,就立即结束校验,不再进行后续的校验。

      如何在spring中配置bean,若有疑问请参看Spring中的Bean配置方式一文

      配置成功后,再次运行服务进行delete请求,系统抛出如下异常:

      [ERROR] 2018-12-23 09:51:10,243 method:com.lianglei.spring.demo.exception.GlobalExceptionHandler.handleException(GlobalExceptionHandler.java:34)
      delete.arg0: name不可以为空
      javax.validation.ConstraintViolationException: delete.arg0: name不可以为空
          at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116)
          at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
          at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
          at com.lianglei.spring.demo.controller.StudentController$$EnhancerBySpringCGLIB$$2cfc55af.delete(<generated>)
          at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
          at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
          at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
          at java.lang.reflect.Method.invoke(Method.java:498)
          at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:215)
          at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:142)
          at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
          at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
          at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
          at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
          at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
          at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
          at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:998)
          at org.springframework.web.servlet.FrameworkServlet.doDelete(FrameworkServlet.java:923)
          ...
      

      从这里可以看出,@RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException而不是@RequestBody中的MethodArgumentNotValidException异常。

    4. 捕获ConstraintViolationException异常

      @ExceptionHandler(value = {ConstraintViolationException.class})
      public Map<String, Object> handleConstraintViolationException(ConstraintViolationException e) {
          LOGGER.error(e.getMessage(), e);
          final String message = e.getConstraintViolations().stream()
                  .map(ConstraintViolation::getMessage)
                  .collect(Collectors.joining());
          return OutPut.failure(HttpStatusWrapper.ILLEGAL_REQUEST_PARAMETERS, message);
      }
      

      自此,@RequestParam就能够自动生效了。

      {
          "status": "Illegal request parameters",
          "code": 460,
          "msg": "student_name不可以为空"
      }
      

    3.3 修改学生信息

    修改学生信息一般通过@RequestBody将参数传递给StudentModel,这时候校验方式同添加学生信息中所述。但是,如果需要通过StudentModel mapping 原先的@RequestParam参数,又该如何呢?

    1. 请求中直接通过@Valid校验StudentModel

      @PutMapping(value = "/update", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
      public Map<String, Object> update(@Valid StudentModel studentModel) {
          return OutPut.success(HttpStatusWrapper.OK,"成功", studentModel);
      }
      

      执行请求:

      localhost:8080/student/update?student_name=wangwu&age=1&gender=0&birth_day=1994-06-15
      

      异常如下:

      Field error in object 'studentModel' on field 'studentName': rejected value [null]; codes [NotBlank.studentModel.studentName,NotBlank.studentName,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [studentModel.studentName,studentName]; arguments []; default message [studentName]]; default message [studentName不能为空]
          at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:164)
          at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124)
          at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:165)
          at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
          at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
          at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
          at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
          at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
          at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
          at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
          at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:998)
          at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:912)
          at javax.servlet.http.HttpServlet.service(HttpServlet.java:710)
          at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:875)
          at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
          at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:848)
      

      可见student_name并没有映射到studentName上,导致studentNamenull。这也是必然的,因为@JsonProperty是用来处理json字符串转对象的,而请求中并没有json格式的学生信息。当把请求中的student_name改回studentName后,studentName就会被正确赋值。
      那么,如何才能解决非POST json形式下,请求参数和对象的属性名不一致的情况呢?

      1. 请求及接口修改成PSOT json的形式
      2. 参考 How to customize parameter names when binding spring mvc command objects中的讨论或者绑定SpringMvc GET请求对象时自定义参数名总结的方法。
      3. 拆分对象字段到接口参数中,通过@RequestParam结合Hibernate Validator完成验证

    3.4 查询学生信息

    通过GET方式查询学生信息,student_name@PathVariable的方式进行赋值。

    @GetMapping(value = "/get/{student_name}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Map<String, Object> get(@NotBlank(message = "student_name不可以为空") @Size(min = 3, message = "student_name长度不能小于3") @PathVariable(name = "student_name") String name) {
        return OutPut.success(HttpStatusWrapper.OK,"成功", name);
    }
    
    1. 正常请求

      localhost:8080/student/get/wangwu
      

      返回

      {
          "status": "OK",
          "code": 200,
          "msg": "成功",
          "data": "wangwu"
      }
      
    2. 异常请求

      localhost:8080/student/get/yy
      

      返回

      {
          "status": "Illegal request parameters",
          "code": 460,
          "msg": "student_name长度不能小于3"
      }
      

      抛出的与@RequestParam方式一样的ConstraintViolationException异常。

    4 总结

    通过上面的示例演示,对于spring mvc中的参数校验,可以得出如下结论:

    1. 如果接口参数对应的是请求中请求体部分(@RequestBody),且请求体的格式为json,可以将请求参数封装到一个类中,在类中通过@NotNull等标注设置校验规则,在接口中通过@Valid表明需要进行校验,校验失败后会抛出MethodArgumentNotValidException异常。如果请求中参数的名称和接口参数中字段的名称不一致,可以通过@JsonProperty标注进行重命名。
    2. 如果接口参数中对应的是请求参数(@RequestParam)或者请求路径中的变量(@PathVariable),则可以通过对应的@RequestParam或者@PathVariable结合@NotNull进行参数检验,注意这里需要在Controller上添加@Validated注解,并且需要给spring配置MethodValidationPostProcessor才能工作。如果校验失败,会抛出ConstraintViolationException异常。如果需要将这些参数封装到一个类中,那么请求中的参数名必须和类中的字段一致,否则会匹配不上。当然,可以通过额外的配置满足这个需求,但是比较麻烦而且不支持继承的类。

    相关文章

      网友评论

        本文标题:spring mvc validation

        本文链接:https://www.haomeiwen.com/subject/gjjnkqtx.html