在 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 列表没有被验证,“ StringUUIDValization”验证器从未被调用。

如果我删除了 List 并且只尝试发送一个 CompanyTag,也就是说:

@RequestBody @Valid List<CompanyTag> categories,

用途:

@RequestBody @Valid CompanyTag category,

它像预期的那样工作,所以很明显 Spring 不喜欢验证事物列表(尝试使用 array 代替,也没有工作)。

有人知道少了什么吗?

141399 次浏览

验证集合不能直接工作。

例如: 如果多个元素验证失败,它应该做什么?第一次确认后就停止?验证所有(如果是这样,那么对消息集合要做些什么) ?

如果在您的配置中,Spring 委托给类似 Hibernate Validator 的 Bean Validator 提供程序,那么您应该在那里查找实现集合验证器的方法。

对于 Hibernate,也讨论了类似的问题 给你

我建议将 List 类别包装到一些 DTO bean 中并对其进行验证。除了工作验证之外,您还将受益于更灵活的 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
}

我找到了另一个有效的方法。最基本的问题是,您希望将一个列表作为服务的输入负载,但是 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 ...
}

我认为最好的解决方案是为 Collection 创建一个自定义 Validator,并在 WebDataBinders 中注册一个@ControllerAdvisory,查看一下 控制器方法中绑定到集合的 RequestBody 参数的 Spring 验证

1 TL; DR

我尝试在我的项目中使用保罗的方法,但有些人说它太复杂了。不久之后,我找到了另一种简单的方法,它的工作原理与下面的代码类似:

@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。释放我使用的。

2异常处理

正如注释中所讨论的,异常可以像下面的代码那样处理:

@RestControllerAdvice
@Component
public class ControllerExceptionHandler {


/**
* handle controller methods parameter validation exceptions
*
* @param exception ex
* @return wrapped result
*/
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public DataContainer handle(ConstraintViolationException exception) {


Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
StringBuilder builder = new StringBuilder();
for (ConstraintViolation<?> violation : violations) {
builder.append(violation.getMessage());
break;
}
DataContainer container = new DataContainer(CommonCode.PARAMETER_ERROR_CODE, builder.toString());
return container;
}
}

以 HTTP状态码代表网络是可以的,这里只返回第一个违规消息。您可以更改它以满足定制的要求。

3如何工作(代码部分)

使用类级别的@Valated,方法的参数通过春季启动中所谓的方法级别验证来验证,这不仅适用于控制器,而且适用于 IOC容器管理的任何 bean。

顺便说一下,方法级验证(简称验证 A)中的方法通过

  • 验证。方法验证拦截器

而典型的弹簧启动控制器方法验证(简称为验证 B)在

  • 注释.RequestResponseBodyMethodProcessor

默认情况下,它们都将实际的验证操作导向 org.hibernate.validator.internal.engine.ValidatorImpl,但是它们调用的方法是不同的,这导致了验证逻辑的差异。

  • ValidatorImpl中调用 validateParameters方法
  • ValidatorImpl中调用 validate方法

它们是不同的方法,具有不同的功能,因此在验证 A/B 时会导致不同的结果,典型的一点是列表对象的验证:

  • 对集合对象的元素进行触发器约束检查

4如何工作(说明部分)

JSR-303定义了我们上面讨论的方法的函数。

验证方法部分解释了 validate方法,并且实现必须遵守 验证程序中定义的逻辑,在 验证程序中它声明它将对对象的所有可到达字段执行所有约束验证,这就是为什么 List对象的元素(或其他集合实例)不能通过这个方法验证-集合的元素不是集合实例的字段。

但是 validateParameters,JSR-303实际上并没有把它作为主要话题,而是把它放在了 Appendix C. Proposal for method-level validation中。它提供了一些描述:

The constraints declarations evaluated are the constraints hosted on the parameters of the method or constructor. If @Valid is placed on a parameter, constraints declared on the object itself are considered.


validateReturnedValue evaluates the constraints hosted on the method itself. If @Valid is placed on the method, the constraints declared on the object itself are considered.


public @NotNull String saveItem(@Valid @NotNull Item item, @Max(23) BigDecimal price)


In the previous example,


- item is validated against @NotNull and all the constraints it hosts
- price is validated against @Max(23)
- the result of saveItem is validated against @NotNull

感叹 Bean Validation providers are free to implement this proposal as a specific extension。据我所知,Hibernate Validation项目实现了这个方法,使约束在对象本身和集合对象的元素上工作。

有人抱怨

我不知道为什么弹簧框架家伙调用 RequestResponseBodyMethodProcessor中的 validate,使许多相关的问题出现在堆栈溢出。也许这只是因为 http post 主体数据通常是一个表单数据,并且可以自然地用 java bean 来表示。如果是我,我会调用 RequestResponseBodyMethodProcessor中的 validateParametes以便于使用。

