【问题标题】:Adding custom ConstraintValidator for @Future and LocalDate to a Spring Boot project将 @Future 和 LocalDate 的自定义 ConstraintValidator 添加到 Spring Boot 项目
【发布时间】:2016-07-15 10:27:45
【问题描述】:

我有一个用于输入日期的 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);
    }
}

我知道我可以简单地编写自己的注释并在那里指定验证器,但没有更简洁的方法吗?

【问题讨论】:

  • @Component 非常没用,因为验证器不是由 spring 管理的。你的@Future 应该指定使用哪个验证器。如果您使用的是最新版本的休眠验证器,则支持开箱即用,您根本不需要自定义验证器。
  • 谢谢,从 hibernate.atlassian.net/browse/HV-874@Future 判断,从 hibernate-validator 5.2 开始应该支持 LocalDate。我实际上使用的是hibernate-validator 5.2.4.Final(我猜是spring-boot-starter-data-jpa自动拉的)。我正在使用 spring-boot-starter-parent 1.3.6.RELEASE
  • LocalDate 的验证器已被删除,也许我应该使用其他类型?! hibernate.atlassian.net/browse/HV-981
  • 那么我猜唯一可行的方法就是创建自己的验证器和注释。我不确定如何为默认注释之一注册自定义验证器。
  • 注册自定义验证器需要通过META-INF/validation.xml

标签: spring-boot validation spring-mvc bean-validation hibernate-validator


【解决方案1】:

我同意 Miloš 的观点,即使用 META-INF/validation.xml 可能是最干净和最简单的方法,但如果你真的想在 Spring @Confguration 类中设置它,那么这是可能的,这是你可以的一种方法去做吧。

Spring Boot 的美妙之处在于它代表您进行大量配置,因此您不必担心。但是,当您想自己专门配置某些东西并且不知道如何做时,这也可能会导致问题。

所以昨天我开始尝试使用 Hardy 建议的 ConstraintDefinitionContributor 机制为 @PastLocalDate 添加 CustomerValidator(并在 Hibernate 文档中提到)。

简单的一点是编写实现类来进行验证,对于我非常具体的目的而言,它包括:

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 的一个自动配置类可能会选择它并将其连接到 Hibernate 验证器。考虑到可用的文档(或缺乏文档)以及 Hardy 和其他人所说的,这是一个相当长的尝试,但没有得到回报。

所以我启动了一个调试器并从org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree 中抛出的异常向后工作,这告诉我它找不到@PastLocalDate 的验证器。

查看ConstraintValidatorFactory 的类型层次结构,我发现我的Spring MVC 应用程序中有两个实现类:SpringConstraintValidatorFactorySpringWebConstraintValidatorFactory,它们都只是尝试从正确类的上下文中获取一个bean。这告诉我,我必须让我的验证器注册到 Spring 的 BeanFactory,但是当我在此设置断点但它没有被我的 PastValidator 击中时,这意味着 Hibernate 不知道它应该甚至请求这个课程。

这是有道理的:没有任何 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 类中,特别是在它的 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();
    }

其他有弹性的东西

我在调试中注意到,因为我有一个相当大的 Spring MVC 应用程序,所以我看到了两个 SpringConstraintValidatorFactory 实例和一个 SpringWebConstraintValidatorFactory 实例。我找到了后者 在验证期间从未使用过,所以我暂时忽略了它。

Spring 还有一种机制来决定使用ValidatorFactory 的哪个实现,因此它可以不使用您的ConstraintContributingValidatorFactoryBean 而是使用 别的东西(对不起,我昨天找到了它做这个的课程,但今天找不到它,尽管我只花了大约 2 分钟的时间寻找)。如果您以任何重要的方式使用 Spring MVC 那么您可能已经不得不编写自己的配置类,例如实现 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>
    // ...
}

这是错误的

正如已经指出的那样,您应该警惕将@Past 验证应用于LocalDate,因为没有时区信息。但是,如果您使用 LocalDate 是因为一切都将在同一个时区运行,或者您有意忽略时区,或者您只是不关心,那么这对您来说很好。

