Kotlin,Java Spring

SpringBootでリクエストパラメータのタイムゾーンをJST(日本時間)に固定する【WebDataBinder】

2024年5月19日

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

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.json.spring.jackson.time-zone

ただこれはリクエストボディを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を設定

-Kotlin,Java, Spring