Проверка списка объектов в Spring
У меня есть следующий метод контроллера:
@RequestMapping(value="/map/update", method=RequestMethod.POST, produces = "application/json; charset=utf-8")
@ResponseBody
public ResponseEntityWrapper updateMapTheme(
HttpServletRequest request,
@RequestBody @Valid List<CompanyTag> categories,
HttpServletResponse response
) throws ResourceNotFoundException, AuthorizationException {
...
}
CompanyTag определяется следующим образом:
public class CompanyTag {
@StringUUIDValidation String key;
String value;
String color;
String icon;
Icon iconObj;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
...
}
Проблема в том, что проверка не инициируется, список CompanyTag не проверяется, валидатор "StringUUIDValidation" никогда не вызывается.
Если я удаляю Список и пытаюсь отправить только один CompanyTag, т.е. вместо:
@RequestBody @Valid List<CompanyTag> categories,
использовать:
@RequestBody @Valid CompanyTag category,
он работает, как и ожидалось, поэтому, видимо, Spring не любит проверять списки вещей (пробовал с массивом, который тоже не работал).
У кого-нибудь есть идеи, чего не хватает?
21 ответ
Я нашел другой подход, который работает. Основная проблема заключается в том, что вы хотите иметь список в качестве входных данных для своего сервиса, но javax.validation не будет проверять список, только JavaBean. Хитрость заключается в том, чтобы использовать пользовательский класс списка, который функционирует как List и JavaBean:
@RequestBody @Valid List<CompanyTag> categories
Изменить на:
@RequestBody @Valid ValidList<CompanyTag> categories
Ваш подкласс списка будет выглядеть примерно так:
public class ValidList<E> implements List<E> {
@Valid
private List<E> list;
public ValidList() {
this.list = new ArrayList<E>();
}
public ValidList(List<E> list) {
this.list = list;
}
// Bean-like methods, used by javax.validation but ignored by JSON parsing
public List<E> getList() {
return list;
}
public void setList(List<E> list) {
this.list = list;
}
// List-like methods, used by JSON parsing but ignored by javax.validation
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
// Other list methods ...
}
Я пытался использовать метод Пола в своем проекте, но некоторые люди говорили, что он слишком сложный. Вскоре после этого я нахожу другой простой способ, который работает как код ниже:
@Validated
@RestController
@RequestMapping("/parent")
public class ParentController {
private FatherRepository fatherRepository;
/**
* DI
*/
public ParentController(FatherRepository fatherRepository) {
this.fatherRepository = fatherRepository;
}
@PostMapping("/test")
public void test(@RequestBody @Valid List<Father> fathers) {
}
}
Работает и прост в использовании. Ключевым моментом является аннотация @Valiated для класса. Кстати, это SpringBootVersion = '2.0.4.RELEASE', который я использую.
Великолепное решение @Paul Strack'а, смешанное с магией Ломбок:
@Data
public class ValidList<E> implements List<E> {
@Valid
@Delegate
private List<E> list = new ArrayList<>();
}
Использование (список подкачки для ValidList):
public ResponseEntityWrapper updateMapTheme(
@RequestBody @Valid ValidList<CompanyTag> categories, ...)
(Нуждается в Lombok, но если вы уже не используете его, вы действительно хотите попробовать его)
Использование Spring Boot
2.4.1
:
Добавить
@Validated
аннотация к классуПереместите
@Valid
аннотация внутри оператора ромба:@RequestBody List<@Valid CompanyTag> categories
Я бы предложил обернуть ваши категории List в некоторый компонент DTO и проверить его. Помимо рабочей проверки вы получите более гибкий API.
@RequestMapping(value="/map/update", method=RequestMethod.POST, produces = "application/json; charset=utf-8")
@ResponseBody
public ResponseEntityWrapper updateMapTheme(
HttpServletRequest request,
@RequestBody @Valid TagRequest tagRequest,
HttpServletResponse response
) throws ResourceNotFoundException, AuthorizationException {
...
}
public static class TagRequest {
@Valid
List<CompanyTag> categories;
// Gettes setters
}
Я сделал следующие шаги, чтобы проверка работала в списке:
1-Аннотировать остальной контроллер с помощью @Valiodated на уровне класса
2-Добавить @Valid перед общим типом в списке, т.е. List<@Vlaid MyClas>
Кроме того, обнаружил, что если проверка не удалась, я получил javax.validation.ConstraintViolationException
Я думаю, что наиболее элегантным решением является создание пользовательского Validator для Collection и @ControllerAdvice, который регистрирует этот Validator в WebDataBinders, взгляните на проверку Spring для параметров RequestBody, связанных с коллекциями в методах Controller.
С более поздними версиями Spring теперь вы можете это сделать.
@RequestMapping(value="/map/update", method=RequestMethod.POST, produces = "application/json; charset=utf-8")
@ResponseBody
public ResponseEntityWrapper updateMapTheme(
HttpServletRequest request,
@RequestBody List<@Valid CompanyTag> categories,
HttpServletResponse response
) throws ResourceNotFoundException, AuthorizationException {
...
}
аннотация @Valid находится в общем параметре. Однако, если вы хотите проверить весь список, вам все равно нужно обернуть его в объект.
Использовать @Validated аннотировать контроллер
используйте @Valid annotate @RequestBody
В @Valid
аннотацию можно использовать внутри оператора ромба:
private List<@Valid MyType> types;
или
@Valid
private List<MyType> types;
Теперь каждый элемент списка будет проверен.
Вот моя попытка согласовать множество разных ответов.
Ответ Лебекки работает без необходимости в оболочке, как того требует ответ Пола, потому что@Validated
помещенный в класс, включает функцию проверки методов API проверки компонентов.
Документация Hibernate Validator конкретно объясняет:
[...] аннотацию @Valid можно использовать для пометки исполняемых параметров и возвращаемых значений для каскадной проверки.
[...]
Каскадная проверка может применяться не только к простым ссылкам на объекты, но и к параметрам типа коллекции и возвращаемым значениям. Это означает, что при добавлении аннотации @Valid к параметру или возвращаемому значению
это массив
реализует java.lang.Iterable
или реализует java.util.Map
проверяется каждый содержащийся элемент.
Если вам нужно проверить коллекцию Beans, это наиболее удобный способ (не забудьте также реализовать@ExceptionHandler
как требуется).
Если вам нужно проверить коллекцию не-Beans, напримерList<String>
где каждый элемент должен соответствовать шаблону, вы можете использовать такие ограничения контейнерных элементов:
controllerMethod(List<@Pattern(regexp="pattern") String> strings)
Также есть возможность использовать только@Valid
в параметре метода контроллера (который в таком случае должен быть типом Bean) без размещения@Validated
на классе. В этом случае вы получите соответствующий подробный ответ HTTP 400 "бесплатно", т. Е. Без необходимости настраивать@ExceptionHandler
. Но при этом не применяется каскадная проверка, поэтому вы не можете проверить что-то вроде@Valid List<SomeBean> beans
, а также не поддерживает ограничения элементов контейнера.
И, наконец, вы можете объединить последний подход с дополнительным параметром, добавленным к методу типа BindingResult
. Это не вызовет автоматический ответ об ошибке в случае ошибки проверки, но вместо этого вы должны проверить введенныйBindingResult
себя в теле метода и действуйте соответственно (что обеспечивает большую гибкость). Это описано в этом исчерпывающем ответе.
Проверка коллекции не работает напрямую.
Например: что делать, если несколько элементов не прошли проверку? Остановить после первой проверки? Подтвердить все (если так, что делать с коллекцией сообщений)?
Если в вашей конфигурации Spring делегирует провайдера Bean Validator, такого как Hibernate Validator, вам следует поискать способы реализации там валидатора коллекции.
Для Hibernate аналогичная проблема обсуждается здесь
Я использую Spring-Boot 1.5.19.RELEASE
Я комментирую свой сервис с @validated
а затем применить @Valid
к List
параметр в методе и элементы в моем списке проверяются.
модель
@Data
@ApiModel
@Validated
public class SubscriptionRequest {
@NotBlank()
private String soldToBpn;
@NotNull
@Size(min = 1)
@Valid
private ArrayList<DataProducts> dataProducts;
private String country;
@NotNull
@Size(min = 1)
@Valid
private ArrayList<Contact> contacts;
}
Сервисный интерфейс (или использовать на конкретном типе, если нет интерфейса)
@Validated
public interface SubscriptionService {
List<SubscriptionCreateResult> addSubscriptions(@NonNull @Size(min = 1) @Valid List<SubscriptionRequest> subscriptionRequestList)
throws IOException;
}
Метод Global Exception Handler (ApiError Type не мой дизайн)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseBody
public ApiError[] handleConstraintViolationException(ConstraintViolationException exception) {
List<InvalidField> invalidFields = exception.getConstraintViolations().stream()
.map(constraintViolation -> new InvalidField(constraintViolation.getPropertyPath().toString(),
constraintViolation.getMessage(),
constraintViolation.getInvalidValue()))
.collect(Collectors.toList());
return new ApiError[] {new ApiError(ErrorCodes.INVALID_PARAMETER, "Validation Error", invalidFields)};
}
пример плохого вызова метода из контроллера
LinkedList<SubscriptionRequest> list = new LinkedList<>();
list.add(new SubscriptionRequest());
return subscriptionService.addSubscriptions(list);
Тело ответа (обратите внимание на индекс [0])
[
{
"errorCode": "invalid.parameter",
"errorMessage": "Validation Error",
"invalidFields": [
{
"name": "addSubscriptions.arg0[0].soldToBpn",
"message": "may not be empty",
"value": null
},
{
"name": "addSubscriptions.arg0[0].dataProducts",
"message": "may not be null",
"value": null
},
{
"name": "addSubscriptions.arg0[0].contacts",
"message": "may not be null",
"value": null
}
]
}
]
Создать класс сущности:
import javax.validation.Valid;
import java.util.List;
public class ValidList<E> {
@Valid
private List<E> list;
public List<E> getList() {
return list;
}
public void setList(List<E> list) {
this.list = list;
}
}
использовать контроллер
@RequestMapping(value = "/sku", method = RequestMethod.POST)
public JsonResult createSKU(@Valid @RequestBody ValidList<Entity> entityList, BindingResult bindingResult) {
if (bindingResult.hasErrors())
return ErrorTools.build().handlerError(bindingResult);
return new JsonResult(200, "result");
}
Для тех, кто использует весеннюю загрузку (я использовал 2.6.7), мне помогло добавление зависимости spring-boot-starter-validation:
org.springframework.boot:spring-boot-starter-validation
После нескольких попыток я нашел это решение! Надеюсь, вам будет полезно.
Проверять! вы указываете на Java 17 из пути сборки (Java 17 или выше, поскольку используется Spring Boot 3.x , который обеспечивает Hibernate-Validator 8.0.0.Final)
@PostMapping
@ResponseStatus(code = HttpStatus.OK)
public List<CompanyTag> updateMapTheme(@RequestBody List<@Valid CompanyTag> companyTag) {
return appService.updateMapTheme(companyTag);
}
Убедитесь, что @Valid находится внутри ромбовидных скобок!@RequestBody List<@Valid Event> events
Окончательно! Перезапустите IDE, как только вы измените путь JDK на 17 или выше.
Ваше здоровье! Приятного кодирования :)
Я использую
- Котлин 1.6
- Весенняя загрузка 2.6.6
- Весенний вебфлюкс
Мне нужно было подтвердитьList<String>
параметры запроса. Вот мой рабочий пример (вдохновленный некоторыми из предыдущих ответов)
@RestController
@Validated
class SearchController {
@GetMapping("/search")
fun search(
@Valid
@RequestParam(value = "term") terms: List<Term>,
): Mono<ResponseEntity<SearchResponse>> {...}
}
data class Term(
@field:NotEmpty(
message = "Term is required"
)
@field:Size(
min = 2,
max = 500,
message = "Term length out of range"
)
val term: String
)
в build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-validation")
}
(этот ответ находится в Котлине, для Java см. /questions/40099525/spring-mvc-valid-v-spiske-binov-v-rest-servise/56563176#56563176)
Для тех, кто использует Котлин и Джексон, вот
ValidatedList
класс, который не требует обертывания, то есть он все равно будет сериализован / десериализован как обычный список:
class ValidatedList<E> {
/**
* By default, spring-boot cannot validate lists, as they are generic AND do not conform to the Java Bean definition.
* This is one work-around: create a wrapper that fits the Java Bean definition, and use Jackson annotations to
* make the wrapper disappear upon (de)serialization.
* Do not change anything (such as making the _value field private) or it won't work anymore !
*
* Usage:
* ```
* @PostMapping("/something")
* fun someRestControllerMethod(@Valid @RequestBody pojoList: ValidatedList<SomePOJOClass>){
* // access list with:
* pojoList.values
*}
* ```
*/
@JsonValue
@Valid
@NotNull
@Size(min = 1, message = "array body must contain at least one item.")
var _values: List<E>? = null
val values: List<E>
get() = _values!!
@JsonCreator
constructor(vararg list: E) {
this._values = list.asList()
}
}
Преимущества:
- нет необходимости в
@Validated
аннотация - выдаст ошибку, если тело представляет собой пустой массив (см.
@Size
) - исключение будет правильно сопоставлено
400 Bad Request
(чего не происходит при использованииjavax
и@Validated
аннотация)
Пример:
data class N(
@field:Min(value = 0, message = "id must be positive.")
val id: Long? = null,
@field:NotNull
@field:Size(min = 2, max = 32, message = "wrong size: should be 32 chars long.")
val token: String? = null
)
@RestController
class XController {
@PostMapping("/ns")
fun getNs(@Valid @NotNull @RequestBody wrap: ListWrapper<N>) = wrap
}
Отправить ОК:
curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[{"id": 11, "token": "something"}]'
[{"id" : 11, "token" : "something"}]
Отправить пустое тело:
curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[]'
{
"timestamp" : "2020-09-25T08:49:30.324+00:00",
"message" : "Validation failed for object='listWrapper'. Error count: 1",
"error" : "Bad Request",
"path" : "/ns",
"status" : 400,
"exception" : "org.springframework.web.bind.MethodArgumentNotValidException",
"trace":"org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo.test.XController$ListWrapper<com.example.demo.test.XController$N> com.example.demo.test.XController.getNs(com.example.demo.test.XController$ListWrapper<com.example.demo.test.XController$N>): [Field error in object 'listWrapper' on field '_values': rejected value [[]]; codes [Size.listWrapper._values,Size._values,Size.java.util.List,Size]; [...]"
}
Отправьте недействительные элементы:
curl -H "Content-Type: application/json" -X POST http://localhost:8080/ns -d '[{"id": -11, "token": ""}]'
{
"message" : "Validation failed for object='listWrapper'. Error count: 2",
"path" : "/ns",
"exception" : "org.springframework.web.bind.MethodArgumentNotValidException",
"timestamp" : "2020-09-25T08:49:54.505+00:00",
"error" : "Bad Request",
"status" : 400,
"trace":"org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo.test.XController$ListWrapper<com.example.demo.test.XController$N> com.example.demo.test.XController.getNs(com.example.demo.test.XController$ListWrapper<com.example.demo.test.XController$N>) with 2 errors: [...]"
}
В версии Spring Boot 2.2.2 ...
Вот фрагмент кода:-
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Validated
public class MyController {
@PostMapping(value = "/test", consumes = "application/json", produces = "application/json")
public String test(@Valid @RequestBody List<Student> st) {
System.out.println("-------------test Method-------");
return "Its' Success";
}
}
class Student{
@NotBlank
String name;
@NotBlank
String password;
@NotBlank
String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Список данных JSON:-
Имя уведомления пусто во втором объекте Student.
[
{
"name": "Sreepad",
"password": "sddwh",
"email": "sample@gmail.oom"
},
{
"name": "",
"password": "sddwh",
"email": "sample@gmail.oom"
}
]
Описание ошибки:-
javax.validation.ConstraintViolationException: test.st[1].name: must not be blank.
Примечание. Список и строка не будут проверяться на уровне параметров метода, если вы удалите @Validated на уровне класса.
Документ SpringBoot говорит :-
17. Проверка
Функция проверки метода, поддерживаемая Bean Validation 1.1, автоматически включается, пока реализация JSR-303 (например, валидатор Hibernate) находится в пути к классам. Это позволяет аннотировать методы компонента с помощью ограничений javax.validation на их параметры и / или на их возвращаемое значение. Целевые классы с такими аннотированными методами необходимо аннотировать аннотацией @Validated на уровне типа, чтобы их методы искали аннотации встроенных ограничений.
Я выполнил пользовательскую проверку списка параметров, которые мы передаем... `
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = PatternListValidator.class)
public @interface PatternList {
public String regexp();
public String message() default "Invalid inputs";
public Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}`
Created the above custom validation annotation / interface and implemented the same with the business logic
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class PatternListValidator implements ConstraintValidator<PatternList, List<String>> {
private String regexp;
@Override
public void initialize(PatternList patternList) {
this.regexp = patternList.regexp();
}
@Override
public boolean isValid(List<String> dataList, ConstraintValidatorContext context) {
for(String data : dataList) {
if(!data.matches(regexp)) {
return false;
}
}
return true;
}
}
used this @PatternList annotation in my controller class as api method parameter as below
Public ResponseEntity<Object> getStudents(
@ApiParam(name = "studentIds", value = "Fetch students for athlete and art. Example values: 1234, 5432", required = true) @PatternList(regexp = "\\d+", message = "student Id's can contain only numbers") @RequestParam(value = "studentId", required = true) List<String> studentIds) {
business logic goes here....
}
Чтобы добавить к вышесказанному @laffuste с Lombok, в Spring Boot 2.7 у меня есть валидатор MyDtoList, который делегирует обратно валидатору в единственном числе для аргумента во множественном числе. В моем Spring RestController, который имеет аргументы в единственном и множественном числе:
В приложении yaml:
spring:
jackson:
deserialization:
accept-single-value-as-array: true
В моем контроллере:
@InitBinder("myDto")
public void addMyDtoValidator(WebDataBinder binder) {
binder.addValidators(myDtoValidator);
}
@InitBinder("myDtoList")
public void addMyDtoListValidator(WebDataBinder binder) {
binder.addValidators(myDtoListValidator);
}
Затем код валидатора:
private MyDtoValidator singleDtoValidator;
public MyDtoListValidator(MyDtoValidator singleDtoValidator) {
this.singleDtoValidator = singleDtoValidator;
}
@Override
public boolean supports(Class<?> clazz) {
return ValidList.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
if (target == null) {
errors.rejectValue(null, "should not be null error");
} else {
ValidList<MyDto> list = (ValidList<MyDto>) target;
for (MyDtodto: list) {
singleDtoValidator.validate(dto, errors);
}
}
}