【问题标题】:Spring MVC - @Valid on list of beans in REST serviceSpring MVC - @Valid 在 REST 服务中的 bean 列表中
【发布时间】:2013-06-20 07:32:02
【问题描述】:

在 Spring MVC REST 服务 (json) 中,我有一个像这样的控制器方法:

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public List<...> myMethod(@Valid @RequestBody List<MyBean> request, BindingResult bindingResult) {

MyBean 类有 bean 验证注解。

在这种情况下似乎没有进行验证,尽管它适用于其他控制器。

我不想将列表封装在 dto 中,这会改变 json 输入。

为什么没有对 bean 列表的验证?有哪些替代方案?


【问题讨论】:

标签: java spring rest spring-mvc bean-validation


【解决方案1】:

@Valid 是 JSR-303 注释,JSR-303 适用于 JavaBeans 上的验证。 java.util.List 不是 JavaBean(根据 JavaBean 的 official description),因此它不能直接使用符合 JSR-303 的验证器进行验证。这得到了两个观察结果的支持。

JSR-303 Specification3.1.3 部分说:

除了支持实例验证外,还支持对象图的验证。图验证的结果作为一组统一的约束违规返回。考虑 bean X 包含 Y 类型的字段 的情况。通过使用@Valid 注释对字段 Y 进行注释,验证器将在 X 被验证时验证 Y(及其属性)。声明为 Y 类型(子类,实现)的字段中包含的值的确切类型 Z 在运行时确定。使用 Z 的约束定义。这确保标记为@Valid 的关联具有正确的多态行为。

集合值、数组值和通常可迭代字段和属性也可以使用@Valid 注释进行修饰。这会导致迭代器的内容被验证。支持任何实现 java.lang.Iterable 的对象。

我已用粗体标出了重要的信息。本节暗示为了验证集合类型,必须将其封装在 bean 中(由 Consider the situation where bean X contains a field of type Y 暗示);此外,不能直接验证集合(由Collection-valued, array-valued and generally Iterable fields and properties may also be decorated 暗示,重点是字段和属性)。

实际的 JSR-303 实现

我有a sample application 使用 Hibernate Validator 和 Apache Beans Validator 测试集合验证。如果您以mvn clean test -Phibernate(使用Hibernate Validator)和mvn clean test -Papache(用于Beans Validator)对这个样本运行测试,两者都拒绝直接验证集合,这似乎符合规范。由于 Hibernate Validator 是 JSR-303 的参考实现,因此该示例进一步证明了集合需要封装在 bean 中才能进行验证。


清除后,我会说尝试以问题中显示的方式直接将集合传递给控制器​​方法也存在设计问题。即使验证直接在集合上工作,控制器方法也无法使用不直接映射到集合的替代数据表示,例如自定义 XML、SOAP、ATOM、EDI、Google 协议缓冲区等。为了支持这些表示,控制器必须接受并返回对象实例。这将需要以任何方式将集合封装在对象实例中。因此,强烈建议将 List 包装在另一个对象中,正如其他答案所建议的那样。

【讨论】:

    【解决方案2】:

    我能找到的唯一方法是包装列表,这也意味着 JSON 输入必须更改

    @RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
    @ResponseBody
    public List<...> myMethod(@Valid @RequestBody List<MyBean> request, BindingResult bindingResult) {
    

    变成:

    @RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
    @ResponseBody
    public List<...> myMethod(@Valid @RequestBody MyBeanList request, BindingResult bindingResult) {
    

    我们还需要:

    import javax.validation.Valid;
    import java.util.List;
    
    public class MyBeanList {
    
        @Valid
        List<MyBean> list;
    
        //getters and setters....
    }
    

    这看起来也可以使用列表的自定义验证器,但我还没有做到这一点。

    @Valid 注解是标准 JSR-303 Bean Validation API 的一部分,不是 Spring 特定的构造。 只要配置了适当的 Validator,Spring MVC 就会在绑定后验证 @Valid 对象。

    参考:http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html

    【讨论】:

      【解决方案3】:

      尝试直接验证。像这样的:

      @Autowired
      Validator validator;
      
      @RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
      @ResponseBody
      public Object myMethod(@RequestBody List<Object> request, BindingResult bindingResult) {
          for (int i = 0; i < request.size(); i++) {
              Object o = request.get(i);
              BeanPropertyBindingResult errors = new BeanPropertyBindingResult(o, String.format("o[%d]", i));
              validator.validate(o, errors);
              if (errors.hasErrors())
                  bindingResult.addAllErrors(errors);
          }
          if (bindingResult.hasErrors())
              ...
      

      【讨论】:

      • 是的,但是注释可以避免很多行。
      【解决方案4】:

      有一种优雅的方式可以将您的请求封装在自定义的 java.util.List 中,它同时充当 ListJavaBeansee here

      【讨论】:

        【解决方案5】:

        使用 com.google.common.collect.ForwardingList

        public class ValidList<T> extends ForwardingList<T> {
        
          private List<@Valid T> list;
        
          public ValidList() {
            this(new ArrayList<>());
          }
        
          public ValidList(List<@Valid T> list) {
            this.list = list;
          }
        
          @Override
          protected List<T> delegate() {
            return list;
          }
        
          /** Exposed for the {@link javax.validation.Validator} to access the list path */
          public List<T> getList() {
            return list;
          }
        }
        

        所以不需要包装器

        你可以使用

        @RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
        @ResponseBody
        public List<...> myMethod(@Valid @RequestBody ValidList<MyBean> request, BindingResult bindingResult) {
        

        通过使用包装器,您的 JSON 需要更改为

        {
          "list": []
        }
        

        通过此实现,您可以使用原始 JSON

        []
        

        【讨论】:

          【解决方案6】:

          给定 Spring-Boot + Jackson 用于 JSON 序列化 + org.springframework.boot:spring-boot-starter-validation(必须手动包含 Spring Boot >= 2.3.0)

          使用内置插件

          • @Validated 添加到您的控制器
          • 在你的控制器方法签名中使用@Valid @NotNull @RequestBody List&lt;@Valid Pojo&gt; pojoList

          这将在无效的 bean 上抛出 javax.validation.ConstraintViolationException 错误,默认情况下映射到 500 Internal Error。因此,请确保您也有一个ControllerAdvice

          使用包装器

          列表包装器很好(即,具有List&lt;E&gt; 类型的单个字段的类),但是从上面的响应中,您还必须更改 JSON({"list": []}[]),这不好看……

          解决方案:

          • 在包装器中,在包装后的列表字段上使用@JsonValue注解
          • 添加一个以列表为参数的构造函数,并用@JsonCreator注释它
          • 在你的控制器方法中,使用@Valid @RequestBody ListWrapper&lt;Pojo&gt; tokenBodies

          这很有效,很优雅,并且不需要更多。此外,它会在无效的 bean 上抛出通常的org.springframework.web.bind.MethodArgumentNotValidException


          包装器示例 (java)

          (有关 Kotlin 中的完整示例,请参阅 https://stackoverflow.com/a/64060909

          public class ValidList<E> {
              @JsonValue
              @Valid
              @NotNull
              @Size(min = 1, message = "array body must contain at least one item.")
              private List<E> values;
          
              @JsonCreator
              public ValidList(E... items) {
                  this.values = Arrays.asList(items);
              }
          
              public List<E> getValues() {
                  return values;
              }
          
              public void setValues(List<E> values) {
                  this.values = values;
              }
          }
          
          public class SomePojo {
              @Min(value = 1)
              int id;
          
              @Size(min = 2, max = 32)
              String token;
          
              // getters and setters
          }
          
          @RestController
          public class SomeController {
          
              @PostMapping("/pojos")
              public ValidList<SomePojo> test(@Valid @RequestBody ValidList<SomePojo> pojos) {
                  return pojos;
              }
          }
          

          提交确定:

          curl -H "Content-Type: application/json" -X POST http://localhost:8080/pojos -d '[{"id": 11, "token": "something"}]'
          
          [{"token" : "something", "id" : 11}]
          

          提交空正文:

          curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[]'
          
          {
             "timestamp" : "2020-09-25T09:55:05.462+00:00",
             "error" : "Bad Request",
             "message" : "Validation failed for object='validList'. Error count: 1",
             "exception" : "org.springframework.web.bind.MethodArgumentNotValidException",
             "path" : "/pojos",
             "status" : 400,
             "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo.ValidList<com.example.demo.SomePojo> com.example.demo.SomeController.test(com.example.demo.ValidList<com.example.demo.SomePojo>): [Field error in object 'validList' on field 'values': rejected value [[]]; codes [Size.validList.values,Size.values,Size. [...]"
          }
          

          提交无效项目:

          curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[{"id": -11, "token": ""}]'
          
          {
             "timestamp" : "2020-09-25T09:53:56.226+00:00",
             "error" : "Bad Request",
             "message" : "Validation failed for object='validList'. Error count: 2",
             "exception" : "org.springframework.web.bind.MethodArgumentNotValidException",
             "path" : "/pojos",
             "status" : 400,
             "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo.ValidList<com.example.demo.SomePojo> com.example.demo.SomeController.test(com.example.demo.ValidList<com.example.demo.SomePojo>) with 2 errors: [Field error in object 'validList' on field 'values[0].id': rejected value [-11]; co [...]"
          }
          

          【讨论】:

            【解决方案7】:

            使用 org.springframework.validation.beanvalidation.LocalValidatorFactoryBean 作为成员实现您自己的验证器,并为每个项目调用该验证器。

            public class CheckOutValidator implements Validator {
            
            
                private Validator validator;
            
               @Override
                public void validate(Object target, Errors errors) { 
                List request = (List) target;
                Iterator it = request.iterator()   
                while(it.hasNext()) {
                MyBean b = it.next();
                validator.validate(b, errors);
            
                 }
            
                 }
            
            //setters and getters
            
            }
            

            【讨论】:

              【解决方案8】:

              如果您不想为您拥有的每个 List 编写包装器,则可以使用通用包装器:

              public class ListWrapper<E> {
              
                  private List<E> list;
              
                  public ListWrapper() {
                      list = new ArrayList<>();
                  }
              
                  public ListWrapper(List<E> list) {
                      this.list = list;
                  }
              
                  @Valid
                  public List<E> getList() {
                      return list;
                  }
              
                  public void setList(List<E> list) {
                      this.list = list;
                  }
              
                  public boolean add(E e) {
                      return list.add(e);
                  }
              
                  public void clear() {
                      list.clear();
                  }
              
              }
              

              【讨论】:

                【解决方案9】:

                我认为您最好的选择是将列表包装起来 - How to validate request parameter if it is not a bean in spring MVC?

                atm 没有办法说 @Valid 适用于集合的元素。

                【讨论】:

                • 有什么方法可以在不改变 json 输入的情况下包装列表?
                【解决方案10】:
                @Valid @RequestBody List<MyBean> request
                

                只要您提交有效的 json,就可以为我工作:-

                [
                    {
                        "property1": "value1",
                        "property2": "value2"
                      },
                    {
                        "property1": "value3",
                        "property2": "value4"
                        }
                ]
                

                【讨论】:

                • 如果您提交有效的 JSON,这确实有效,但问题是如果 JSON 无效,如何使用@Valid 触发验证...
                猜你喜欢
                • 1970-01-01
                • 2013-04-14
                • 2015-08-04
                • 2016-10-17
                • 1970-01-01
                • 2011-04-19
                • 2015-05-01
                • 1970-01-01
                • 2013-06-02
                相关资源
                最近更新 更多