Java 验证 Spring 中的对象列表

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/28150405/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 05:41:06  来源:igfitidea点击:

Validation of a list of objects in Spring

javaspringlistvalidation

提问by TheZuck

I have the following controller method:

我有以下控制器方法:

@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 is defined this way:

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

The problem is that validation is not triggered, the CompanyTag list is not validated, the "StringUUIDValidation" validator is never called.

问题是未触发验证,未验证 CompanyTag 列表,从未调用“StringUUIDValidation”验证器。

If I remove the List and only try to send a single CompanyTag, i.e. instead of:

如果我删除列表并仅尝试发送一个 CompanyTag,即而不是:

@RequestBody @Valid List<CompanyTag> categories,

use:

用:

@RequestBody @Valid CompanyTag category,

it works as expected, so apparently Spring does not like to validate lists of things (tried with array instead, that did not work either).

它按预期工作,因此显然 Spring 不喜欢验证事物列表(尝试使用数组,但也不起作用)。

Anybody have any idea what's missing?

有人知道缺少什么吗?

回答by Cristian Sevescu

Validating a collection does not work directly.

验证集合不能直接工作。

For example: what should it do if multiple elements fail the validation? Stop after first validation? Validate all (if so what is to be done with the collection of messages)?

例如:多个元素验证不通过怎么办?第一次验证后停止?验证所有(如果是这样,要对消息集合做什么)?

If in your configuration Spring delegates to a Bean Validator provider like Hibernate Validator, you should look up for ways of implementing a collection validator there.

如果在你的配置中 Spring 委托给一个 Bean Validator 提供者,比如 Hibernate Validator,你应该寻找在那里实现集合验证器的方法。

For Hibernate, a similar problem is discussed here

对于 Hibernate,这里讨论了一个类似的问题

回答by Babl

I would suggest to wrap your List categories into some DTO bean and validate it. Beside of working validation you will benefit from more flexible API.

我建议将您的 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
}

回答by Paul Strack

I found another approach that works. The basic problem is that you want to have a list as your input payload for your service, but javax.validation won't validate a list, only a JavaBean. The trick is to use a custom list class that functions as both a List anda JavaBean:

我找到了另一种有效的方法。基本问题是您希望将列表作为服务的输入有效负载,但 javax.validation 不会验证列表,只会验证 JavaBean。诀窍是使用一个自定义列表类,它既可以用作 List又可以用作 JavaBean:

@RequestBody @Valid List<CompanyTag> categories

Change to:

改成:

@RequestBody @Valid ValidList<CompanyTag> categories

Your list subclass would look something like this:

您的列表子类将如下所示:

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

回答by eruiz

I think the most elegant solution is to create a custom Validator for Collection and a @ControllerAdvice that registers that Validator in the WebDataBinders, take a look to Spring validation for RequestBody parameters bound to collections in Controller methods

我认为最优雅的解决方案是为集合创建一个自定义验证器和一个在 WebDataBinders 中注册该验证器的 @ControllerAdvice,查看绑定到控制器方法中集合的 RequestBody 参数的 Spring 验证

回答by Lebecca

1 TL;DR

1 TL;博士

I tried to use Paul's method in my project, but some people said it's too complex. Not long after that, I find another easy way which works like code below:

我尝试在我的项目中使用Paul的方法,但有人说它太复杂了。不久之后,我找到了另一种简单的方法,其工作方式类似于以下代码:

@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) {

  }
}

It works and easy to use. The key point is the @Valiated annotation on the class. Btw, it's springBootVersion = '2.0.4.RELEASE' that I use.

它有效且易于使用。关键点是类上的@Valiated 注释。顺便说一句,我使用的是 springBootVersion = '2.0.4.RELEASE'。

2 Exception handling

2 异常处理

As discussed in comments, exceptions can be handled like code below:

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

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

Taking http status code as representing network is ok and only first violation message is returned here. You may change it to satisfy customized requirements.

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

3 How it works (code part)

3 工作原理(代码部分)

With @Validated on class level, parameters of methods are validated by what called method-level validation in spring boot, which is not only worked for controllers, but any bean the IOCcontainer managed.

使用类级别的 @Validated,方法的参数通过 Spring Boot 中所谓的方法级别验证进行验证,这不仅适用于控制器,还适用于IOC容器管理的任何 bean 。

By the way, the methods in method level validation (short as validation A) is enhanced by

顺便说一下,方法级验证(简称验证 A)中的方法增强了

  • org.springframework.validation.beanvalidation.MethodValidationInterceptor
  • org.springframework.validation.beanvalidation.MethodValidationInterceptor

while the typical spring boot controller methods validation (short as validation B) is processed in

而典型的spring boot控制器方法验证(简称验证B)在

  • org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
  • org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor

