rakugakibox.net

技術ノート。兼JS/CSS実験場。 ♡:Java, Spring, AWS.

Spring Boot+周辺ライブラリでJava8日時APIを使う! (Java Advent Calendar 2015 4日目)

これは Java Advent Calendar 2015 4日目の記事です。
昨日は@susumuisさんの「WindowsでJavaな人に捧げる素敵なスクリプティングツール jrunscriptの使い方」でした。
明日は@megascusさんです。


Javaで日時を扱う場合は、できるだけ Java 8 Date and Time API を使っています。
旧API (java.util.Date, java.util.Calendar) と比較して、

  • Immutable なこと。
  • 日付, 時間, 日付+時間等でクラスが分かれてること。

が気に入ってます。

が、まだ一手間加えないと何も考えずに使えるとは言えない状態でした。

この記事では、 Spring Boot & 僕がよく使う周辺ライブラリで
Java8日時APIを使うときにやったことをまとめました。

前置き

普段は、ビジネスロジックやデータはJava8日時に統一して、
どうしてもなところだけ共通処理で旧型日時に変換する考え方でやってます。

今回、試した環境はこちらです。

  • Spring Boot 1.3.0
  • Java 1.8.0 Update 60
  • Apache Maven 3.3.3

次の部分について、Java8日時を1つずつ扱えるようにしました。

  • Jackson
  • JAXB
  • JPA
  • Thymeleaf
  • プロパティ (@Value, @ConfigurationProperties)
  • リクエストパラメータ (@PathVariable, @RequestParam, @ModelAttribute, etc)

使う日時の型は、次の3つに限定します。
Offset系, Zoned系はあまり扱ったことがありません (^^;

  • java.time.LocalDate
  • java.time.LocalTime
  • java.time.LocalDateTime

日時のフォーマットは、基本はISOで統一しています。

Jackson

spring-boot-starter-web のデフォで使える Jackson.
@RequestBody, @ResponseBody 等のJSON変換に Jackson を使う場合は、
次の2点を設定すればフィールドにJava8日時が使えました。

1) 依存関係に Jackson Datatype JSR310 を追加。

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

2) application.yml 等で Jackson のオプションをセット。

spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS: false

@JsonFormat を添えれば、
フィールドごとにフォーマット変更もいけました。

@JsonFormat(pattern = "y年M月d日")
private LocalDate customDate;
@JsonFormat(pattern = "H時m分s秒")
private LocalTime customTime;
@JsonFormat(pattern = "y年M月d日 H時m分s秒")
private LocalDateTime customDateTime;

サンプルコード

実行後、こんなcurlコマンドでGET/POSTできます。

$ curl "http://localhost:8080/hoge"
$ curl -X POST "http://localhost:8080/hoge" \
       -H "Content-Type: application/json" \
       -d '{ "isoDate": "2012-01-23", "isoTime": "23:59:48", "isoDateTime": "2012-01-23T23:59:48", "customDate": "2012年1月23日", "customTime": "23時59分48秒", "customDateTime": "2012年1月23日 23時59分48秒" }'

参考

JAXB

Java 標準で使える JAXB.
@RequestBody, @ResponseBody 等のXML変換に JAXB を使う場合は、
次の2点を設定すればフィールドにJava8日時が使えました。

日時の型ごとにXmlAdapterを用意しないといけないのですが、
作るのが面倒なので軽くぐぐったら、
JAXB adapters for Java 8 Date and Time API (JSR-310) types
というのがあったので使ってみました。

1) 依存関係に追加。

<dependency>
  <groupId>com.migesok</groupId>
  <artifactId>jaxb-java-time-adapters</artifactId>
  <version>1.1.3</version>
</dependency>

2) パッケージにアノテーションを付与してアダプタを指定。

@XmlJavaTypeAdapters({
    @XmlJavaTypeAdapter(value = LocalDateXmlAdapter.class, type = LocalDate.class),
    @XmlJavaTypeAdapter(value = LocalTimeXmlAdapter.class, type = LocalTime.class),
    @XmlJavaTypeAdapter(value = LocalDateTimeXmlAdapter.class, type = LocalDateTime.class)
})
package your.pkg;

サンプルコード

実行後、こんなcurlコマンドでGET/POSTできます。

$ curl "http://localhost:8080/hoge.xml"
$ curl -X POST "http://localhost:8080/hoge.xml" \
       -H "Content-Type: application/xml" \
       -d '<hoge><isoDate>2012-01-23</isoDate><isoDateTime>2012-01-23T23:59:48</isoDateTime><isoTime>23:59:48</isoTime></hoge>'

参考

JPA

最近はDBMSには MySQL 5.6 を使っています。

DB側の日時の型は DATE, TIME, DATETIME を使い分けたいのですが、
単純に @Entity のフィールドに LocalDate 等を使うと
BLOB にマッピングされてしまいました。