【讨论】:

  • 请注意,ConstraintDefinitionContributor 仅存在于 5.2 中,后续版本的 Hibernate Validator 使用类似但略有不同的方法。
【解决方案2】:

您需要将自己的验证器添加到 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.

【讨论】:

  • 您不必排除现有的验证器,但通常使用 XML 添加新的 ConstraintValidator 是正确的做法。如果您乐于使用非标准化方法,您也可以查看 ConstraintDefinitionContributor。在这种情况下,您可以使用 Java ServiceLoader 方法来添加新的约束验证器。也就是说,LocalDate 没有 @Future 实现是有充分理由的。根据定义无法确定 LocalDate 是否在未来,因为它不包含时区信息。在你的情况下这可能没问题,但是..
  • 它们没有被排除,但实际上包括在内:)
  • 我有一个 Spring Boot 应用程序,我试图基本上做同样的事情。我如何告诉 Spring 我的自定义验证器?尽管有一个validation.xml文件(在我的资源文件夹中),我仍然收到UnexpectedTypeException
  • 您能否分享更多细节或(理想情况下)打开一个新问题并分享一些重现该问题的代码?因为这种方法在 Boot 项目中对我有用...
【解决方案3】:

如果有人对 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>

【讨论】:

    【解决方案4】:

    可以使用自定义映射配置自定义 LocalValidatorFactoryBean bean,然后将其连接为 Spring Bean,替换默认的自动连接 Spring Boot 验证器:

    @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 不会自动注入或以其他方式自动将验证器作为 bean 处理,因此不需要 LocalDateFutureValidator 上的 @Component 注释,它没有完成任何事情,并且具有误导性。因此应该将其删除。

    另请注意,较新版本的 Hibernate Validator 默认具有LocalDate @Future 功能,因此LocalDate 验证不再需要这种方法。但是,对于非内置类型的自定义 ConstraintValidators,这种方法仍然有用。

    说明

    首先,请注意 Spring 不会自动注入或以其他方式将验证器作为 bean 处理,因此不需要 LocalDateFutureValidator 上的 @Component 注释,它没有完成任何事情,并且具有误导性。因此应该将其删除。

    Hibernate Bean Validation reference guidea section 专门用于添加约束定义。它讨论了添加约束定义的两种方法:通过 Java 的 ServiceLoader 和以编程方式。对于这个答案,我将讨论如何以编程方式添加它们。

    使用这种方法,您的新约束可以添加到 Hibernate 配置中,如下所示:

    HibernateValidatorConfiguration hibernateConfiguration = [...];
    
    ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
    constraintMapping
            .constraintDefinition(Future.class)
            .validatedBy(LocalDateFutureValidator.class)
            .includeExistingValidators(true);
    hibernateConfiguration.addMapping(constraintMapping);
    

    为此,我们需要修改应用程序验证器使用的HibernateValidatorConfigurationLocalValidatorFactoryBean 类是 Spring 使用的 bean 工厂类型:

    这是在 Spring 应用程序上下文中设置 javax.validation (JSR-303) 的中心类:它引导 javax.validation.ValidationFactory 并通过 Spring Validator 接口以及 JSR-303 @987654327 公开它@ 接口和ValidatorFactory 接口本身。

    Spring Boot 通过 defaultValidator bean 每个 ValidationAutoConfiguration.defaultValidator() 提供这种类型的 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。这可以通过使用带有@Bean 定义方法的@Configuration 类或通过将CustomValidatorFactoryBean 类注释为@Component 来完成。

    【讨论】:

      【解决方案5】:

      根据FutureValidatorForLocalDate,Hibernate 6.0 添加了对具有LocalDate 值的@Future 的内置支持。因此,如果代码升级到 Hibernate 6.0 或更高版本,它应该可以开箱即用。

      【讨论】:

        猜你喜欢
        • 2023-04-09
        • 2019-12-29
        • 2023-04-02
        • 2021-04-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-08
        • 2016-01-21
        相关资源
        最近更新 更多