Spring Boot Validate JSON Mapped через ObjectMapper GET @RequestParam

Какой самый простой подход к проверке сложного объекта JSON, передаваемого в контроллер 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 на контроллере, а также @Valid в @RequestParam, и ничто не заставляет проверки курок.


person Always Learning    schedule 20.02.2018    source источник
comment
Как насчет Hibernate Validator? Вы можете скормить ему аннотацию productRequest с вашими любимыми аннотациями валидатора спящего режима, прежде чем выполнять какие-либо действия с ним.   -  person Raf    schedule 20.02.2018


Ответы (2)


С моей точки зрения, Hibernate Bean Validator, вероятно, является одним из наиболее удобных методов проверки annotated полей bean-компонента в любое время и в любом месте. Это как setup и forget

  • Настройте валидатор компонентов Hibernate
  • Настройте, как должна выполняться проверка
  • Запускать валидатор на bean-компоненте в любом месте

Я выполнил инструкции в документации, приведенной здесь

Установить зависимости

Я использую Gradle, поэтому я собираюсь добавить необходимые зависимости, как показано ниже.

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

Создайте универсальный вальдиатор фасоли

Я настраиваю интерфейс валидатора 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);
        }
    }
}

Класс исключения

ValidationsFatalException, который я использовал выше, является настраиваемым классом исключений, расширяющим RuntimeException. Как видите, я передаю сообщение и список 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, как показано ниже.
  • Создайте класс ProductRequest, как показано выше
  • Я аннотировал productId @NotNull и @Length(min=5, max=10)
  • Я использовал Postman, чтобы сделать GET запрос с params, имеющим значение url-encoded тело json

Предполагая, что CustomBeanValidator автоматически подключается к контроллеру, запустите проверку, как показано ниже, после создания объекта productRequest.

beanValidator.validateFields(productRequest);

Вышеупомянутое вызовет исключение, если какие-либо нарушения на основе использованных аннотаций.

Как исключение обрабатывается контроллером исключений?

Как упоминалось в заголовке, я использую ExceptionController для обработки исключений в моем приложении.

Вот как скелет моего exception handler, где ValidationsFatalException отображается, а затем я обновляю сообщение и устанавливаю желаемый код состояния на основе типа исключения и возвращаю настраиваемый объект (то есть 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.

person Raf    schedule 20.02.2018
comment
Отлично! Пара вещей, так как у меня это почти работает, как вы, но не совсем так. Можете ли вы включить код для RuntimeException? Я получаю код статуса 500, как вы получили 400? Как вы заполнили поле сведений в ответе об ошибке? Также пара потенциальных мелких правок; похоже, что имя конструктора неверно в вашем примере кода CustomBeanValidatorImpl? Наконец, я полагаю, вы имеете в виду, что расширяет RuntimeException, а не реализует RuntimeException? Спасибо! - person Always Learning; 20.02.2018
comment
Да я имел ввиду extend не реализовывать. 1) Добавлен класс Exception 2) Добавлена ​​конечная точка Exception Controller, как исключение обрабатывается и преобразуется в Bad Request Вы также можете использовать Spring AOP, если не хотите использовать Hibernate Bean Validator, хотя валидатор bean-компонентов - это способ войти моя точка зрения. - person Raf; 20.02.2018

Вы имеете в виду что-то подобное?

@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;
    }
}
person Aly Saleh    schedule 20.02.2018