前提 前面通过五篇文章基本介绍完JSR-310
常用的日期时间API
以及一些工具类,这篇博文主要说说笔者在生产实战中使用JSR-310
日期时间API
的一些经验。
系列文章:
::: info 不经意间,JDK8发布已经超过6年了,如果还在用旧的日期时间API,可以抽点时间熟悉一下JSR-310的日期时间API。 :::
仿真场景 下面会结合一下仿真场景介绍具体的API
选取,由于OffsetDateTime
基本能满足大部分场景,因此挑选OffsetDateTime
进行举例。
场景一:字符串输入转换为日期时间对象 一般在Web
应用的表单提交或者Reuqest Body
提交的内容中,需要把字符串形式的日期时间转换为对应的日期时间对象。Web
应用多数情况下会使用SpringMVC
,而SpringMVC
的消息转换器在处理application/json
类型的请求内容的时候会使用ObjectMapper
(Jackson
)进行反序列化。这里引入org.springframework.boot:spring-boot-starter-web:2.2.5.RELEASE
做一个演示。
引入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
模式的字符串。例如:
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)); }
请求如下:
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
日所有订单的总金额:
@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
,常用于查询比较多的定时任务或者数据迁移。
场景三:计算两个日期时间之间的差值 计算两个日期时间之间的差值也是很常见的场景,笔者遇到过的场景就是:运营需要导出一批用户数据,主要包括用户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 -092021 年父亲节是: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)