Пустые сообщения с ошибками проверки в Spring Data REST

Я создаю приложение, используя Spring Boot, Spring Data REST, Spring HATEOAS, Hibernate, Spring Validation.

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

Итак, у меня есть мой валидатор:

  public class SpELClassValidator implements ConstraintValidator<ValidateClassExpression, Object> {
    private Logger log = LogManager.getLogger();

    private ValidateClassExpression annotation;
    private ExpressionParser parser = new SpelExpressionParser();

    public void initialize(ValidateClassExpression constraintAnnotation) {
        annotation = constraintAnnotation;
        parser.parseExpression(constraintAnnotation.value());
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {
        try {           
            StandardEvaluationContext spelContext = new StandardEvaluationContext(value);
            return (Boolean) parser.parseExpression(annotation.value()).getValue(spelContext);
        } catch (Exception e) {
            log.error("", e);
            return false;
        }

    }
}

и моя аннотация:

@Target({ java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { SpELClassValidator.class })
@Documented
@Repeatable(ValidateClassExpressions.class)
public @interface ValidateClassExpression {

    String message() default "{expression.validation.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String value();

}

Конфигурация валидатора:

@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.setBasenames("classpath:/i18n/messages");
    // messageSource.setDefaultEncoding("UTF-8");
    // set to true only for debugging
    messageSource.setUseCodeAsDefaultMessage(false);
    messageSource.setCacheSeconds((int) TimeUnit.HOURS.toSeconds(1));
    messageSource.setFallbackToSystemLocale(false);
    return messageSource;
}

/**
 * Enable Spring bean validation
 * https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation
 * 
 * @return
 */
@Bean   
public LocalValidatorFactoryBean validator() {
    LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
    factoryBean.setValidationMessageSource(messageSource());
    return factoryBean;
}

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
    MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
    methodValidationPostProcessor.setValidator(validator());
    return methodValidationPostProcessor;
}

..и определили валидатор для репозиториев REST:

    @Configuration
public class RestConfig extends RepositoryRestConfigurerAdapter {
    @Autowired
    private Validator validator;

    public static final DateTimeFormatter ISO_FIXED_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
            .withZone(ZoneId.of("Z"));

    @Bean
    public RootResourceProcessor rootResourceProcessor() {
        return new RootResourceProcessor();
    }

    @Override
    public void configureExceptionHandlerExceptionResolver(ExceptionHandlerExceptionResolver exceptionResolver) {

    }

    @Override
    public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
        validatingListener.addValidator("beforeCreate", validator);
        validatingListener.addValidator("beforeSave", validator);
        super.configureValidatingRepositoryEventListener(validatingListener);
    }
}

это мой боб:

    @Entity
// Validate the number of seats if the bus is a minibus
@ValidateClassExpression(value = "#this.isMiniBus() == true ? #this.getSeats()<=17 : true", message = "{Expression.licenseplate.validminibus}")
public class LicensePlate extends AbstractEntity {
    private static final long serialVersionUID = -6871697166535810224L;

    @NotEmpty
    @ColumnTransformer(read = "UPPER(licensePlate)", write = "UPPER(?)")
    @Column(nullable = false, unique = true)
    private String licensePlate;

    // The engine euro level (3,4,5,6)
    @Range(min = 0, max = 6)
    @NotNull
    @Column(nullable = false, columnDefinition = "INTEGER default 0")
    private int engineEuroLevel = 0;

    @NotNull(message = "{NotNull.licenseplate.enginetype}")
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private EngineType engineType = EngineType.DIESEL;

    // If the bus has the particulate filter
    @NotNull(message = "{NotNull.licenseplate.particulatefilter}")
    @Column(nullable = false, columnDefinition = "BOOLEAN default false")
    private boolean particulateFilter = false;

    // Number of seats
    @NotNull
    @Range(min = 1, max = 99)
    @Column(nullable = false, columnDefinition = "INTEGER default 50")
    private int seats = 50;

    // If the vehicle is a minibus
    @NotNull
    @Column(nullable = false, columnDefinition = "BOOLEAN default false")
    private boolean miniBus = false;

    @NotNull(message = "{NotNull.licenseplate.country}")
    // The country of the vehicle
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Country country;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Note> notes = new ArrayList<>();

    public LicensePlate() {
    }

    public String getLicensePlate() {
        return licensePlate;
    }

    public void setLicensePlate(String licensePlate) {
        this.licensePlate = licensePlate;
    }

    public int getEngineEuroLevel() {
        return engineEuroLevel;
    }

    public void setEngineEuroLevel(int engineEuroLevel) {
        this.engineEuroLevel = engineEuroLevel;
    }

    public int getSeats() {
        return seats;
    }

    public void setSeats(int seats) {
        this.seats = seats;
    }

    public boolean isMiniBus() {
        return miniBus;
    }

    public void setMiniBus(boolean miniBus) {
        this.miniBus = miniBus;
    }

    public EngineType getEngineType() {
        return engineType;
    }

    public void setEngineType(EngineType engineType) {
        this.engineType = engineType;
    }

    public boolean isParticulateFilter() {
        return particulateFilter;
    }

    public void setParticulateFilter(boolean particulateFilter) {
        this.particulateFilter = particulateFilter;
    }

    public Country getCountry() {
        return country;
    }

    public void setCountry(Country country) {
        this.country = country;
    }

    @Override
    public String toString() {
        return "LicensePlate [licensePlate=" + licensePlate + ", engineEuroLevel=" + engineEuroLevel + ", engineType="
                + engineType + ", particulateFilter=" + particulateFilter + ", seats=" + seats + ", miniBus=" + miniBus
                + "]";
    }

