Spring MVC - REST服务中bean列表上的@Valid

时间:2013-06-20 07:32:03

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

在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列表进行验证?有哪些替代方案?


10 个答案:

答案 0 :(得分:41)

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

  

JSR-303 Specification 3.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包装在另一个对象中,如其他答案所示。

答案 1 :(得分:12)

我能找到的唯一方法就是包装列表,这也意味着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

答案 2 :(得分:5)

尝试直接验证。像这样:

@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())
        ...

答案 3 :(得分:2)

使用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

}

答案 4 :(得分:1)

有一种优雅的方式可以将您的请求包装在自定义java.util.List中,该List同时充当JavaBeanabs(r1) > 15see here

答案 5 :(得分:1)

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

 var tagsMeta = [];
    //alert(tagsMeta);
    var tagsString = document.getElementById('metaTags').value;
    //alert(tagsString);
    var tagsArray = tagsString.split(',');
    for(i=0; i < tagsArray.length; i++) {
      tagsMeta.push({tag: tagsArray[i]});
    }

    $('.chips-initial').material_chip({
        data: tagsMeta
    });

答案 6 :(得分:1)

给Spring-Boot + Jackson进行JSON序列化+ org.springframework.boot:spring-boot-starter-validation(对于Spring Boot> = 2.3.0,必须手动提供)

使用内置

  • @Validated添加到您的控制器
  • 在控制器方法签名中使用@Valid @NotNull @RequestBody List<@Valid Pojo> pojoList

这将在无效的bean上引发javax.validation.ConstraintViolationException错误,默认情况下,该错误映射到500 Internal Error。因此,请确保您也为此使用ControllerAdvice

使用包装器

列表包装器很好(也就是说,一个类具有单个List<E>类型的字段),但是从上面的响应中,您还必须更改JSON({"list": []}与{{ 1}}),这不是很好...

解决方案:

  • 在包装中,在包装列表字段上使用[]批注
  • 添加一个以列表为参数的构造函数,并使用@JsonValue
  • 对其进行注释
  • 在您的控制器方法中,使用@JsonCreator

这有效,优雅,并且不需要任何其他功能。而且,它将在无效的bean上抛出通常的@Valid @RequestBody ListWrapper<Pojo> tokenBodies


包装器示例(java)

(有关科特琳的完整示例,请参见https://stackoverflow.com/a/64060909

org.springframework.web.bind.MethodArgumentNotValidException
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": ""}]'

答案 7 :(得分:0)

我认为你最好的选择是包装清单 - How to validate request parameter if it is not a bean in spring MVC?

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

答案 8 :(得分:0)

使用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

[]

答案 9 :(得分:0)

@Valid @RequestBody List<MyBean> request
只要您提交有效的json,

就可以为我工作:-

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