今回は、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#fixed
でClock
を生成しているのがポイントですね。
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
で一度だけ取得しています。(これが後で問題になるのですが)
isValid
でConstraintValidatorContext
をHibernateConstraintValidatorContext
にキャストしても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
を取得するためにConstraintValidatorContext
をHibernateConstraintValidatorContext
にキャストするのがポイントですね。
おまけ: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