    public List<Note> getNotes() {
        return notes;
    }

    public void setNotes(List<Note> notes) {
        this.notes = notes;
    }

}

В конфигурации у меня также есть этот класс:

@RestControllerAdvice
public class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
        throw new RuntimeException(ex);
    }

    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
        throw new RuntimeException(ex);
    }

    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
        throw new RuntimeException(ex);
    }

}

Использование моего репозитория:

@Transactional
@RepositoryRestResource(excerptProjection = LicensePlateProjection.class)
@PreAuthorize("isAuthenticated()")
public interface LicensePlateRepository
        extends PagingAndSortingRepository<LicensePlate, Long>, RevisionRepository<LicensePlate, Long, Integer> {

    public LicensePlate findByLicensePlate(String licencePlate);

Используя Swagger, я делаю POST этого json:

{"licensePlate":"asdfg","engineEuroLevel":"4","particulateFilter":true,"seats":18,"miniBus":true,"country":"http://localhost:8080/api/v1/countries/1"}

Поскольку у меня есть правило проверки, которое проверяет, что микроавтобус имеет менее 17 мест, я должен увидеть ошибку проверки, вместо этого я вижу это:

 {
  "errors": []
}

с ошибкой HTTP 400 (это правильный код возврата).

Я должен указать, что я создал тестовые примеры Junit и вижу правильное сообщение:

@Test
@WithMockUser(roles = "ADMIN")
public void validateMinibusWithMoreThan17SeatsFails() {
    assertEquals(1, countryRepository.count());

    LicensePlate plate = new LicensePlate();
    plate.setLicensePlate("AA123BB");
    plate.setEngineEuroLevel(3);
    plate.setMiniBus(true);
    plate.setSeats(18);
    plate.setCountry(countryRepository.findFirstByOrderByIdAsc());

    Set<ConstraintViolation<LicensePlate>> constraintViolations = validator.validate(plate);
    assertEquals(1, constraintViolations.size());
    ConstraintViolation<LicensePlate> constraintViolation = constraintViolations.iterator().next();
    assertEquals("I veicoli di tipo minibus possono avere al massimo 17 posti (16 passeggeri più il conducente).",
            constraintViolation.getMessage());
}

Итак, я думаю, проблема в части REST/MVC. Я отладил запрос и проверил класс org.springframework.data.rest.core.RepositoryConstraintViolationException; в конструкторе я вижу, что мои ошибки верны, и я вижу сообщение об ошибке и правильную структуру:

org.springframework.data.rest.core.ValidationErrors: 1 errors
Error in object 'LicensePlate': codes [ValidateClassExpression.LicensePlate,ValidateClassExpression]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [LicensePlate.,]; arguments []; default message [],org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@520b6a25]; default message [I veicoli di tipo minibus possono avere al massimo 17 posti (16 passeggeri più il conducente).]

Я не вижу, где я делаю ошибку. С другими (также) пользовательскими валидаторами я вижу правильное сообщение. Я кто-то также направить меня в правильном направлении, чтобы решить проблему?


person drenda    schedule 10.10.2017    source источник
comment
Привет! ) Вы зарегистрировали свой валидатор? Разве вы не хотите увидеть мой пример проверки SDR?..   -  person Cepr0    schedule 10.10.2017
comment
@ Cepr0 Да, я сделал. Я обновил свой вопрос. Я проверил ваш пример и, похоже, делаю то же самое.   -  person drenda    schedule 10.10.2017
comment
Как я вижу, вы зарегистрировали только стандартный валидатор (@Autowired private Validator validator;), а не пользовательский: validatingListener.addValidator("beforeSave", new SpELClassValidator());. Или я ошибаюсь?..   -  person Cepr0    schedule 10.10.2017
comment
Хм, я не думаю, что мне нужно это делать. Мой валидатор (javax.validation.ConstraintValidator) используется из моей конкретной аннотации ValidateClassExpression. Я не могу установить в validatingListener, потому что он имеет другой тип. Кроме того, у меня есть другие пользовательские валидаторы, и они отлично работают (я имею в виду ответное сообщение).   -  person drenda    schedule 10.10.2017
comment
Работает ли он с другими пользовательскими ограничениями class-level, например. Hibernate Validator @ScriptAssert?   -  person Gunnar    schedule 15.10.2017
comment
@Gunnar на самом деле ты прав. У меня такая же проблема с ScriptAssert. Поскольку вы указали, что я надеюсь, что у вас также есть хороший совет, чтобы решить эту проблему ;-)   -  person drenda    schedule 15.10.2017
comment
Я думаю, я сделаю. Смотрите мой ответ ниже :)   -  person Gunnar    schedule 15.10.2017


Ответы (1)


Я считаю, что Spring MVC не знает, где показать сообщение об ошибке, поскольку нарушение ограничения ограничения уровня класса не указывает на какое-либо конкретное свойство.

@ScriptAssert HV предоставляет reportOn() для указания свойства, для которого необходимо сообщить об ошибке.

Для вашего пользовательского ограничения вы можете сделать то же самое, создав настраиваемое нарушение ограничения и путь к свойству, используя API, доступный через ConstraintValidatorContext.

person Gunnar    schedule 15.10.2017
comment
Ты прав. Я также догадался, что без определенного свойства Spring mvc может отображать ошибки CoinstraintValidation. Кстати, мне пришлось обновить Hibernate Validator до версии = 5.4, потому что reportOn() доступен только из этого выпуска. - person drenda; 15.10.2017