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.withMonth(6).with(TemporalAdjusters.dayOfWeekInMonth(3,DayOfWeek.SUNDAY)).toLocalDate().toString()));
        time = time.plusYears(1);
        System.out.println(String.format("%d年母亲节是:%s",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文摘》),不定期推送笔者原创技术文章(绝不抄袭或者转载):

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


应用场景 C端用户提交工单、工单创建完成之后、会发布一条工单创建完成的消息事件(异步消息)、MQ消费者收到消息之后、会通知各处理器处理该消息、各处理器处理完后都会发布一条将该工单写入搜索引擎的消息、最终该工单出现在搜索引擎、被工单处理人检索和处理。 事故异常体现 1、异常体现 从工单的流转记录发现、
线程类,设置有一个公共资源 package cn.org.chris.concurrent; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @Descrip
Java中的数字(带有0前缀和字符串)
在Java 9中使用JLink的目的是什么?
Java Stream API Filter(过滤器)
在Java中找到正数和负数数组元素的数量
Java 9中JShell中的不同启动脚本是什么?
使用Java的位填充错误检测技术
java中string是什么
如何使用Java中的JSON-lib API将Map转换为JSON对象?
Java菜单驱动程序以检查数字类型
使用Junit的Maven项目 - 检查银行账号
JAVA编程基础
在Java中使用throw、catch和instanceof来处理异常
在Java中,将数组分割为基于给定查询的子数组后,找到子数组的最大子数组和
如何在Java中从给定的字符串中删除HTML标签?
在PHP中,IntlChar getBlockCode()函数的翻译如下:
如何在Android中实现按下返回键再次退出的功能?
如何使用Java中的流式API解析JSON字符串?
Java中的模式类