验证Spring中的对象列表

时间:2015-01-26 12:47:19

标签: java spring list validation

我有以下控制器方法:

@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列表,&#34; StringUUIDValidation&#34;永远不会调用验证器。

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

@RequestBody @Valid List<CompanyTag> categories,

使用:

@RequestBody @Valid CompanyTag category,

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

任何人都知道缺少什么?

15 个答案:

答案 0 :(得分:38)

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

答案 1 :(得分:8)

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

答案 2 :(得分:5)

我认为最优雅的解决方案是创建一个自定义Validator for Collection和一个@ControllerAdvice,在WebDataBinders中注册Validator,看一看Spring validation for RequestBody parameters bound to collections in Controller methods

答案 3 :(得分:5)

我试图在项目中使用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) {

  }
}

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

答案 4 :(得分:3)

验证集合不会直接起作用。

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

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

对于Hibernate,讨论了类似的问题here

答案 5 :(得分:2)

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

答案 6 :(得分:1)

@Paul Strack的great solution与龙目岛魔法混在一起:

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

用法(有效列表的交换列表):

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

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

答案 7 :(得分:1)

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

Lebecca's answer不需要Paul's answer便需要包装器,因为放置在类上的@Validated启用了Bean Validation API的method validation feature

Hibernate Validator documentation专门说明:

  

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

     

[...]

     

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

     
      
  • 是一个数组

  •   
  • 实现java.lang.Iterable

  •   
  • 或实现java.util.Map

  •   
     

每个包含的元素都经过验证。

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

如果您需要验证 Non-Beans 的集合,例如List<String>,其中每个元素必须匹配一个模式,您可以像这样使用container element constraints

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

也有可能在控制器方法参数上使用@Valid(必须为Bean类型)还要放置{{1} }。在这种情况下,您可以“免费”获得适当,详细的HTTP 400响应,即无需自定义@Validated。但这不应用级联验证,因此您无法验证类似@ExceptionHandler之类的东西,也不支持容器元素约束。

最后,您可以将后一种方法与类型为@Valid List<SomeBean> beans的方法中添加的额外参数结合使用。在验证错误的情况下,这不会触发自动错误响应,但是您必须自己在方法主体中检查注入的BindingResult并采取相应的措施(这样可以提供更大的灵活性)。 this综合答案中对此进行了描述。

答案 8 :(得分:0)

我正在使用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;
}

全局异常处理程序方法(ApiError类型不是我的设计)

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

答案 9 :(得分:0)

使用 @Validated 注释控制器
使用 @有效注释@RequestBody

答案 10 :(得分:0)

创建实体类:

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

答案 11 :(得分:0)

@Valid批注可在菱形运算符内部使用:

private List<@Valid MyType> types;

@Valid
private List<MyType> types;

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

答案 12 :(得分:0)

(此答案在科特林中,对于 Java 请参见https://stackoverflow.com/a/64061936

对于使用 kotlin jackson 的用户,这是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: [...]"
}

答案 13 :(得分:0)

使用 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 数据列表:-

注意 name 在第二个 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 doc says:-

17.验证

只要 JSR-303 实现(例如 Hibernate 验证器)在类路径上,Bean Validation 1.1 支持的方法验证功能就会自动启用。这让 bean 方法可以使用 javax.validation 对其参数和/或返回值的约束进行注释。 具有此类注释方法的目标类需要在类型级别使用@Validated 注释进行注释,以便在其方法中搜索内联约束注释。

答案 14 :(得分:0)

使用更高版本的 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 注释位于通用参数中。 但是,如果您想验证整个列表,您仍然需要将其包装在一个对象中..