上一篇文章,介紹了 LocalDate、LocalTime 與 LocalDateTime,但它們沒有時區。本文將介紹具有時區概念的 ZonedDateTime。並示範如何在 Spring Boot 中,以 request body 接收這些日期時間物件。
此篇亦轉載到個人部落格。
Java 8 提供 ZonedDateTime
類別,它加入了時區的概念,便於我們進行換算。
var dateTime = LocalDateTime.of(2023, 8, 24, 15, 30, 0);
var taipeiZoneId = ZoneId.of("Asia/Taipei");
var taipeiDateTime = ZonedDateTime.of(dateTime, taipeiZoneId);
var tokyoZoneId = ZoneId.of("Asia/Tokyo");
var tokyoDateTime = taipeiDateTime.withZoneSameInstant(tokyoZoneId);
System.out.println(tokyoDateTime.getHour()); // 16
System.out.println(tokyoDateTime.getMinute()); // 30
從範例程式中能看出,由 LocalDateTime
與 ZoneId
,可組合出 ZonedDateTime
。此處建立的是「臺北時間 15:30」
若要換算成其他地區的時間,請呼叫 withZoneSameInstant
方法,並傳入指定時區的 ZoneId
即可。此處換算成「東京時間」後,輸出的時間為 16:30。
在上面的範例程式中,藉由「Taipei」和「Tokyo」這樣子的 key,可建立出 ZoneId
物件,用來代表時區。呼叫 ZoneId.getAvailableZoneIds()
方法,我們能得到所有支援的 key,舉例幾個如下:
然而在開發中,要我們配合上面這份清單,感覺並不方便。因此除了傳入地區,也能直接使用偏移量(offset),即 UTC/GMT,來定義 ZoneId
。
var dateTime = LocalDateTime.of(2023, 8, 24, 15, 30, 0);
var taipeiZoneId = ZoneId.of("+0800");
var taipeiDateTime = ZonedDateTime.of(dateTime, taipeiZoneId);
var hawaiiZoneId = ZoneId.of("-1000");
var hawaiiDateTime = taipeiDateTime.withZoneSameInstant(hawaiiZoneId);
System.out.println(hawaiiDateTime.getDayOfMonth()); // 23
System.out.println(hawaiiDateTime.getHour()); // 21
System.out.println(hawaiiDateTime.getMinute()); // 30
以上範例是將臺北時間 2023-08-24 15:30(UTC+8)
,換算成夏威夷時間 2023-08-23 21:30(UTC-10)
。
根據參考資料,ZoneId 偏移量的寫法有 h
、hh
、hhmm
、hh:mm
等多種格式,如 +8
、-10
、-0800
、+10:00
。
Ref:
Java 8 新增的時間系列用法
Java 8之新日期時間API筆記(四)ZoneId
最後示範這兩天提到的日期時間類別,要如何應用在 Spring Boot 中的 RESTful API 上,以便好好地與前端串接。
以下是一個用來當作 request body 的範例類別。
public class DateTimeDto {
private LocalDate birthday; // 生日
private LocalTime playStartTime; // 開播時間
private LocalDateTime bookingDateTime; // 預約時間
private ZonedDateTime arrivalDateTime; // 班機到達時間
// getter, setter ...
}
而以下是簡單的 RESTful API。
@PostMapping("/date-time-body")
public ResponseEntity<DateTimeDto> foo(@RequestBody DateTimeDto dto) {
return ResponseEntity.ok(dto);
}
當前端發出 request 時,JSON 欄位值有一定的預設格式,如此才能讓後端正確接收日期時間的資料。
POST http://localhost:8080/date-time-body
{
"birthday": "2023-01-01",
"playStartTime": "20:30:00",
"bookingDateTime": "2023-01-01T20:30:00",
"arrivalDateTime": "2023-01-01T20:30:00+08:00"
}
當讀者用開發工具的 debug 模式確認,會發現 ZonedDateTime
被換算成 UTC+0 的時間了。這可以在 application.properties
檔案添加以下參數來避免掉。
spring.jackson.deserialization.ADJUST_DATES_TO_CONTEXT_TIME_ZONE=false
如果想要定義自己的日期時間格式,請在欄位冠上 Jackson 套件提供的 @JsonFormat
標記,並於 pattern
參數傳入格式。
public class DateTimeDto {
@JsonFormat(pattern = "yyyy/MM/dd")
private LocalDate birthday;
@JsonFormat(pattern = "HH.mm.ss")
private LocalTime playStartTime;
@JsonFormat(pattern = "yyyy/MM/dd HH.mm.ss")
private LocalDateTime bookingDateTime;
@JsonFormat(pattern = "yyyy/MM/dd'T'HH.mm.ssXXX")
private ZonedDateTime arrivalDateTime;
// getter, setter ...
}
這次的範例自訂了「年月日」之間與「時分秒」之間的分隔符號,故 request body 中的欄位值,可以更改格式了!
POST http://localhost:8080/date-time-body
{
"birthday": "2023/01/01",
"playStartTime": "20.30.00",
"bookingDateTime": "2023/01/01 20.30.00",
"arrivalDateTime": "2023/01/01T20.30.00+08:00"
}
若是在 controller 經由 @RequestParam
標記接收查詢字串(query string),則最簡單的做法是加上 @DateTimeFormat
標記,於 pattern
參數傳入格式。
@GetMapping("/date-time-param")
public ResponseEntity<DateTimeDto> bar(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate birthday,
@RequestParam @DateTimeFormat(pattern = "HH:mm:ss") LocalTime playStartTime,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime bookingDateTime,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") ZonedDateTime arrivalDateTime
) {
return ResponseEntity.ok().build();
}
要留意的是,如果想將 2023-01-01T20:30:00+08:00
這樣的格式當作 query string,+
號要記得做 url encode(變成 %2B
),否則會被視為字串串接。
以下是一個攜帶 query string 的 url 範例。
GET http://localhost:8080/dateTime?birthday=2023-01-01&playStartTime=20:30:00&bookingDateTime=2023-01-01 20:30:00&arrivalDateTime=2023-01-01T20:30:00%2B08:00
如果讀者想用 @ModelAttribute
標記,將 query string 都收集到一個自訂的類別物件中,資料的轉換就得手動處理了。
@GetMapping("/dateTime")
public ResponseEntity<DateTimeDto> bar(@ModelAttribute DateTimeParam params) {
return ResponseEntity.ok().build();
}
此處的自訂類別叫做 DateTimeParam
,其 setter 方法要改成以 String
的型態來接收參數。並在賦值時使用 parse
方法,自行轉換為日期時間物件,如下:
public class DateTimeParam {
private LocalDate birthday;
private LocalTime playStartTime;
private LocalDateTime bookingDateTime;
private ZonedDateTime arrivalDateTime;
public void setBirthday(String birthday) {
this.birthday = LocalDate.parse(birthday);
}
// setPlayStartTime, setBookingDateTime, setArrivalDateTime
// getter ...
}
這些 parse
方法所接受的日期時間格式,預設與本節第一段介紹的相同。如果想要定義自己的格式,請搭配上一篇介紹的 DateTimeFormatter
,對不同種類的日期時間做解析。
public class DateTimeParam {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd");
private LocalDate birthday;
public void setBirthday(String birthday) {
this.birthday = LocalDate.parse(birthday, DATE_FORMATTER);
}
// setPlayStartTime, setBookingDateTime, setArrivalDateTime
// getter ...
}
讀者亦可設計一些 util method,並由統一的
DateTimeFormatter
來解析。
今日文章到此結束!
最後推廣一下自己的部落格,我是「新手工程師的程式教室」的作者,請多指教