1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Spring MVC+Bean Validation(Hibernate Validator)で日付バリデーションの現在日時を変更する

Posted at

今回は、Spring MVC+Bean Validation(Hibernate Validator)において、日付バリデーション(@Future,@Pastなど)の現在日時を変更する方法を紹介します。

アプリで一意のシステム日時を使用したい場合や、テスト時のみ任意の日時を使用したい場合に役立つと思います。

今回使用するライブラリ

Hibernate Validator 6 (Bean Validation 2.0)

  • Spring MVC 5.1.4
  • Hibernate Validator 6.0.14
  • Lombok

Hibernate Validator 5 (Bean Validation 1.1)

  • Spring MVC 4.3.22
  • Hibernate Validator 5.4.3
  • Lombok

現在日時を提供するプロバイダ

Hibernate Validatorの日付バリデーション(@Future,@Pastなど)実装では、現在日時を単純にnew Date()のように取得しているわけではなく、現在日時を提供するプロバイダを利用して取得します。

Hibernate Validator 6と5でプロバイダが異なります。

Hibernate Validator 6 (Bean Validation 2.0)

Bean Validation 2.0で導入されたClockProviderインターフェイスが現在日時を提供します。

Validator validator = Validation.byDefaultProvider()
                        .configure()
                        .clockProvider(new MyClockProvider())
                        .buildValidatorFactory()
                        .getValidator();

Hibernate Validator 5 (Bean Validation 1.1)

Hibernate ValidatorのTimeProviderインターフェイスが現在日時を提供します。

Validator validator = Validation.byDefaultProvider()
                        .configure()
                        .timeProvider(new MyTimeProvider())
                        .buildValidatorFactory()
                        .getValidator();

プロバイダの作成

今回は、Spring MVCアプリのテスト時のみ任意の日時を使用するためのプロバイダを作成してみます。

Hibernate Validator 6 (Bean Validation 2.0)

ClockProvider#getClockで現在日時を取得するためのClockを提供します。
ここでは、日時文字列とタイムゾーンを指定して、常に同じ日時を提供することにします。

public class FixedClockProvider implements ClockProvider, InitializingBean {

    @Setter
    private String instantPattern;
    
    @Setter
    private String zoneIdPattern;
    
    private Clock clock;
    
    @Override
    public Clock getClock() {
        return clock;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Instant instant = instantPattern == null ? Instant.now() : Instant.parse(instantPattern);
        ZoneId zoneId = zoneIdPattern == null ? ZoneId.systemDefault() : ZoneId.of(zoneIdPattern);
        clock = Clock.fixed(instant, zoneId);
    }
}

Bean生成時にClock#fixedClockを生成しているのがポイントですね。

Hibernate Validator 5 (Bean Validation 1.1)

TimeProvider#getCurrentTimeで現在日時のミリ秒を提供します。
同様に日時文字列とタイムゾーンを指定して、常に同じ日時を提供することにします。

public class FixedTimeProvider implements TimeProvider, InitializingBean {

    @Setter
    private String instantPattern;
    
    @Setter
    private String zoneIdPattern;
    
    private Clock clock;
    
    @Override
    public long getCurrentTime() {
        return clock.millis();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Instant instant = instantPattern == null ? Instant.now() : Instant.parse(instantPattern);
        ZoneId zoneId = zoneIdPattern == null ? ZoneId.systemDefault() : ZoneId.of(zoneIdPattern);
        clock = Clock.fixed(instant, zoneId);
    }
}

Hibernate Validator 6向けの実装と互換性を持たせるため、一度Clockを経由してミリ秒を取得していますが、必ずしも必要な実装ではありませんw

プロバイダの適用

作成したプロバイダを適用するには、SpringのLocalValidatorFactoryBeanを拡張する必要があります。

Hibernate Validator 6 (Bean Validation 2.0)

LocalValidatorFactoryBean#postProcessConfigurationでBean Validationの設定を追加することができます。
ここでは、Bean ValidationにHibernate Validatorが利用されている場合に、ClockProviderを適用します。

public class LocalHibernateValidatorFactoryBean extends LocalValidatorFactoryBean {

    @Setter
    private ClockProvider clockProvider;
    
    @Override
    protected void postProcessConfiguration(Configuration<?> configuration) {
        if (configuration instanceof HibernateValidatorConfiguration) {
            postProcessHibernateValidatorConfiguration((HibernateValidatorConfiguration) configuration);
        }
    }

    protected void postProcessHibernateValidatorConfiguration(HibernateValidatorConfiguration configuration) {
        configuration.clockProvider(clockProvider);
    }
}

Configuration<?>HibernateValidatorConfigurationにキャストすることで、Hibernate Validator特有の設定を追加することができます。

Hibernate Validator 5 (Bean Validation 1.1)

同様に、Bean Validationにhibernate Validatorが利用されている場合は、TimeProviderを適用します。

public class LocalHibernateValidatorFactoryBean extends LocalValidatorFactoryBean {

    @Setter
    private TimeProvider timeProvider;
    
    @Override
    protected void postProcessConfiguration(Configuration<?> configuration) {
        if (configuration instanceof HibernateValidatorConfiguration) {
            postProcessHibernateValidatorConfiguration((HibernateValidatorConfiguration) configuration);
        }
    }

    protected void postProcessHibernateValidatorConfiguration(HibernateValidatorConfiguration configuration) {
        configuration.timeProvider(timeProvider);
    }
}

拡張したLocalValidatorFactoryBeanの適用

Bean定義で拡張したLocalValidatorFactoryBeanを適用します。

