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 创建自定义约束违规和 属性 路径来执行相同的操作
我正在使用 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
.