
https://unsplash.com/photos/white-and-pink-analog-alarm-clock-jOeh3Lv88xA
筆者的第一份工作,公司對於日期時間一律都使用舊式的「java.util.Date」類別來處理。若想做增減,還要搭配 Apache 提供的 DateUtils 來完成。
後來才在網路上得知如「LocalDateTime」、「ZonedDateTime」這種 Java 8 推出的類別。本文將會介紹它們,除了了解操作方式,也要知道如何在 Spring Boot 的 controller,以這些型態來接收資料。
此篇轉載自本人的 iThome 鐵人賽文章。
一、無時區的日期時間
這些日期時間套件,位於「java.time」的 package 中。
(一)LocalDate
代表日期,只儲存了年、月、日。
var date1 = LocalDate.now(); // 現在 | |
System.out.println(date1); // 2023-08-25 | |
var date2 = LocalDate.of(2018, 8, 1); // 自訂日期 | |
date2 = date2.plusDays(10); // 增加 10 天 | |
System.out.println(date2.getYear()); // 2018 | |
System.out.println(date2.getMonth()); // AUGUST | |
System.out.println(date2.getMonthValue()); // 8 | |
System.out.println(date2.getDayOfMonth()); // 11 |
要注意的是,該物件儲存的日期是不可變的(immutable),因此進行日期的增減後,要將回傳值接起來。
(二)LocalTime
代表時間,除了儲存時、分、秒,也能儲存奈秒。
var time1 = LocalTime.now(); // 現在 | |
System.out.println(time1); // 12:39:53.561241100 | |
var time2 = LocalTime.of(12, 30, 15); // 自訂時間 | |
time2 = time2.minusSeconds(10); // 減少 10 秒 | |
System.out.println(time2.getHour()); // 12 | |
System.out.println(time2.getMinute()); // 30 | |
System.out.println(time2.getSecond()); // 5 |
(三)LocalDateTime
封裝了上述的 LocalDate 和 LocalTime。
var dateTime1 = LocalDateTime.now(); // 現在 | |
System.out.println(dateTime1); // 2023-08-25T12:53:55.822241500 | |
var dateTime2 = LocalDateTime.of(2023, 8, 24, 12, 30, 15); // 自訂日期時間 | |
dateTime2 = dateTime2.plusDays(180).minusMinutes(5); | |
System.out.println(dateTime2.getYear()); // 2024 | |
System.out.println(dateTime2.getMonthValue()); // 2 | |
System.out.println(dateTime2.getDayOfMonth()); // 20 | |
System.out.println(dateTime2.getMinute()); // 25 |
二、格式化與解析
DateTimeFormatter 可用來進行日期時間物件,與字串之間的互換。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd HHmmss"); | |
LocalDateTime dateTime = LocalDateTime.parse("20230824 123015", formatter); | |
System.out.println(dateTime); // 2023-08-24T12:30:15 | |
System.out.println(formatter.format(dateTime)); // 20230824 123015 |
讀者可注意到,這裡不必像舊式的 SimpleDateFormat 那樣,還要處理 ParseException 這個 checked exception。
三、有時區的日期時間
(一)ZonedDateTime
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。
(二)ZoneId
在上面的範例程式中,藉由「Taipei」和「Tokyo」這樣子的 key,可建立出 ZoneId 物件,用來代表時區。呼叫 ZoneId.getAvailableZoneIds() 方法,我們能得到所有支援的 key,舉例幾個如下:
- Asia/Taipei
- Asia/Tokyo
- America/New_York
- Australia/Sydney
- Greenwich
- ...
然而在開發中,要我們配合上面這份清單,感覺並不方便。因此除了傳入地區,也能直接使用偏移量(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 |
ZoneId 偏移量的寫法,尚有 h、hh、hhmm、hh:mm 等多種格式,如 +8、-10、-0800、+10:00。
四、在 Spring Boot Controller 接收
本節示範這些日期時間類別,要如何應用在 Spring Boot 中的 controller 上,以便好好地與前端串接。
(一)用於 request body
以下是一個用來當作 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" }
(二)用於查詢字串(@RequestParam)
若是在 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/date-time-param?birthday=2023-01-01&playStartTime=20:30:00&bookingDateTime=2023-01-01 20:30:00&arrivalDateTime=2023-01-01T20:30:00%2B08:00
(三)用於查詢字串(@ModelAttribute)
如果讀者想用 @ModelAttribute 標記,將 query string 都收集到一個自訂的類別物件中,資料的轉換就得手動處理了。
@GetMapping("/date-time-param") | |
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 來解析。
留言
張貼留言