Hibernate Validator 6 (Bean Validation 2.0)

  • XML Config
    <mvc:annotation-driven validator="validator">
        ...
    </mvc:annotation-driven>
    <bean id="validator" class="xxx.LocalHibernateValidatorFactoryBean">
        <property name="clockProvider">
            <bean class="xxx.FixedClockProvider">
                <property name="instantPattern" value="2010-06-30T01:20:00Z" />
            </bean>
        </property>
    </bean>
  • Java Config
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public Validator getValidator() {
        return validator();
    }

    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalHibernateValidatorFactoryBean validator = new LocalHibernateValidatorFactoryBean();
        validator.setClockProvider(clockProvider());
        return validator;
    }

    @Bean
    public ClockProvider clockProvider() {
        FixedClockProvider provider = new FixedClockProvider();
        provider.setInstantPattern("2010-06-30T01:20:00Z");
        return provider;
    }
}

Hibernate Validator 5 (Bean Validation 1.1)

  • XML Config
    <mvc:annotation-driven validator="validator">
        ...
    </mvc:annotation-driven>
    <bean id="validator" class="xxx.LocalHibernateValidatorFactoryBean">
        <property name="timeProvider">
            <bean class="xxx.FixedTimeProvider">
                <property name="instantPattern" value="2010-06-30T01:20:00Z" />
            </bean>
        </property>
    </bean>
  • Java Config
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public Validator getValidator() {
        return validator();
    }

    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalHibernateValidatorFactoryBean validator = new LocalHibernateValidatorFactoryBean();
        validator.setTimeProvider(timeProvider());
        return validator;
    }

    @Bean
    public TimeProvider timeProvider() {
        FixedTimeProvider provider = new FixedTimeProvider();
        provider.setInstantPattern("2010-06-30T01:20:00Z");
        return provider;
    }
}

現在日時を使用するConstraintValidatorの作成

では次に、現在日時を使用して入力チェックを行うConstraintValidatorの作り方を見ていきます。

Hibernate Validator 6 (Bean Validation 2.0)

ClockProviderからClockを取得して、現在日時を取得します。

public class MyValidator implements HibernateConstraintValidator<MyCheck, LocalDate> {

    private Clock clock;
    
    @Override
    public void initialize(ConstraintDescriptor<MyCheck> constraintDescriptor,
            HibernateConstraintValidatorInitializationContext initializationContext) {
        clock = initializationContext.getClockProvider().getClock();
    }
    
    @Override
    public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
        LocalDate referenceDate = clock.instant().atZone(ZoneId.systemDefault()).toLocalDate();

        // バリデーションを実装する
    }
}

ClockProviderを取得するためにHibernate ValidatorのHibernateConstraintValidatorインターフェイスを継承する必要があるのがポイントですね。

Clockは時を刻み続けるため、initializeで一度だけ取得しています。(これが後で問題になるのですが)

isValidConstraintValidatorContextHibernateConstraintValidatorContextにキャストしてもClockProviderを取得できると思いますが、@Future@Pastに実装を合わせています。

Hibernate Validator 5 (Bean Validation 1.1)

TimProviderから現在日時を取得します。

public class MyValidator implements ConstraintValidator<MyCheck, LocalDate> {

    @Override
    public void initialize(ThisYear constraintAnnotation) {
    }
    
    @Override
    public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
        TimeProvider timeProvider = context.unwrap(HibernateConstraintValidatorContext.class).getTimeProvider();
       LocalDate referenceDate = Instant.ofEpochMilli(timeProvider.getCurrentTime()).atZone(ZoneId.systemDefault()).toLocalDate();

        // バリデーションを実装する
    }
}

TimeProviderを取得するためにConstraintValidatorContextHibernateConstraintValidatorContextにキャストするのがポイントですね。

おまけ:TERASOLUNAのDateFactoryをプロバイダに適用する

TERASOLUNAのDateFactoryは、データベースが持つシステム日時を複数のアプリで共有するなど、エンタープライズ向けWebアプリに役立つ機能です。

今度はこれを適用したプロバイダを作成してみます。

Hibernate Validator 6 (Bean Validation 2.0)

以下のように、DateFactoryによりシステム日時を取得するClockProviderを実装できます。

public class DateFactoryClockProvider2 implements ClockProvider {

    @Setter
    private JodaTimeDateFactory dateFactory;

    @Override
    public Clock getClock() {
        return Clock.offset(Clock.systemDefaultZone(), Duration.between(Instant.now(), dateFactory.newDate().toInstant()));
    }
}

TERASOLUNA 5.4.1では、JSR310 (Java 8 Date and Time API)に対応したDateFactoryが提供されていないため、DateFactoryが取得した日付からClockを生成する部分は独自に実装する必要があります。

しかし、DateFactoryを完全にラップするClockProviderを作るのは不可能ではないかと思います。

DateFactoryはデータベースからシステム日時を取得するため、システム運用中や総合試験中の日時変更が可能ですが、ValidatorはHibernateConstraintValidator#initializeで一度だけClockを取得するため、@Future@Pastのような標準Validatorに運用中の日時変更を反映することができない。。。と思われます。

Hibernate Validator 5 (Bean Validation 1.1)

以下のように、DateFactoryによりシステム日時を取得するTimeProviderを実装できます。

public class DateFactoryTimeProvider implements TimeProvider {

    @Setter
    private JodaTimeDateFactory dateFactory;
    
    @Override
    public long getCurrentTime() {
        return dateFactory.newDateTime().getMillis();
    }
}

まとめ

仕組みはそれほど難しくないので、機会があれば適用してみようと思います。

Bean Validation 2.0から現在日時の制御が標準化されたので、実装方法が変わりましたが、使い勝手も変わったので微妙なところですね。

ちなみに、SpringのLocalValidatorFactoryBeanは1.1と2.0の互換性を保つため、ClockProviderのカスタマイズをあえて採用していないようです。 -> spring-framework#21994

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?