zoukankan      html  css  js  c++  java
  • JSR310新日期API(完结篇)-生产实战

    前提

    前面通过五篇文章基本介绍完JSR-310常用的日期时间API以及一些工具类,这篇博文主要说说笔者在生产实战中使用JSR-310日期时间API的一些经验。

    系列文章:

    ::: info
    不经意间,JDK8发布已经超过6年了,如果还在用旧的日期时间API,可以抽点时间熟悉一下JSR-310的日期时间API。
    :::

    仿真场景

    下面会结合一下仿真场景介绍具体的API选取,由于OffsetDateTime基本能满足大部分场景,因此挑选OffsetDateTime进行举例。

    场景一:字符串输入转换为日期时间对象

    一般在Web应用的表单提交或者Reuqest Body提交的内容中,需要把字符串形式的日期时间转换为对应的日期时间对象。Web应用多数情况下会使用SpringMVC,而SpringMVC的消息转换器在处理application/json类型的请求内容的时候会使用ObjectMapperJackson)进行反序列化。这里引入org.springframework.boot:spring-boot-starter-web:2.2.5.RELEASE做一个演示。

    引入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模式的字符串。例如:

    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-100:00:0023: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-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)

    技术公众号(《Throwable文摘》),不定期推送笔者原创技术文章(绝不抄袭或者转载):

    娱乐公众号(《天天沙雕》),甄选奇趣沙雕图文和视频不定期推送,缓解生活工作压力:

  • 相关阅读:
    垂死挣扎还是涅槃重生 -- Delphi XE5 公布会归来感想
    自考感悟,话谈备忘录模式
    [每日一题] OCP1z0-047 :2013-07-26 alter table set unused之后各种情况处理
    Java实现 蓝桥杯 算法提高 p1001
    Java实现 蓝桥杯 算法提高 拿糖果
    Java实现 蓝桥杯 算法提高 拿糖果
    Java实现 蓝桥杯 算法提高 求arccos值
    Java实现 蓝桥杯 算法提高 求arccos值
    Java实现 蓝桥杯 算法提高 因式分解
    Java实现 蓝桥杯 算法提高 因式分解
  • 原文地址:https://www.cnblogs.com/throwable/p/12405693.html
Copyright © 2011-2022 走看看