Both of them lead the actual validation operation to org.hibernate.validator.internal.engine.ValidatorImplby default, but the methods they call are different, which leads to the differences in validation logic.

两者都org.hibernate.validator.internal.engine.ValidatorImpl默认引导实际的验证操作,只是调用的方法不同,导致验证逻辑不同。

  • MethodValidationInterceptorcall validateParametersmethod in ValidatorImpl
  • RequestResponseBodyMethodProcessorcall validatemethod in ValidatorImpl
  • MethodValidationInterceptor调用validateParameters方法ValidatorImpl
  • RequestResponseBodyMethodProcessor调用validate方法ValidatorImpl

They are different methods with different functions, so lead to different results in validation A/B, the typical point is the validation of list object:

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

  • A triggers constraint check on element of collection object while B not
  • A 触发对集合对象元素的约束检查,而 B 不触发

4 How it works (specification part)

4 工作原理(规范部分)

The JSR-303defines functions of the methods we discussed above.

JSR-303的方法中定义的功能,我们上面讨论的。

validatemethod is explained in the validation methodpart, and the implementation must obey the logic defined in validation routine, in which it states that it will execute all the constraint validation for all reachable fields of the object, this is why element of Listobject (or other collection instance) cannot be validated via this method - the elements of the collection are not fields of the collection instance.

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

But validateParameters, JSR-303 actually doesn't treat it as main topic and put it in Appendix C. Proposal for method-level validation. It provides some description:

但是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

and exclaim that Bean Validation providers are free to implement this proposal as a specific extension. As far as I know, the Hibernate Validationproject implements this method, makes constraints works on the object itself, and element of collection object.

并惊呼Bean Validation providers are free to implement this proposal as a specific extension。据我所知,该Hibernate Validation项目实现了这个方法,使约束对对象本身和集合对象的元素起作用。

5 Some complain

5 有人抱怨

I don't know why the spring framework guys call validatein RequestResponseBodyMethodProcessor, makes lots of related questions appeare in stackoverflow. Maybe it's just because http post body data usually is a form data, and can be represented by a java bean naturally. If it's me, I'll call the validateParametesin RequestResponseBodyMethodProcessorfor easy use.

我不知道为什么春天框架家伙打电话validateRequestResponseBodyMethodProcessor,使得很多相关问题在计算器appeare。也许只是因为http post body数据通常是表单数据,自然可以用java bean来表示。如果是我,我会打电话给validateParametesRequestResponseBodyMethodProcessor易于使用。

回答by laffuste

@Paul Strack's great solutionmixed with Lombok magic:

@Paul Strack 的出色解决方案与 Lombok 魔法相结合:

@Data
public class ValidList<E> implements List<E> {
    @Valid
    @Delegate
    private List<E> list = new ArrayList<>();
}

Usage (swap List for ValidList):

用法(将列表替换为 ValidList):

public ResponseEntityWrapper updateMapTheme(
        @RequestBody @Valid ValidList<CompanyTag> categories, ...)

(Needs Lombok, but if you don't use it already you really want to try it out)

(需要Lombok,但如果您还没有使用它,您真的很想尝试一下)

回答by Richard Collette

I'm using spring-boot 1.5.19.RELEASE

我正在使用 spring-boot 1.5.19.RELEASE

I annotate my service with @validatedand then apply @Validto the Listparameter in the method and items in my list get validated.

我对我的服务进行注释@validated,然后应用@ValidList方法中的参数,我的列表中的项目得到验证。

Model

模型

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

Service Interface (or use on concrete type if no interface)

服务接口(如果没有接口,则在具体类型上使用)

@Validated
public interface SubscriptionService {
    List<SubscriptionCreateResult> addSubscriptions(@NonNull @Size(min = 1) @Valid List<SubscriptionRequest> subscriptionRequestList)
        throws IOException;
}

Global Exception Handler method (ApiError Type is not my design)

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

example bad method call from a controller

来自控制器的错误方法调用示例

 LinkedList<SubscriptionRequest> list = new LinkedList<>();
 list.add(new SubscriptionRequest());
 return subscriptionService.addSubscriptions(list);

Response body (note the index [0])

响应正文(注意索引 [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
            }
        ]
    }
]

回答by MattonRoi

use @Validatedannotate controller
use @Validannotate @RequestBody

使用@Validated注释控制器
使用@Valid注释@RequestBody

回答by QiongLee

create entity class:

创建实体类:

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

use Controller

使用控制器

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

回答by Hamid Mohayeji

The @Validannotation can be used inside the diamond operator:

@Valid注释可以在钻石经营者内部使用:

private List<@Valid MyType> types;

or

或者

@Valid
private List<MyType> types;

Now, every list item will be validated.

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