この場合は、 java.util.Date, java.sql.Date 等に変換する
AttributeConverter を組み込んであげれば良いようです。
Spring Data JPA には Jsr310JpaConverters というクラスが
用意されていたので使ってみました。

Jsr310JpaConverters をスキャン対象に加えれば使えました。

@SpringBootApplication
@EntityScan(basePackageClasses = {
    YourApplication.class,
    Jsr310JpaConverters.class
})
public class YourApplication {
}

※MySQL 5.6, Hibernate でしか試していないので、他のDBMSやJPA実装でも大丈夫かは分かりません (^^;

サンプルコード

localhostにtestスキーマがあること前提のコードです。
実行すると日時を持つhogeテーブルを作成して、INSERT/SELECTします。

参考

Thymeleaf

テンプレートエンジンの Thymeleaf.
旧型日時は #dates, #calendars で扱えますが、
これらはJava8日時は扱えません。

次の2点を設定すれば、#temporals でJava8日時も扱えるようになりました。

1) 依存関係に thymeleaf-extras-java8time を追加。

<dependency>
  <groupId>org.thymeleaf.extras</groupId>
  <artifactId>thymeleaf-extras-java8time</artifactId>
  <version>2.1.0.RELEASE</version>
</dependency>

2) DialectをDIコンテナに登録。
TemplateEngine への組込みは ThymeleafAutoConfiguration がやってくれます。

@Bean
public IDialect java8TimeDialect() {
    return new Java8TimeDialect();
}

サンプルコード

実行後、/hogeにアクセスすると日時フォーマットしたHTMLを返します。

参考

プロパティ (@Value, @ConfigurationProperties)

例えばこんな application.yml を書いて、

try-springboot-with-java8time:
  iso:
    date: "2015-12-04"
    time: "12:34:56"
    date-time: "2015-12-04T12:34:56"

@Value でJava8日時フィールドをセットしたい場合。

@Value("${try-springboot-with-java8time.iso.date}")
private LocalDate isoDate;
@Value("${try-springboot-with-java8time.iso.time}")
private LocalTime isoTime;
@Value("${try-springboot-with-java8time.iso.date-time}")
private LocalDateTime isoDateTime;

これは ConversionService (Bean名はconversionService) を
自前で用意してやるといけました。

@Configuration
public class ConversionServiceConfiguration {
    @Bean
    public ConversionService conversionService() {
        FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean();
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        factory.setFormatterRegistrars(Collections.singleton(registrar));
        factory.afterPropertiesSet();
        return factory.getObject();
    }
}

@DateTimeFormat を添えれば、
フィールドごとにフォーマット変更もいけました。

@Value("${try-springboot-with-java8time.custom.date}")
@DateTimeFormat(pattern = "y年M月d日")
private LocalDate customDate;
@Value("${try-springboot-with-java8time.custom.time}")
@DateTimeFormat(pattern = "H時m分s秒")
private LocalTime customTime;
@Value("${try-springboot-with-java8time.custom.date-time}")
@DateTimeFormat(pattern = "y年M月d日 H時m分s秒")
private LocalDateTime customDateTime;

サンプルコード

実行すると application.yml の日時をログ出力します。

参考

リクエストパラメータ (@PathVariable, @RequestParam, @ModelAttribute, etc)

MVCコントローラでは、特に何もしなくても
Java8日時型で受取れるようになってました。

ただ、デフォルトのフォーマットは FormatStyle.SHORT でした。
日付なら15/12/04, 時間なら12:34, 日時なら15/12/04 12:34 といった感じ。

こちらも @DateTimeFormat を添えれば
パラメータごとにフォーマット変更もいけました。

@RequestMapping("/hoge")
public Map hoge(
        @RequestParam(required = false) LocalDate defaultDate,
        @RequestParam(required = false) LocalTime defaultTime,
        @RequestParam(required = false) LocalDateTime defaultDateTime,
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate isoDate,
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.TIME) LocalTime isoTime,
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime isoDateTime
) {
    // ...
}

デフォルトのフォーマットを変えたい場合は、
WebMvcConfigurer で自前のフォーマッタを組み込むと出来ました。

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}

サンプルコード

実行後、こんなcurlコマンドでGETできます。

$ curl "http://localhost:8080/hoge?defaultDate=2015-12-04&defaultTime=12:34:56.78&defaultDateTime=2015-12-04T12:34:56.78&isoDate=2015-12-04&isoTime=12:34:56.78&isoDateTime=2015-12-04T12:34:56.78"

まとめ

  • Jackson: jackson-datatype-jsr310 を使う。
  • JAXB: 日時の型ごとに XmlAdapter を用意。
  • JPA: Jsr310JpaConverters を使う。
  • Thymeleaf: thymeleaf-extras-java8time を使う。
  • プロパティ: ConversionService を設定。
  • リクエストパラメータ: そのままでも使える。フォーマット変えたい場合は WebMvcConfigurer で設定。

近いうち、特に意識しなくてもJava8日時が
さくさく使えるようになるといいなーと思います :)

関連記事