将 @Future 和 LocalDate 的自定义 ConstraintValidator 添加到 Spring 引导项目
Adding custom ConstraintValidator for @Future and LocalDate to a Spring Boot project
我有一个用于输入日期的 Spring MVC 表单,输入被发送到控制器并通过标准 Spring MVC 验证进行验证。
型号:
public class InvoiceForm {
@Future
private LocalDate invoicedate;
}
控制器:
public String postAdd(@Valid @ModelAttribute InvoiceForm invoiceForm, BindingResult result) {
....
}
提交表单时出现以下错误:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.Future' validating type 'java.time.LocalDate'. Check configuration for 'invoicedate'
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.throwExceptionForNullValidator(ConstraintTree.java:229) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getConstraintValidatorNoUnwrapping(ConstraintTree.java:310) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getConstraintValidatorInstanceForAutomaticUnwrapping(ConstraintTree.java:244) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getInitializedConstraintValidator(ConstraintTree.java:163) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:116) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:87) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:73) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:617) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraint(ValidatorImpl.java:580) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:524) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:492) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:457) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:407) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:205) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:108) ~[spring-context-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.validation.DataBinder.validate(DataBinder.java:866) ~[spring-context-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.validateIfApplicable(ModelAttributeMethodProcessor.java:164) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:111) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:99) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:832) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:743) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:961) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:869) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-embed-websocket-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:316) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:126) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:114) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:122) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at com.balticfinance.jwt.StatelessAuthenticationFilter.doFilter(StatelessAuthenticationFilter.java:46) ~[classes/:na]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:676) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:528) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1099) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:670) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1520) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1476) [tomcat-embed-core-8.0.36.jar:8.0.36]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_91]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_91]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.0.36.jar:8.0.36]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_91]
我为此案例实施了自己的 ConstraintValidator
。但是不知何故 Spring Boot 没有选择它进行验证。
@Component
public class LocalDateFutureValidator implements ConstraintValidator<Future, LocalDate> {
@Override
public void initialize(Future future) {
}
@Override
public boolean isValid(LocalDate localDate, ConstraintValidatorContext constraintValidatorContext) {
LocalDate today = LocalDate.now();
return localDate.isEqual(today) || localDate.isAfter(today);
}
}
我知道我可以简单地编写自己的注解并在那里指定验证器,但难道没有更简洁、更简单的方法吗?
您需要将自己的验证器添加到 META-INF/validation.xml
文件中,如下所示:
<constraint-mappings
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.1.xsd"
xmlns="http://jboss.org/xml/ns/javax/validation/mapping" version="1.1">
<constraint-definition annotation="javax.validation.constraints.Future">
<validated-by include-existing-validators="true">
<value>package.to.LocalDateFutureValidator</value>
</validated-by>
</constraint-definition>
</constraint-mappings>
我同意 Miloš 的观点,使用 META-INF/validation.xml
可能是最简洁、最简单的方法,但如果您真的想将其设置为 Spring @Confguration
class 那么这是可能的,这是您可以做到的一种方法。
Spring 引导的美妙之处在于,它会为您进行大量配置,因此您不必担心。但是,当您想自己专门配置某些东西时,这也会导致问题,而如何做却不是很明显。
所以昨天我开始尝试使用 Hardy 建议的 ConstraintDefinitionContributor
机制为 @Past
和 LocalDate
添加 CustomerValidator
文档)。
最简单的一点是编写实现 class 来进行验证,对于我非常具体的目的,它包括:
public class PastValidator implements ConstraintValidator<Past, LocalDate> {
@Override
public void initialize(Past constraintAnnotation) {}
@Override
public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
return null != value && value.isBefore(LocalDate.now());
}
}
然后我偷懒了,只是在我的配置中实例化了一个 @Bean
,就在 Spring 的 auto-configuration class 之一的 off-chance 上es 只会将其拾取并将其连接到 Hibernate 验证器中。考虑到可用的文档(或缺乏文档)以及 Hardy 和其他人所说的话,这是一个很长的尝试,但没有得到回报。
所以我启动了一个调试器并从 org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree
中抛出的异常向后工作,该异常告诉我它找不到 @Past
和 LocalDate
的验证器。
查看 ConstraintValidatorFactory
的类型层次结构,我发现在我的 Spring MVC 应用程序中有两个实现 classes:SpringConstraintValidatorFactory
和 SpringWebConstraintValidatorFactory
两者都只是尝试从正确的上下文中获取一个 bean class。这告诉我,我必须让我的验证器注册到 Spring 的 BeanFactory
,但是当我在此设置断点但它没有被我的 PastValidator
击中时,它意味着 Hibernate 甚至不知道它应该请求这个 class.
这是有道理的:没有任何 ConstraintDefinitionContributor
可以告诉 Hibernate 它需要向 Spring 询问 PastValidator 的实例。文档中的示例位于
http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html_single/#section-constraint-definition-contributor
建议我需要访问 HibernateValidatorConfiguration
所以我只需要找到 Spring 在哪里进行配置。
经过一些挖掘,我发现这一切都发生在 Spring 的 LocalValidatorFactoryBean
class 中,特别是在它的 afterPropertiesSet()
方法中。从它的 javadoc:
/*
* This is the central class for {@code javax.validation} (JSR-303) setup in a Spring
* application context: It bootstraps a {@code javax.validation.ValidationFactory} and
* exposes it through the Spring {@link org.springframework.validation.Validator} interface
* as well as through the JSR-303 {@link javax.validation.Validator} interface and the
* {@link javax.validation.ValidatorFactory} interface itself.
*/
基本上,如果您不设置和配置自己的验证器,那么这就是 Spring 尝试为您做的地方,并且在真正的 Spring 风格中它提供了一个方便的扩展方法这样您就可以让它进行配置,然后将您自己的配置添加到组合中。
所以我的解决方案是扩展 LocalValidatorFactoryBean
以便我能够注册自己的 ConstraintDefinitionContributor
个实例:
import java.util.ArrayList;
import java.util.List;
import javax.validation.Configuration;
import org.hibernate.validator.internal.engine.ConfigurationImpl;
import org.hibernate.validator.spi.constraintdefinition.ConstraintDefinitionContributor;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
public class ConstraintContributingValidatorFactoryBean extends LocalValidatorFactoryBean {
private List<ConstraintDefinitionContributor> contributors = new ArrayList<>();
public void addConstraintDefinitionContributor(ConstraintDefinitionContributor contributor) {
contributors.add(contributor);
}
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
if(configuration instanceof ConfigurationImpl) {
ConfigurationImpl config = ConfigurationImpl.class.cast(configuration);
for(ConstraintDefinitionContributor contributor : contributors)
config.addConstraintDefinitionContributor(contributor);
}
}
}
然后在我的 Spring 配置中实例化和配置它:
@Bean
public ConstraintContributingValidatorFactoryBean validatorFactory() {
ConstraintContributingValidatorFactoryBean validatorFactory = new ConstraintContributingValidatorFactoryBean();
validatorFactory.addConstraintDefinitionContributor(new ConstraintDefinitionContributor() {
@Override
public void collectConstraintDefinitions(ConstraintDefinitionBuilder builder) {
builder.constraint( Past.class )
.includeExistingValidators( true )
.validatedBy( PastValidator.class );
}
});
return validatorFactory;
}
为了完整起见,这里也是我用 PastValidator
bean 实例化的地方:
@Bean
public PastValidator pastValidator() {
return new PastValidator();
}
其他springy-thingies
我在调试中注意到,因为我有一个相当大的 Spring MVC 应用程序,所以我看到了 SpringConstraintValidatorFactory
的两个实例和 SpringWebConstraintValidatorFactory
的一个实例。我找到了后者
在验证期间从未使用过,所以我暂时忽略了它。
Spring 也有一个机制来决定使用哪个 ValidatorFactory
的实现,所以它可能不使用你的 ConstraintContributingValidatorFactoryBean
而是使用
别的东西(抱歉,我昨天找到了 class,但今天找不到了,虽然我只花了大约 2 分钟找)。如果您以任何一种 non-trivial 方式使用 Spring MVC
那么您可能已经不得不编写自己的配置 class,例如实现 WebMvcConfigurer
的配置,您可以在其中显式连接 Validator
bean:
public static class MvcConfigurer implements WebMvcConfigurer {
@Autowired
private ConstraintContributingValidatorFactoryBean validatorFactory;
@Override
public Validator getValidator() {
return validatorFactory;
}
// ...
// <snip>lots of other overridden methods</snip>
// ...
}
这是错误的
正如已经指出的那样,您应该警惕对 LocalDate
应用 @Past
验证,因为没有时区信息。但是,如果您使用 LocalDate
因为所有内容都将 运行 在同一时区,或者您故意要忽略时区,或者您根本不在乎,那么这对您来说没问题.
万一有人对 validation.xml
方法有问题并且出现 Cannot find the declaration of element constraint-mappings
错误,就像我一样,我必须进行以下修改。希望这会节省一些我花在解决这个问题上的时间。
META-INF/validation.xml
:
<validation-config
xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://jboss.org/xml/ns/javax/validation/configuration
validation-configuration-1.1.xsd"
version="1.1">
<constraint-mapping>META-INF/validation/past.xml</constraint-mapping>
</validation-config>
META-INF/validation/past.xml
:
<constraint-mappings
xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://jboss.org/xml/ns/javax/validation/mapping
validation-mapping-1.1.xsd"
version="1.1">
<constraint-definition annotation="javax.validation.constraints.Past">
<validated-by include-existing-validators="false">
<value>your.package.PastConstraintValidator</value>
</validated-by>
</constraint-definition>
</constraint-mappings>
Hibernate 6.0 添加了对 @Future
和 LocalDate
值的内置支持,根据 FutureValidatorForLocalDate
。因此,如果代码升级到 Hibernate 6.0 或更高版本,它应该开箱即用。
可以使用自定义映射配置自定义 LocalValidatorFactoryBean
bean,然后连接为 Spring Bean,替换默认的自动连接 Spring 引导验证程序:
@Configuration
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean defaultValidator() {
return new CustomValidatorFactoryBean();
}
private static class CustomValidatorFactoryBean extends LocalValidatorFactoryBean {
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
HibernateValidatorConfiguration hibernateConfiguration = (HibernateValidatorConfiguration) configuration;
ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
constraintMapping
.constraintDefinition(Future.class)
.validatedBy(LocalDateFutureValidator.class)
.includeExistingValidators(true);
hibernateConfiguration.addMapping(constraintMapping);
}
}
}
请注意,Spring 不会自动注入或以其他方式自动将验证器作为 beans 处理,因此不需要 LocalDateFutureValidator
上的 @Component
注释,它没有完成任何事情,并且具有误导性.因此应将其删除。
另请注意,较新版本的 Hibernate Validator 默认具有 LocalDate
@Future
功能,因此 LocalDate
验证不再需要这种方法。但是,对于非内置类型的自定义 ConstraintValidator
,此方法仍然有用。
说明
首先,请注意 Spring 不会自动注入或以其他方式将验证器作为 beans 处理,因此不需要 LocalDateFutureValidator
上的 @Component
注释,它没有完成任何事情,并且是误导。因此应将其删除。
Hibernate Bean Validation reference guide has a section devoted to adding constraint definitions. It discusses two ways of adding constraint definitions: via Java's ServiceLoader
并以编程方式。对于这个答案,我将讨论如何以编程方式添加它们。
使用这种方法,您的新约束可以添加到 Hibernate 配置中,如下所示:
HibernateValidatorConfiguration hibernateConfiguration = [...];
ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
constraintMapping
.constraintDefinition(Future.class)
.validatedBy(LocalDateFutureValidator.class)
.includeExistingValidators(true);
hibernateConfiguration.addMapping(constraintMapping);
为此,我们需要修改应用程序验证器使用的 HibernateValidatorConfiguration
。 LocalValidatorFactoryBean
class 是 Spring 使用的 bean 工厂类型:
This is the central class for javax.validation
(JSR-303) setup in a Spring application context: It bootstraps a javax.validation.ValidationFactory
and exposes it through the Spring Validator
interface as well as through the JSR-303 Validator
interface and the ValidatorFactory
interface itself.
Spring Boot 通过每个 ValidationAutoConfiguration.defaultValidator()
的 defaultValidator
bean 提供了这种类型的 bean。这个 bean 定义用 @ConditionalOnMissingBean(Validator.class)
注释,这意味着您的 Boot 应用程序可以通过声明自己的 Validator
bean 来替换它,例如 LocalValidatorFactoryBean
.
要添加自定义映射,可以扩展 LocalValidatorFactoryBean
并覆盖受保护的 postProcessConfiguration
方法:
public class CustomValidatorFactoryBean extends LocalValidatorFactoryBean {
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
HibernateValidatorConfiguration hibernateConfiguration = (HibernateValidatorConfiguration) configuration;
ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
constraintMapping
.constraintDefinition(Future.class)
.validatedBy(LocalDateFutureValidator.class)
.includeExistingValidators(true);
hibernateConfiguration.addMapping(constraintMapping);
}
}
此时剩下的就是将它连接到 Spring 上下文中,以便它替换默认的 Validator
bean。这可以通过使用 @Configuration
class 和 @Bean
定义方法来完成,或者通过将 CustomValidatorFactoryBean
class 注释为 @Component
.
我有一个用于输入日期的 Spring MVC 表单,输入被发送到控制器并通过标准 Spring MVC 验证进行验证。
型号:
public class InvoiceForm {
@Future
private LocalDate invoicedate;
}
控制器:
public String postAdd(@Valid @ModelAttribute InvoiceForm invoiceForm, BindingResult result) {
....
}
提交表单时出现以下错误:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.Future' validating type 'java.time.LocalDate'. Check configuration for 'invoicedate'
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.throwExceptionForNullValidator(ConstraintTree.java:229) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getConstraintValidatorNoUnwrapping(ConstraintTree.java:310) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getConstraintValidatorInstanceForAutomaticUnwrapping(ConstraintTree.java:244) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getInitializedConstraintValidator(ConstraintTree.java:163) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:116) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:87) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:73) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:617) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraint(ValidatorImpl.java:580) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:524) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:492) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:457) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:407) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:205) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:108) ~[spring-context-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.validation.DataBinder.validate(DataBinder.java:866) ~[spring-context-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.validateIfApplicable(ModelAttributeMethodProcessor.java:164) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:111) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:99) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:832) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:743) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:961) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:869) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-embed-websocket-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:316) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:126) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:114) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:122) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at com.balticfinance.jwt.StatelessAuthenticationFilter.doFilter(StatelessAuthenticationFilter.java:46) ~[classes/:na]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:676) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:528) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1099) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:670) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1520) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1476) [tomcat-embed-core-8.0.36.jar:8.0.36]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_91]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_91]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.0.36.jar:8.0.36]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_91]
我为此案例实施了自己的 ConstraintValidator
。但是不知何故 Spring Boot 没有选择它进行验证。
@Component
public class LocalDateFutureValidator implements ConstraintValidator<Future, LocalDate> {
@Override
public void initialize(Future future) {
}
@Override
public boolean isValid(LocalDate localDate, ConstraintValidatorContext constraintValidatorContext) {
LocalDate today = LocalDate.now();
return localDate.isEqual(today) || localDate.isAfter(today);
}
}
我知道我可以简单地编写自己的注解并在那里指定验证器,但难道没有更简洁、更简单的方法吗?
您需要将自己的验证器添加到 META-INF/validation.xml
文件中,如下所示:
<constraint-mappings
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.1.xsd"
xmlns="http://jboss.org/xml/ns/javax/validation/mapping" version="1.1">
<constraint-definition annotation="javax.validation.constraints.Future">
<validated-by include-existing-validators="true">
<value>package.to.LocalDateFutureValidator</value>
</validated-by>
</constraint-definition>
</constraint-mappings>
我同意 Miloš 的观点,使用 META-INF/validation.xml
可能是最简洁、最简单的方法,但如果您真的想将其设置为 Spring @Confguration
class 那么这是可能的,这是您可以做到的一种方法。
Spring 引导的美妙之处在于,它会为您进行大量配置,因此您不必担心。但是,当您想自己专门配置某些东西时,这也会导致问题,而如何做却不是很明显。
所以昨天我开始尝试使用 Hardy 建议的 ConstraintDefinitionContributor
机制为 @Past
和 LocalDate
添加 CustomerValidator
文档)。
最简单的一点是编写实现 class 来进行验证,对于我非常具体的目的,它包括:
public class PastValidator implements ConstraintValidator<Past, LocalDate> {
@Override
public void initialize(Past constraintAnnotation) {}
@Override
public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
return null != value && value.isBefore(LocalDate.now());
}
}
然后我偷懒了,只是在我的配置中实例化了一个 @Bean
,就在 Spring 的 auto-configuration class 之一的 off-chance 上es 只会将其拾取并将其连接到 Hibernate 验证器中。考虑到可用的文档(或缺乏文档)以及 Hardy 和其他人所说的话,这是一个很长的尝试,但没有得到回报。
所以我启动了一个调试器并从 org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree
中抛出的异常向后工作,该异常告诉我它找不到 @Past
和 LocalDate
的验证器。
查看 ConstraintValidatorFactory
的类型层次结构,我发现在我的 Spring MVC 应用程序中有两个实现 classes:SpringConstraintValidatorFactory
和 SpringWebConstraintValidatorFactory
两者都只是尝试从正确的上下文中获取一个 bean class。这告诉我,我必须让我的验证器注册到 Spring 的 BeanFactory
,但是当我在此设置断点但它没有被我的 PastValidator
击中时,它意味着 Hibernate 甚至不知道它应该请求这个 class.
这是有道理的:没有任何 ConstraintDefinitionContributor
可以告诉 Hibernate 它需要向 Spring 询问 PastValidator 的实例。文档中的示例位于
http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html_single/#section-constraint-definition-contributor
建议我需要访问 HibernateValidatorConfiguration
所以我只需要找到 Spring 在哪里进行配置。
经过一些挖掘,我发现这一切都发生在 Spring 的 LocalValidatorFactoryBean
class 中,特别是在它的 afterPropertiesSet()
方法中。从它的 javadoc:
/*
* This is the central class for {@code javax.validation} (JSR-303) setup in a Spring
* application context: It bootstraps a {@code javax.validation.ValidationFactory} and
* exposes it through the Spring {@link org.springframework.validation.Validator} interface
* as well as through the JSR-303 {@link javax.validation.Validator} interface and the
* {@link javax.validation.ValidatorFactory} interface itself.
*/
基本上,如果您不设置和配置自己的验证器,那么这就是 Spring 尝试为您做的地方,并且在真正的 Spring 风格中它提供了一个方便的扩展方法这样您就可以让它进行配置,然后将您自己的配置添加到组合中。
所以我的解决方案是扩展 LocalValidatorFactoryBean
以便我能够注册自己的 ConstraintDefinitionContributor
个实例:
import java.util.ArrayList;
import java.util.List;
import javax.validation.Configuration;
import org.hibernate.validator.internal.engine.ConfigurationImpl;
import org.hibernate.validator.spi.constraintdefinition.ConstraintDefinitionContributor;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
public class ConstraintContributingValidatorFactoryBean extends LocalValidatorFactoryBean {
private List<ConstraintDefinitionContributor> contributors = new ArrayList<>();
public void addConstraintDefinitionContributor(ConstraintDefinitionContributor contributor) {
contributors.add(contributor);
}
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
if(configuration instanceof ConfigurationImpl) {
ConfigurationImpl config = ConfigurationImpl.class.cast(configuration);
for(ConstraintDefinitionContributor contributor : contributors)
config.addConstraintDefinitionContributor(contributor);
}
}
}
然后在我的 Spring 配置中实例化和配置它:
@Bean
public ConstraintContributingValidatorFactoryBean validatorFactory() {
ConstraintContributingValidatorFactoryBean validatorFactory = new ConstraintContributingValidatorFactoryBean();
validatorFactory.addConstraintDefinitionContributor(new ConstraintDefinitionContributor() {
@Override
public void collectConstraintDefinitions(ConstraintDefinitionBuilder builder) {
builder.constraint( Past.class )
.includeExistingValidators( true )
.validatedBy( PastValidator.class );
}
});
return validatorFactory;
}
为了完整起见,这里也是我用 PastValidator
bean 实例化的地方:
@Bean
public PastValidator pastValidator() {
return new PastValidator();
}
其他springy-thingies
我在调试中注意到,因为我有一个相当大的 Spring MVC 应用程序,所以我看到了 SpringConstraintValidatorFactory
的两个实例和 SpringWebConstraintValidatorFactory
的一个实例。我找到了后者
在验证期间从未使用过,所以我暂时忽略了它。
Spring 也有一个机制来决定使用哪个 ValidatorFactory
的实现,所以它可能不使用你的 ConstraintContributingValidatorFactoryBean
而是使用
别的东西(抱歉,我昨天找到了 class,但今天找不到了,虽然我只花了大约 2 分钟找)。如果您以任何一种 non-trivial 方式使用 Spring MVC
那么您可能已经不得不编写自己的配置 class,例如实现 WebMvcConfigurer
的配置,您可以在其中显式连接 Validator
bean:
public static class MvcConfigurer implements WebMvcConfigurer {
@Autowired
private ConstraintContributingValidatorFactoryBean validatorFactory;
@Override
public Validator getValidator() {
return validatorFactory;
}
// ...
// <snip>lots of other overridden methods</snip>
// ...
}
这是错误的
正如已经指出的那样,您应该警惕对 LocalDate
应用 @Past
验证,因为没有时区信息。但是,如果您使用 LocalDate
因为所有内容都将 运行 在同一时区,或者您故意要忽略时区,或者您根本不在乎,那么这对您来说没问题.
万一有人对 validation.xml
方法有问题并且出现 Cannot find the declaration of element constraint-mappings
错误,就像我一样,我必须进行以下修改。希望这会节省一些我花在解决这个问题上的时间。
META-INF/validation.xml
:
<validation-config
xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://jboss.org/xml/ns/javax/validation/configuration
validation-configuration-1.1.xsd"
version="1.1">
<constraint-mapping>META-INF/validation/past.xml</constraint-mapping>
</validation-config>
META-INF/validation/past.xml
:
<constraint-mappings
xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://jboss.org/xml/ns/javax/validation/mapping
validation-mapping-1.1.xsd"
version="1.1">
<constraint-definition annotation="javax.validation.constraints.Past">
<validated-by include-existing-validators="false">
<value>your.package.PastConstraintValidator</value>
</validated-by>
</constraint-definition>
</constraint-mappings>
Hibernate 6.0 添加了对 @Future
和 LocalDate
值的内置支持,根据 FutureValidatorForLocalDate
。因此,如果代码升级到 Hibernate 6.0 或更高版本,它应该开箱即用。
可以使用自定义映射配置自定义 LocalValidatorFactoryBean
bean,然后连接为 Spring Bean,替换默认的自动连接 Spring 引导验证程序:
@Configuration
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean defaultValidator() {
return new CustomValidatorFactoryBean();
}
private static class CustomValidatorFactoryBean extends LocalValidatorFactoryBean {
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
HibernateValidatorConfiguration hibernateConfiguration = (HibernateValidatorConfiguration) configuration;
ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
constraintMapping
.constraintDefinition(Future.class)
.validatedBy(LocalDateFutureValidator.class)
.includeExistingValidators(true);
hibernateConfiguration.addMapping(constraintMapping);
}
}
}
请注意,Spring 不会自动注入或以其他方式自动将验证器作为 beans 处理,因此不需要 LocalDateFutureValidator
上的 @Component
注释,它没有完成任何事情,并且具有误导性.因此应将其删除。
另请注意,较新版本的 Hibernate Validator 默认具有 LocalDate
@Future
功能,因此 LocalDate
验证不再需要这种方法。但是,对于非内置类型的自定义 ConstraintValidator
,此方法仍然有用。
说明
首先,请注意 Spring 不会自动注入或以其他方式将验证器作为 beans 处理,因此不需要 LocalDateFutureValidator
上的 @Component
注释,它没有完成任何事情,并且是误导。因此应将其删除。
Hibernate Bean Validation reference guide has a section devoted to adding constraint definitions. It discusses two ways of adding constraint definitions: via Java's ServiceLoader
并以编程方式。对于这个答案,我将讨论如何以编程方式添加它们。
使用这种方法,您的新约束可以添加到 Hibernate 配置中,如下所示:
HibernateValidatorConfiguration hibernateConfiguration = [...];
ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
constraintMapping
.constraintDefinition(Future.class)
.validatedBy(LocalDateFutureValidator.class)
.includeExistingValidators(true);
hibernateConfiguration.addMapping(constraintMapping);
为此,我们需要修改应用程序验证器使用的 HibernateValidatorConfiguration
。 LocalValidatorFactoryBean
class 是 Spring 使用的 bean 工厂类型:
This is the central class for
javax.validation
(JSR-303) setup in a Spring application context: It bootstraps ajavax.validation.ValidationFactory
and exposes it through the SpringValidator
interface as well as through the JSR-303Validator
interface and theValidatorFactory
interface itself.
Spring Boot 通过每个 ValidationAutoConfiguration.defaultValidator()
的 defaultValidator
bean 提供了这种类型的 bean。这个 bean 定义用 @ConditionalOnMissingBean(Validator.class)
注释,这意味着您的 Boot 应用程序可以通过声明自己的 Validator
bean 来替换它,例如 LocalValidatorFactoryBean
.
要添加自定义映射,可以扩展 LocalValidatorFactoryBean
并覆盖受保护的 postProcessConfiguration
方法:
public class CustomValidatorFactoryBean extends LocalValidatorFactoryBean {
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
HibernateValidatorConfiguration hibernateConfiguration = (HibernateValidatorConfiguration) configuration;
ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
constraintMapping
.constraintDefinition(Future.class)
.validatedBy(LocalDateFutureValidator.class)
.includeExistingValidators(true);
hibernateConfiguration.addMapping(constraintMapping);
}
}
此时剩下的就是将它连接到 Spring 上下文中,以便它替换默认的 Validator
bean。这可以通过使用 @Configuration
class 和 @Bean
定义方法来完成,或者通过将 CustomValidatorFactoryBean
class 注释为 @Component
.