JSR310新日期API(完結篇)-生產實戰

前提

前面經過五篇文章基本介紹完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類型的請求內容的時候會使用ObjectMapperJackson)進行反序列化。這裏引入org.springframework.boot:spring-boot-starter-web:2.2.5.RELEASE作一個演示。json

引入spring-boot-starter-web的最新版本以後,內置的Jackson已經引入了JSR-310相關的兩個依賴。SpringBoot中引入在裝載ObjectMapper經過Jackson2ObjectMapperBuilder中的建造器方法加載了JavaTimeModuleJdk8Module,實現了對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-100:00:0023: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)

相關文章
相關標籤/搜索