Spring 引导验证 JSON 通过 ObjectMapper GET @RequestParam 映射

Spring Boot Validate JSON Mapped via ObjectMapper GET @RequestParam

验证复杂 JSON 对象被传递到 spring 引导中的 GET REST 控制器的最简单方法是什么,我正在使用 com.fasterxml.jackson.databind.ObjectMapper?

这是控制器:

@RestController
@RequestMapping("/products")
public class ProductsController {

@GetMapping
public ProductResponse getProducts(
        @RequestParam(value = "params") String requestItem
) throws IOException {
    final ProductRequest productRequest =
            new ObjectMapper()
                    .readValue(requestItem, ProductRequest.class);

    return productRetriever.getProductEarliestAvailabilities(productRequest);
}}

我要验证的DTO请求对象:

public class ProductRequest {
private String productId;

public String getProductId() {
    return productId;
}

public void setProductId(String productId) {
    this.productId = productId;
}}

我正在考虑在请求 DTO 上使用注释,但是当我这样做时,它们不会触发任何类型的异常,即 @NotNull。我尝试了在控制器中使用 @Validated 以及在 @RequestParam[=27 中使用 @Valid 的各种组合=] 并且没有任何事情导致验证触发。

在我看来,Hibernate Bean Validator可能是随时随地验证bean的annotated字段最方便的方法之一。就像 setupforget

  • 设置 Hibernate Bean 验证器
  • 配置验证的方式
  • 在任何地方触发 bean 上的验证器

我按照给定的文档中的说明进行操作 here

设置依赖项

我使用 Gradle 所以,我将添加所需的依赖项,如下所示

// Hibernate Bean validator
compile('org.hibernate:hibernate-validator:5.2.4.Final')

创建通用 bean valdiator

我按照文档中的描述设置了一个 bean 验证器接口,然后使用它来验证所有被注释的东西

public interface CustomBeanValidator {
    /**
     * Validate all annotated fields of a DTO object and collect all the validation and then throw them all at once.  
     * 
     * @param object
     */
    public <T> void validateFields(T object); 
}

实现上面的接口如下

@Component
public class CustomBeanValidatorImpl implements CustomBeanValidator {
    ValidatorFactory valdiatorFactory = null; 

    public CustomBeanValidatorImpl() {
        valdiatorFactory = Validation.buildDefaultValidatorFactory(); 
    }

    @Override
    public <T> void validateFields(T object) throws ValidationsFatalException {
        Validator validator = valdiatorFactory.getValidator(); 
        Set<ConstraintViolation<T>> failedValidations = validator.validate(object); 

        if (!failedValidations.isEmpty()) {
            List<String> allErrors = failedValidations.stream().map(failure -> failure.getMessage())
                    .collect(Collectors.toList());
            throw new ValidationsFatalException("Validation failure; Invalid request.", allErrors);
        }
    }
}

异常class

我上面使用的ValidationsFatalException是一个扩展RuntimeException的自定义异常class。如您所见,我正在传递一条消息和一个 violations 列表,以防 DTO 出现多个验证错误。

public class ValidationsFatalException extends RuntimeException {
    private String message; 
    private Throwable cause;
    private List<String> details; 

    public ValidationsFatalException(String message, Throwable cause) {
        super(message, cause);
    } 

    public ValidationsFatalException(String message, Throwable cause, List<String> details) {
        super(message, cause); 
        this.details = details;
    }

    public List<String> getDetails() {
        return details; 
    }
}

场景模拟

为了测试这是否有效,我确实使用了您的代码进行测试,这就是我所做的

  • 如上所示创建端点
  • 自动连接 CustomBeanValidator 并触发它的 validateFields 方法,将 productRequest 传递给它,如下所示
  • 创建一个ProductRequestclass如上图
  • 我用 @NotNull@Length(min=5, max=10)
  • 注释了 productId
  • 我使用 Postman 发出 GET 请求,其中 params 的值为 url-encoded json body

假设CustomBeanValidator在controller中自动装配,在构建productRequestobject后触发验证如下。

beanValidator.validateFields(productRequest);

如果有任何基于使用的注释的违规行为,以上将抛出异常。

异常控制器如何处理异常?

如标题所述,我使用ExceptionController来处理我的应用程序中的异常。

这是 exception handler 的框架,ValidationsFatalException 映射到该框架,然后我更新消息并根据异常类型和 return 自定义设置我想要的状态代码object(即您在下面看到的 json)

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({SomeOtherException.class, ValidationsFatalException.class})
public @ResponseBody Object handleBadRequestExpection(HttpServletRequest req, Exception ex) {
    if(ex instanceof CustomBadRequestException) 
        return new CustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage()); 
    else 
        return new DetailedCustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage(),((ValidationsFatalException) ex).getDetails()); 
}

测试 1

原始 params = {"productId":"abc123"}
Url 编码 parmas = %7B%22productId%22%3A%22abc123%22%7D
最终 URL:http://localhost:8080/app/product?params=%7B%22productId%22%3A%22abc123%22%7D
结果:一切顺利。

测试 2

原始 params = {"productId":"ab"}
Url 编码 parmas = %7B%22productId%22%3A%22ab%22%7D
最终 URL:http://localhost:8080/app/product?params=%7B%22productId%22%3A%22ab%22%7D
结果:

{
    "statusCode": 400,
    "status": "BAD_REQUEST",
    "message": "Validation failure; Invalid request.",
    "details": [
        "length must be between 5 and 10"
    ]
}

您可以扩展 Validator 实现以提供 field vs message 错误消息的映射。

你的意思是这样的吗?

@RequestMapping("/products")
public ResponseEntity getProducts(
        @RequestParam(value = "params") String requestItem) throws IOException {

    ProductRequest request = new ObjectMapper().
            readValue(requestItem, ProductRequest.class);

    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();

    Validator validator = factory.getValidator();
    Set<ConstraintViolation<ProductRequest>> violations
            = validator.validate(request);

    if (!violations.isEmpty()) {
        return ResponseEntity.badRequest().build();
    }
    return ResponseEntity.ok().build();
}



public class ProductRequest {
    @NotNull
    @Size(min = 3)
    private String id;

    public String getId() {
        return id;
    }

public String setId( String id) {
    return this.id = id;
    }
}