将 @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>

更多详情,refer to the official documentation.

我同意 Miloš 的观点,使用 META-INF/validation.xml 可能是最简洁、最简单的方法,但如果您真的想将其设置为 Spring @Confguration class 那么这是可能的,这是您可以做到的一种方法。

Spring 引导的美妙之处在于,它会为您进行大量配置,因此您不必担心。但是,当您想自己专门配置某些东西时,这也会导致问题,而如何做却不是很明显。

所以昨天我开始尝试使用 Hardy 建议的 ConstraintDefinitionContributor 机制为 @PastLocalDate 添加 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 中抛出的异常向后工作,该异常告诉我它找不到 @PastLocalDate 的验证器。

查看 ConstraintValidatorFactory 的类型层次结构,我发现在我的 Spring MVC 应用程序中有两个实现 classes:SpringConstraintValidatorFactorySpringWebConstraintValidatorFactory 两者都只是尝试从正确的上下文中获取一个 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 添加了对 @FutureLocalDate 值的内置支持,根据 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);

为此,我们需要修改应用程序验证器使用的 HibernateValidatorConfigurationLocalValidatorFactoryBean 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.