前面經過五篇文章基本介紹完JSR-310
經常使用的日期時間API
以及一些工具類,這篇博文主要說說筆者在生產實戰中使用JSR-310
日期時間API
的一些經驗。java
系列文章:web
::: info
不經意間,JDK8發佈已經超過6年了,若是還在用舊的日期時間API,能夠抽點時間熟悉一下JSR-310的日期時間API。
:::spring
下面會結合一下仿真場景介紹具體的API
選取,因爲OffsetDateTime
基本能知足大部分場景,所以挑選OffsetDateTime
進行舉例。shell
通常在Web
應用的表單提交或者Reuqest Body
提交的內容中,須要把字符串形式的日期時間轉換爲對應的日期時間對象。Web
應用多數狀況下會使用SpringMVC
,而SpringMVC
的消息轉換器在處理application/json
類型的請求內容的時候會使用ObjectMapper
(Jackson
)進行反序列化。這裏引入org.springframework.boot:spring-boot-starter-web:2.2.5.RELEASE
作一個演示。json
引入spring-boot-starter-web
的最新版本以後,內置的Jackson
已經引入了JSR-310
相關的兩個依賴。SpringBoot
中引入在裝載ObjectMapper
經過Jackson2ObjectMapperBuilder
中的建造器方法加載了JavaTimeModule
和Jdk8Module
,實現了對JSR-310
特性的支持。值得注意的是JavaTimeModule
中和日期時間相關的格式化器DateTimeFormatter
都使用了內置的實現,如日期時間使用的是DateTimeFormatter.ISO_OFFSET_DATE_TIME
,沒法解析yyyy-MM-dd HH:mm:ss
模式的字符串。例如:api
public class Request { private OffsetDateTime createTime; public OffsetDateTime getCreateTime() { return createTime; } public void setCreateTime(OffsetDateTime createTime) { this.createTime = createTime; } } @PostMapping(path = "/test") public void test(@RequestBody Request request) throws Exception { LOGGER.info("請求內容:{}", objectMapper.writeValueAsString(request)); }
請求以下:app
curl --location --request POST 'localhost:9091/test' \ --header 'Content-Type: application/json' \ --data-raw '{ "createTime": "2020-03-01T21:51:03+08:00" }' // 請求內容:{"createTime":"2020-03-01T13:51:03Z"}
若是執意要選用yyyy-MM-dd HH:mm:ss
模式的字符串,那麼屬性的類型只能選用LocalDateTime
而且要重寫對應的序列化器和反序列化器,覆蓋JavaTimeModule
中原有的實現,參考前面的一篇文章。框架
筆者負責的系統中,常常有定時調度的場景,舉個例子:天天凌晨1點要跑一個定時任務,查詢T-1
日或者上一週的業務數據,更新到對應的業務統計表中,以便次日早上運營的同事查看報表數據。查詢T-1
日的數據,實際上就是查詢T-1
日00:00:00
到23:59:59
的數據。這裏舉一個案例,計算T-1
日全部訂單的總金額:curl
@Slf4j public class Process { static ZoneId Z = ZoneId.of("Asia/Shanghai"); static DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); JdbcTemplate jdbcTemplate; @Data private static class Order { private Long id; private String orderId; private BigDecimal amount; private OffsetDateTime createTime; } public void processTask() { // 這裏的時區要按實際狀況選擇 OffsetDateTime now = OffsetDateTime.now(Z); OffsetDateTime start = now.plusDays(-1L).withHour(0).withMinute(0).withSecond(0).withNano(0); OffsetDateTime end = start.withHour(23).withMinute(59).withSecond(59).withNano(0); BigDecimal totalAmount = BigDecimal.ZERO; int limit = 500; long maxId = 0L; while (true) { List<Order> orders = selectPendingProcessingOrders(start, end, limit, maxId); if (!orders.isEmpty()) { totalAmount = totalAmount.add(orders.stream().map(Order::getAmount).reduce(BigDecimal::add) .orElse(BigDecimal.ZERO)); maxId = orders.stream().map(Order::getId).max(Long::compareTo).orElse(Long.MAX_VALUE); } else { break; } } log.info("統計[{}-{}]的訂單總金額爲:{}", start.format(F), end.format(F), totalAmount); } static ResultSetExtractor<List<Order>> MANY = r -> { List<Order> orders = new ArrayList<>(); while (r.next()) { Order order = new Order(); orders.add(order); order.setId(r.getLong("id")); order.setOrderId(r.getString("order_id")); order.setAmount(r.getBigDecimal("amount")); order.setCreateTime(OffsetDateTime.ofInstant(r.getTimestamp("create_time").toInstant(), Z)); } return orders; }; private List<Order> selectPendingProcessingOrders(OffsetDateTime start, OffsetDateTime end, int limit, long id) { return jdbcTemplate.query("SELECT * FROM t_order WHERE create_time >= ? AND create_time <= ? AND id > ? LIMIT ?", p -> { p.setTimestamp(1, Timestamp.from(start.toInstant())); p.setTimestamp(2, Timestamp.from(end.toInstant())); p.setLong(3, id); p.setInt(4, limit); }, MANY); } }
上面的只是僞代碼,不能直接執行,使用的是基於日期時間和ID
翻頁的設計,在保證效率的同時能夠下降IO
,經常使用於查詢比較多的定時任務或者數據遷移。spring-boot
計算兩個日期時間之間的差值也是很常見的場景,筆者遇到過的場景就是:運營須要導出一批用戶數據,主要包括用戶ID
、脫敏信息、用戶註冊日期時間以及註冊日期時間距當前日期的天數。
用戶ID | 用戶姓名 | 註冊日期時間 | 註冊距今天數 | |
---|---|---|---|---|
1 | 張小狗 | 2019-01-03 12:11:23 | x | |
2 | 張大狗 | 2019-10-02 23:22:13 | y |
設計的僞代碼以下:
@Data private static class CustomerDto { private Long id; private String name; private OffsetDateTime registerTime; private Long durationInDay; } @Data private static class Customer { private Long id; private String name; private OffsetDateTime registerTime; } static ZoneId Z = ZoneId.of("Asia/Shanghai"); static OffsetDateTime NOW = OffsetDateTime.now(Z); public List<CustomerDto> processUnit() { return Optional.ofNullable(select()).filter(Objects::nonNull) .map(list -> { List<CustomerDto> result = new ArrayList<>(); list.forEach(x -> { CustomerDto dto = new CustomerDto(); dto.setId(x.getId()); dto.setName(x.getName()); dto.setRegisterTime(x.getRegisterTime()); Duration duration = Duration.between(x.getRegisterTime(), NOW); dto.setDurationInDay(duration.toDays()); result.add(dto); }); return result; }).orElse(null); } private List<Customer> select() { // 模擬查詢 return null; }
經過Duration
能夠輕鬆計算兩個日期時間之間的差值,而且能夠輕鬆轉換爲不一樣的時間計量單位。
利用日期時間校準器TemporalAdjuster
能夠十分方便地計算XX月YY日是ZZ節
這種日期形式的節日。例如:五月第二個星期日是母親節,六月的第三個星期日是父親節。
public class X { public static void main(String[] args) throws Exception { OffsetDateTime time = OffsetDateTime.now(); System.out.println(String.format("%d年母親節是:%s", time.getYear(), time.withMonth(5).with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.SUNDAY)).toLocalDate().toString())); System.out.println(String.format("%d年父親節是:%s", time.getYear(), time.withMonth(6).with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.SUNDAY)).toLocalDate().toString())); time = time.plusYears(1); System.out.println(String.format("%d年母親節是:%s", time.getYear(), time.withMonth(5).with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.SUNDAY)).toLocalDate().toString())); System.out.println(String.format("%d年父親節是:%s", time.getYear(), time.withMonth(6).with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.SUNDAY)).toLocalDate().toString())); } } // 輸出結果 2020年母親節是:2020-05-10 2020年父親節是:2020-06-21 2021年母親節是:2021-05-09 2021年父親節是:2021-06-20
有些定時調度或者提醒消息發送須要在這類特定的日期時間觸發,那麼經過TemporalAdjuster
就能夠相對簡單地計算出具體的日期。
關於JSR-310
的日期時間API
就介紹這麼多,筆者最近從事數據方面的工做,不過確定會持續和JSR-310
打交道。
這裏貼一個工具類OffsetDateTimeUtils
:
@Getter @RequiredArgsConstructor public enum TimeZoneConstant { CHINA(ZoneId.of("Asia/Shanghai"), "上海-中國時區"); private final ZoneId zoneId; private final String description; } public enum DateTimeUtils { // 單例 X; public static final DateTimeFormatter L_D_T_F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public static final DateTimeFormatter S_D_F = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public static final DateTimeFormatter S_D_M_F = DateTimeFormatter.ofPattern("yyyy-MM"); public static final DateTimeFormatter S_T_F = DateTimeFormatter.ofPattern("HH:mm:ss"); public OffsetDateTime getCurrentOffsetDateTime() { return OffsetDateTime.now(TimeZoneConstant.CHINA.getZoneId()); } public OffsetDateTime getDeltaDayOffsetDateTimeStart(long delta) { return getCurrentOffsetDateTime().plusDays(delta).withHour(0).withMinute(0).withSecond(0).withNano(0); } public OffsetDateTime getDeltaDayOffsetDateTimeEnd(long delta) { return getCurrentOffsetDateTime().plusDays(delta).withHour(23).withMinute(59).withSecond(59).withNano(0); } public OffsetDateTime getYesterdayOffsetDateTimeStart() { return getDeltaDayOffsetDateTimeStart(-1L); } public OffsetDateTime getYesterdayOffsetDateTimeEnd() { return getDeltaDayOffsetDateTimeEnd(-1L); } public long durationInDays(OffsetDateTime start, OffsetDateTime end) { return Duration.between(start, end).toDays(); } public OffsetDateTime getThisMonthOffsetDateTimeStart() { OffsetDateTime offsetDateTime = getCurrentOffsetDateTime(); return offsetDateTime.with(TemporalAdjusters.firstDayOfMonth()).withHour(0).withMinute(0).withSecond(0).withNano(0); } public OffsetDateTime getThisMonthOffsetDateTimeEnd() { OffsetDateTime offsetDateTime = getCurrentOffsetDateTime(); return offsetDateTime.with(TemporalAdjusters.lastDayOfMonth()).withHour(23).withMinute(59).withSecond(59).withNano(0); } }
(本文完 c-3-d e-a-20200302)