Spring MVC - @Valid в списке бинов в REST-сервисе

В REST-сервисе Spring MVC (json) у меня есть такой метод контроллера:

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

Где у класса MyBean есть аннотации проверки компонента.

В этом случае проверки не выполняются, хотя это хорошо работает для других контроллеров.

Я не хочу инкапсулировать список в dto, который изменил бы вход json.

Почему нет проверки для списка бобов? Какие есть альтернативы?


7 ответов

Решение

@Valid является аннотацией JSR-303, а JSR-303 применяется для проверки на JavaBeans. java.util.List не является JavaBean (согласно официальному описанию JavaBean), поэтому его нельзя проверить напрямую с помощью совместимого с JSR-303 валидатора. Это подтверждается двумя наблюдениями.

Раздел 3.1.3 Спецификации JSR-303 гласит, что:

В дополнение к поддержке проверки экземпляров, также поддерживается проверка графиков объекта. Результат проверки графа возвращается в виде унифицированного набора нарушений ограничений. Рассмотрим ситуацию, когда бин X содержит поле типа Y. Аннотируя поле Y аннотацией @Valid, Валидатор проверяет Y (и его свойства), когда проверяется X. Точный тип Z значения, содержащегося в объявленном поле типа Y (подкласс, реализация), определяется во время выполнения. Используются определения ограничений 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

У меня есть пример приложения, которое проверяет валидацию коллекции с помощью 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 внутри другого объекта, как предлагали другие ответы.

Единственный способ найти способ сделать это - обернуть список, это также означает, что входные данные 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 является частью стандартного API-интерфейса проверки компонентов JSR-303 и не является конструкцией, специфичной для Spring. Spring MVC будет проверять объект @Valid после привязки, пока будет настроен соответствующий Validator.

Ссылка: 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 который действует как оба List а также JavaBean, посмотреть здесь

Использование 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

[]

Учитывая Spring-Boot + Jackson для сериализации JSON + org.springframework.boot:spring-boot-starter-validation (необходимо включить вручную для весенней загрузки>= 2.3.0)

Использование встроенных модулей

  • Добавить @Validated к вашему контроллеру
  • использовать @Valid @NotNull @RequestBody List<@Valid Pojo> pojoList в вашей сигнатуре метода контроллера

Это вызовет javax.validation.ConstraintViolationException ошибка для недопустимых bean-компонентов, которая отображается на 500 Internal Errorпо умолчанию. Следовательно, убедитесь, что у вас есть ControllerAdvice для этого тоже!

Использование обертки

Оболочка списка хороша (то есть класс с одним полем типа List<E>), но, судя по приведенным выше ответам, вам также придется изменить JSON ({"list": []} против []), что нехорошо...

Решение:

  • в обертке используйте @JsonValue аннотация в поле обернутого списка
  • добавить конструктор, принимающий список в качестве аргумента, и аннотировать его с помощью @JsonCreator
  • в вашем методе контроллера используйте @Valid @RequestBody ListWrapper<Pojo> tokenBodies

Это работает, элегантно и больше ничего не требует. Более того, бросит обычный org.springframework.web.bind.MethodArgumentNotValidException на недопустимые бобы.


Пример оболочки (java):

(Полный пример на Kotlin см. /questions/25434157/proverka-spiska-obektov-v-spring/56597479#56597479)

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

}

Если вы не хотите писать оболочку для каждого имеющегося списка, вы можете использовать универсальную оболочку:

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();
    }

}

Я думаю, что ваш лучший вариант, чтобы обернуть список - Как проверить параметр запроса, если это не bean-компонент весной MVC?

Нельзя сказать, что @Valid применяется к элементам коллекции.

@Valid @RequestBody List<MyBean> request

работает для меня, пока вы отправляете действительный json:-

[
    {
        "property1": "value1",
        "property2": "value2"
      },
    {
        "property1": "value3",
        "property2": "value4"
        }
]
Другие вопросы по тегам