Spring 数据 REST 中存在验证错误的空消息

Empty messages with Validation errors in Spring Data REST

我正在使用 Spring Boot、Spring Data REST、Spring HATEOAS、Hibernate、Spring Validation 创建应用程序。

我在 this guide.

之后创建了自己的验证来支持 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;
    }

}

关于配置我也有这个 class:

@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 错误(此 return 代码正确)。

我必须指出我创建了 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 部分。我调试了请求并检查了 class 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).]

我看不出我哪里出错了。对于其他(也)自定义验证器,我看到了正确的消息。我也有人给我指出正确的方向来解决问题吗?

我认为 Spring MVC 不知道在哪里显示错误消息,因为 class 级约束的约束违反没有指示任何特定的 属性。

HV 的 @ScriptAssert 提供了 reportOn() 属性,用于指定要报告错误的 属性。

对于您的自定义约束,您可以通过使用通过 ConstraintValidatorContext.

公开的 API 创建自定义约束违规和 属性 路径来执行相同的操作