@ 保罗 · 斯特拉克的 abc 0与龙目岛魔术的结合:

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

(需要 龙目岛,但如果你还没有使用它,你真的想尝试一下)

我使用的是 Spring-boot 1.5.19。释放

我用 @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;
}

全局异常处理程序方法(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
}
]
}
]

使用 @ 确认注释控制器
使用 @ Valid注释@RequestBody

创建实体类:

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

@Valid注释可以在菱形操作符内部使用:

private List<@Valid MyType> types;

或者

@Valid
private List<MyType> types;

现在,将验证每个列表项。

这是我试图调和许多不同答案的尝试。

正如 保罗的回答所要求的那样,Lebecca 的回答 可以在不需要包装器的情况下工作,因为放置在类上的 @Validated支持 Bean 验证 API 的 方法验证功能方法验证功能

Hibernate Validator 文档特别解释道:

[ ... ]@Valid 注释可以用来标记可执行参数并为级联验证返回值。

[...]

级联验证不仅可以应用于简单对象 引用,但也指向集合类型的参数和返回值。 这意味着在将@Valid 注释放到参数或返回值时 价值

  • 是一个数组

  • 实现 java.lang. Iterable

  • 或者实现 java.util.Map

验证每个包含的元素。

如果您需要验证 豆子的集合,这是最方便的方法(请确保还根据需要实现 @ExceptionHandler)。

如果您需要验证 非豆类的集合,例如 List<String>,其中每个元素必须匹配一个模式,您可以像这样使用 容器元素约束:

controllerMethod(List<@Pattern(regexp="pattern") String> strings)

也有可能 只有在控制器方法参数(然后必须是 Bean 类型)上使用 @Valid没有也在类上放置 @Validated。在这种情况下,您可以“免费”获得适当的、详细的 HTTP 400响应,即不需要定制的 @ExceptionHandler。但是这并不应用级联验证,因此您不能验证类似 @Valid List<SomeBean> beans的东西,它也不支持容器元素约束。

最后,您可以将后一种方法与添加到类型 BindingResult的方法中的额外参数结合起来。在出现验证错误的情况下,这不会触发自动错误响应,但是您必须自己在方法体中检查注入的 BindingResult并相应地采取行动(这允许更大的灵活性)。这在 这个综合答案中有所描述。

(这个答案在 科特林中,有关 爪哇咖啡,请参阅 https://stackoverflow.com/a/64061936)

对于那些使用 KotlinJackson的人,这里是 不需要包装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
}

提交 OK:

 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: [...]"
}

使用 弹簧靴 2.4.1:

  1. @Validated注释添加到类中

  2. @Valid注释移到菱形操作符中:

    @RestController
    @Validated          // <-- This activates the Spring Validation AOP interceptor
    public class MyController {
    
    
    ...
    @RequestBody List<@Valid CompanyTag> categories
    // ^^^ - NOTE: the @Valid annotation is inside <> brackets
    

在 SpringBoot2.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.

注意: List 和 String 不会在方法参数级别验证,如果在 Class 级别删除 @ 确认

SpringBoot 文档说:-

17. 验证

只要类路径上有一个 JSR-303实现(比如 Hibernate 验证器) ,Bean 验证1.1支持的方法验证特性就会自动启用。这使得 bean 方法可以在其参数和/或返回值上使用 javax.via 约束进行注释。具有这种带注释的方法的目标类需要在类型级别使用@Valated 注释进行注释,以便搜索它们的方法以获得内联约束注释。

在 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 注释在泛型参数中。

如果使用自定义的 javax 验证注释,请确保将 TYPE _ USE 添加到注释目标

@Target({ ElementType.TYPE_USE})
public @interface ValidationAnnotation {.. }

我采取了以下步骤来验证列表:

  1. 在类级别用 @Validated注释 rest 控制器
  2. 在列表中的泛型类型之前添加@Valid,即 List<@Valid MyClass>

另外,发现如果验证失败,我会得到 javax.validy.ConstraintViviationException

对于那些使用 spring boot (我使用的是2.6.7)的用户,我的工作方式是添加 spring-boot-starter-validation 依赖项:

org.springframework.boot:spring-boot-starter-validation

我在吸毒

  • 科特林1.6
  • SpringBoot2.6.6
  • Spring Webflux

我需要验证一个 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
)

建筑,教育,体育

dependencies {
implementation("org.springframework.boot:spring-boot-starter-validation")
}

我已经对我们传递的参数列表进行了自定义验证...’

    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....
    

}