SpringBootで以下のようなAPIを開発していた際のTipsです。
- GETリクエスト
- クエリパラメータで検索条件を受け取り、リソースを検索する
- 検索条件にはタイムゾーン込みの時刻が入ってくる
- 例「http://localhost:8080/test?time=2024-05-18T06:30:00Z」
- メモリ上で検索条件時刻はjava.time.ZonedDateTimeクラスとして取り扱う
ZonedDateTimeには「2024-05-18T06:30+09:00」のようにタイムゾーン情報が含まれるのですが、開発する中で、リクエストでどのようなタイムゾーン情報が来たとしても、日本時間(JST、+09:00)に変換した値で持っておきたいということがありました。
本記事ではその仕様の実現のために調べたことを記載します。
この記事で伝えたいこと
- 「リクエストパラメータのTimeZoneを固定したい」時は[org.springframework.web.bind.WebDataBinder]をカスタマイズすべし
- 「リクエストボディのTimeZoneを固定したい」時はapplication.yamlに「spring.jackson.time-zone」を設定すべし
- 上記二点、設定すべきところが違うので要注意
実験したVer
- Spring Boot 2.7.3
- JDK: Amazon Correto Ver 17
実験PullRequest
https://github.com/kannna5296/SpringBoot-OnionArchitecture-MultiModule-Template/pull/19/files
クエリパラメータのTimeZoneを固定する
こういったクラスを書けばOKです。
リクエストパラメータのTimeZoneによらず、受け取ったZonedDateTimeオブジェクトのタイムゾーンを日本時間に固定できます。
import org.springframework.web.bind.WebDataBinder
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.InitBinder
import java.beans.PropertyEditorSupport
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@ControllerAdvice
class WebDataBinderCustom {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.registerCustomEditor(
ZonedDateTime::class.java,
ZonedDateTimeJSTEditor()
)
}
class ZonedDateTimeJSTEditor : PropertyEditorSupport() {
override fun setAsText(text: String?) {
if (!text.isNullOrEmpty()) {
val zdt = ZonedDateTime.parse(text, DateTimeFormatter.ISO_DATE_TIME)
val zonedId= ZoneId.of("Asia/Tokyo")
val jstZdt = zdt.withZoneSameInstant(zonedId)
value = jstZdt
} else {
value = null
}
}
}
}
クエリパラメータ→Kotlinオブジェクトの変換調査
以下のようなコントローラを考えます。
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.time.ZonedDateTime
@RestController
@RequestMapping("/")
@Tag(name = "Book", description = "書籍情報")
class TestController {
@GetMapping("/test")
fun hoge(@ModelAttribute form: TestForm) : ResponseEntity<Void> {
println(form.zonedDateTime)
return ResponseEntity.ok().build()
}
}
data class TestForm(
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
val zonedDateTime: ZonedDateTime
)
実際に動作確認した値を以下に記載します。
WebDataBinderカスタマイズなし
カスタマイズなしだと、「クエリパラメータに記載されたタイムゾーンのままZonedDateTimeに保持する」ような動きをします。
- リクエスト「http://localhost:8080/test?zonedDateTime=2024-05-18T06:30:00Z」
- println(form.zonedDateTime)は「2024-05-18T06:30Z」。リクエストがUTCなので、UTCのまま。
- リクエスト「http://localhost:8080/test?zonedDateTime=2024-05-18T15:30:00%2B09:00」
- println(form.zonedDateTime)は「2024-05-18T15:30+09:00」リクエストがJSTなので、JSTのまま。
WebDataBinderカスタマイズあり
クエリパラメータに記載されたタイムゾーンによらず、常にJST時刻で保持される
- リクエスト「http://localhost:8080/test?zonedDateTime=2024-05-18T06:30:00Z」
- println(form.zonedDateTime)は「2024-05-18T15:30+09:00[Asia/Tokyo]」 リクエストがUTCだが、JSTに変換されて保持される。
- リクエスト「http://localhost:8080/test?zonedDateTime=2024-05-18T15:30:00%2B09:00」
- println(form.zonedDateTime)は「2024-05-18T15:30+09:00[Asia/Tokyo]」 リクエストがJSTで、そのまま保持される。
罠:クエリパラメータの変換にspring.jackson.time-zoneは効かない
SpringBootには「spring.jackson.time-zone」という、いかにもタイムゾーンを指定してくれそうな設定値があり、これをapplicatio.yaml(or json)に書くことができます。
公式URL
ただこれはリクエストボディをKotlin/Javaオブジェクトにデシリアライズする際に利用される値であるらしく、ここまで書いているクエリパラメータの変換には効いてくれないようでした。(ここ勘違いしてて、実装時にすごく混乱しました。。)
spring:
jackson:
time-zone: Asia/Tokyo
のように書くことができますが、挙動としては「WebDataBinderカスタマイズなし」の場合と同じく、ZonedDateTimeオブジェクトはリクエストパラメータと同じTimeZoneで扱われます。
リクエストボディのTimeZoneを固定する
すでに答えを書いていますが、リクエストボディのTimeZoneを固定したい場合は、
spring:
jackson:
time-zone: Asia/Tokyo
を設定してください。
リクエストボディ→Kotlinオブジェクトの変換調査
以下のようなコントローラを想定します
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.time.ZonedDateTime
@RestController
@RequestMapping("/")
@Tag(name = "Book", description = "書籍情報")
class TestController {
@PostMapping("/test")
fun post(@RequestBody req: TestRequest) : ResponseEntity<Void> {
println(req.zonedDateTime)
return ResponseEntity.ok().build()
}
}
data class TestRequest(
val zonedDateTime: ZonedDateTime
)
spring.jackson.time-zone設定なし
リクエストボディに記載されたタイムゾーンによらず、UTCで保持するような動きをします。
リクエストパラメータ側は「リクエストで指定されたTimeZoneに依存する」だったので、若干考え方というかお作法が違うっぽいです。。
{
"zonedDateTime": "2024-05-18T06:30:00Z"
}
- println(req.zonedDateTime)は「2024-05-18T06:30Z[UTC]」
{
"zonedDateTime": "2024-05-18T15:30:00+09:00"
}
- println(req.zonedDateTime)は「2024-05-18T06:30Z[UTC]」
spring.jackson.time-zone設定あり
リクエストボディに記載されたタイムゾーンによらず、JSTで保持するような動きをします。
{
"zonedDateTime": "2024-05-18T06:30:00Z"
}
- println(req.zonedDateTime)は「2024-05-18T15:30+09:00[Asia/Tokyo]」
{
"zonedDateTime": "2024-05-18T15:30:00+09:00"
}
- println(req.zonedDateTime)は「2024-05-18T15:30+09:00[Asia/Tokyo]」
まとめ
デフォルト | サーバ側でTimeZone固定するには | |
クエリパラメータ | リクエストのTimeZoneを保持 | WebDataBinderをカスタマイズ |
リクエストボディ | UTCで保持 | spring.jackson.time-zoneを設定 |