繁体   English   中英

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

[英]Spring MVC - @Valid on list of beans in REST service

在 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 列表的验证? 有哪些选择?


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

JSR-303 规范的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暗示, Collection-valued, array-valued and generally Iterable fields and properties may also be decorated ,重点是字段和属性)。

实际的 JSR-303 实现

我有一个示例应用程序,它使用 Hibernate Validator 和 Apache Beans Validator 测试集合验证。 如果你在这个示例上运行测试作为mvn clean test -Phibernate (with Hibernate Validator) 和mvn clean test -Papache (for Beans Validator),两者都拒绝直接验证集合,这似乎符合规范。 由于 Hibernate Validator 是 JSR-303 的参考实现,此示例进一步证明了集合需要封装在 bean 中才能进行验证。


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

我能找到的唯一方法是包装列表,这也意味着 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 的构造。 只要配置了适当的验证器,Spring MVC 就会在绑定后验证 @Valid 对象。

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

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

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

有一种优雅的方法可以将您的请求包装在一个自定义java.util.List ,它同时充当ListJavaBean 看这里

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

[]

鉴于用于 JSON 序列化的 Spring-Boot + Jackson + 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 ,默认情况下映射到500 Internal Error 因此,请确保您也有一个ControllerAdvice

使用包装器

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

解决方案:

  • 在包装器中,在包装的列表字段上使用@JsonValue注释
  • 添加一个以列表为参数的构造函数,并使用@JsonCreator对其进行@JsonCreator
  • 在您的控制器方法中,使用@Valid @RequestBody ListWrapper<Pojo> 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 [...]"
}

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

}

如果您不想为您拥有的每个 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();
    }

}

我认为你最好的选择是包装列表 - 如果它不是 Spring MVC 中的 bean,如何验证请求参数?

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

@Valid @RequestBody List<MyBean> request

只要您提交有效的 json,就对我有用:-

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM