【小家java】java8新特性之---全新的日期、时间API(JSR 310规范),附SpringMVC、Mybatis中使用JSR310的正确姿势(下)

简介: 【小家java】java8新特性之---全新的日期、时间API(JSR 310规范),附SpringMVC、Mybatis中使用JSR310的正确姿势(下)

获取一周、一个月、一年、一小时、一分钟后的日期等


LocalDate是用来表示无时间的日期,他又一个plus()方法可以用来增加日,星期,月,ChronoUnit则用来表示时间单位


image.png

表示和处理固定的日期,比如信用卡过期时间


YearMonth是另外一个组合,可以很好处理信用卡有效期只有年、月的问题。LengthOfMonth()这个方法返回的是这个YearMonth实例有多少天,这对于检查2月是否润2月很有用


image.png


两个日期之间包含多少天,多少月(这个非常实用)


计算两个日期之间包含多少天、周、月、年。可以用java.time.Period类完成该功能。下面例子中将计算日期与将来的日期之间一共有几个月


image.png


带时区的日期与时间(以后处理时区问题,还是用ZoneDateTime吧)


在java8中,可以使用ZoneOffset来代表某个时区,可以使用它的静态方法ZoneOffset.of()方法来获取对应的时区,只要获得了这个偏移量,就可以用这个偏移量和LocalDateTime创建一个新的OffsetDateTime


image.png


说明:OffsetDateTime主要是用来给机器理解的,平时使用就用前面结束的ZoneDateTime类就可以了


如何在两个日期之间获得所有日期


这个需求其实是比较常见的需求,所有很有必要在这里实现一把。因为其实实现起来并不见得那么简单,还有不少误区:所以我这里展开说一下


LocalDate start = LocalDate.of(2018, Month.DECEMBER, 1);
System.out.println(start.lengthOfMonth()); //31
        System.out.println(start.lengthOfYear()); //365


因此我们先造出两个日期出来,然后求出他们的差值如下:


 LocalDate start = LocalDate.of(2018, Month.DECEMBER, 1);
LocalDate end = LocalDate.of(2020, Month.APRIL, 10);


有的人可能第一眼可能会想到用Period来做:

 Period period = Period.between(start, end);
        System.out.println(period); //P1Y4M9D
        System.out.println(period.getYears()); //1
        System.out.println(period.getMonths()); //4
        System.out.println(period.getDays()); //9
//备注:Period period = start.until(end);  //效果同上


我们会发现,根本就就不是我们想要的。其实这里需要注意一点:从输出的值可以看出,Period得到的是差值的绝对值,而并不表示真正的区间距离。因为它表示一个时段,所以肯定是绝对值含义。


所以我们想到可以如下处理(方法一):


//先计算出两个日期的像个
long distance = ChronoUnit.DAYS.between(start, end);
//for循环往里面处理
for(int i = 0; i <= distance; i++){
    start.plusDays(i); //...do the stuff with the new date...
}


下面介绍一种更优雅的方案(方案二)


List<LocalDate> days = Stream.iterate(start, d -> d.plusDays(1)).limit(distance + 1).collect(toList());


采用迭代流来生成,显得逼格满满。


这里面穿插一下,ChronoUnit类。它像是一个单位类


start.plus(1,ChronoUnit.DAYS);
        //等价于
        start.plusDays(1);


下面这个需要注意,LocalDate本身具备的一种能力:


 long distance1 = start.until(end, ChronoUnit.DAYS);
        System.out.println(distance1); //496
        long distance2 = ChronoUnit.DAYS.between(start, end);
        System.out.println(distance2); //496


大赞Java8 时间API的设计,条条大路通罗马啊


如何在两个日期之间获得所有的月份


有了上面的额例子,这个自然不在话下。那么就继续来上代码:

//获取开始、结束日期内所有的月份
        long monthCount = ChronoUnit.MONTHS.between(start, end);
        Stream.iterate(start, x -> x.plusMonths(1)).limit(monthCount + 1).forEach(System.out::println);



照葫芦画瓢,只是简单的把单位换一下就ok了。


ZoneOffset 于 ZoneId


ZoneOffset 表示与UTC时区偏移的固定区域。

ZoneOffset不随着由夏令时导致的区域偏移的更改。


UTC是UTC的时区偏移常量(Z用作UtC时区的区域偏移指示符。)。MAX和MIN是最大和最小支持的区域偏移。


我们可以用小时,分钟和秒的组合创建 ZoneOffset 。

    public static void main(String[] args) {
        //一般只会用到Hours的便宜
        ZoneOffset zoneOffset1 = ZoneOffset.ofHours(-1); //-01:00
        System.out.println(zoneOffset1);
        ZoneOffset zoneOffset2 = ZoneOffset.ofHoursMinutes(6, 30); //+06:30
        System.out.println(zoneOffset2);
        ZoneOffset zoneOffset3 = ZoneOffset.ofHoursMinutesSeconds(9, 30, 45); //+09:30:45
        System.out.println(zoneOffset3);
    }

以下代码显示如何从偏移创建区域偏移。


    public static void main(String[] args) {
        ZoneOffset zoneOffset1 = ZoneOffset.of("+05:00"); //+05:00
        ZoneOffset zoneOffset2 = ZoneOffset.of("Z"); //Z   效果同:ZoneOffset.UTC
        System.out.println(zoneOffset1);
        System.out.println(zoneOffset2);
    }

API支持-18:00到+18:00之间的区域偏移。


ZoneId 表示区域偏移及其用于更改区域偏移的规则夏令时。

每个时区都有一个ID,可以用三种格式定义:



   在区域偏移中,可以是“Z”,“+ hh:mm:ss”或“-hh:mm:ss”,例如“+01:00”。


   前缀为“UTC”,“GMT”或“UT”,后跟区域偏移量,例如“UTC + 01:00”。


   在区域名称中,例如,“美洲/芝加哥”。(比较常用)


以下代码显示如何使用of()工厂方法创建ZoneId。


    public static void main(String[] args) {
        //备注:此字符串必须合法   否则报错
        ZoneId usChicago = ZoneId.of("Asia/Shanghai"); //Asia/Shanghai
        System.out.println(usChicago);
        ZoneId fixedZoneId = ZoneId.of("+01:00");
        System.out.println(fixedZoneId); //+01:00
    }

ZoneId 中的 getAvailableZoneIds()返回所有已知时区ID

    public static void main(String[] args) {
        System.out.println(ZoneId.systemDefault()); //Asia/Shanghai
        System.out.println(ZoneId.getAvailableZoneIds()); //[Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8
    }


使用java8我们知道使用ZoneId.default()可以获得系统默认值ZoneId,但如何获取默认值ZoneOffset?我看到一个ZoneId有一些“规则”而且每个规则都有一个ZoneOffset,这意味着一个ZoneId可能有一个以上ZoneOffset吗?答案如下:


    public static void main(String[] args) {
        System.out.println(ZoneOffset.of("+8")); //+08:00
        System.out.println(ZoneOffset.ofHours(8)); //+08:00
        //获取系统的默认值==================推荐使用
        System.out.println(OffsetDateTime.now().getOffset()); //+08:00
        System.out.println(ZoneId.systemDefault()); //Asia/Shanghai
    }


Spring MVC、MyBatis、Feign中使用JSR310的日期



首先你需要引入对应的Jar(这是很多人不知道怎么支持的最重要原因)


    <-- 让Mybatis支持JSR310 -->
    <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-typehandlers-jsr310</artifactId>
            <version>1.0.2</version>
        </dependency>
         <-- 让SpringMVC支持JSR310 -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.9.7</version>
        </dependency>



备注:


如果你是SpringBoot环境,SpringMVC依赖的版本号version都可以省略,而且建议省略。SpringBoot2.0以上版本,不需要自己再额外导入SpringMVC的那个JSR310依赖的jar,因为默认就自带了


如果你的Mybatis版本在3.4.0以上,导包就支持。如果在3.4.0一下版本,就需要自己手动配置文件里注册(不过我建议直接升MyBatis版本吧)


重点说明:MyBatis @since 3.4.5(2017.8月份发布)之后,就内置了对jsr310的支持,不用再额外导包了哦~


包名都没有改变,所以若你的MyBatis在3.4.5以上的版本,直接移除掉你jackson-datatype-jsr310这个pom就行了


建议以后放弃使用Date和Timestamp类型。

DB的entiry使用LocalDateTime对应sql的datetime、LocalDate对应date、LocalTime对应time 足够你用的了,而且安全性更高


为何能够处理这些时间?看到下面截图一目了然:


image.png


导入之后:SpringMVC传入参数如下:


{
  "startDate" : "2018-11-01"  //“2018/11/01”默认是非法的
}


服务端直接这样接受就行:


@NotNull
private LocalDate startDate; //什么注解都不需要


注解@DateTimeFormat只对Date类型有效,对JSR类型都将无效


需要注意的是,LocalDate使用这种格式的串没问题。但LocalDateTime可不行。比如:


{
  "startDateTime" : "2018-11-01 18:00:00"  //这个是非法的  而"2018-11-24T09:04:16.383" 这种格式才是默认合法的
}

为什么呢?进源码看一下:LocalDateTimeSerializer类有这么一句


    protected DateTimeFormatter _defaultFormatter() {
        return DateTimeFormatter.ISO_LOCAL_DATE_TIME; //它的值是形如这种格式的模版"2018-11-24T09:04:16.383"
    }


其实从他们的默认的toString()方法也能看出一点端倪:

    public static void main(String[] args) {
        System.out.println(LocalDateTime.now()); //2018-11-24T17:12:27.395
        System.out.println(LocalDate.now()); //2018-11-24
        System.out.println(LocalTime.now()); //17:12:57.323
    }


那么问题来了,怎么样才能让LocalDateTime友好的接受我们想的那种字符串呢?

方案一:自己写一个LocalDateTimeSerializer的实现,然后通过@JsonSerialize指定序列化器

方法二(推荐):在字段上面采用@JsonFormat指定序列化以及反序列化的格式

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime localDateTime;


小知识:


SpringMVC默认采用Jackson进行序列化和反序列话。对于时间类型的默认的序列化(序列化表示把对象对外输出,如SpringMVC的返回值就需要经过这个过程):


   Date类型按照GMT标准时间 成时间戳


   Timestamp类型按照GMT标准时间 成时间戳


   LocalDate:“startDate”: [ 2018,11,1] 序列化成数组类型


显然LocalDate等类型序列化成数组,是不优雅的方案。而且如果你使用的是feign进行API调用的话,肯定报错。因为对方根本不能识别这个数组,我们希望序列化的结果是:“2018-11-01”这样子优雅,切feign也能正常使用了,咋办呢?


方案:

1、各种自定义类型转换器(这里不做过多讲解)

2、采用全局的converter转换器

3、采用@JsonFormat(pattern = “yyyy-MM-dd”) 注解标注字段输出(推荐)

@Bean
    public ObjectMapper serializingObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());
        return objectMapper;
    }


SpringMVC Get请求中,LocalDateTime、LocalDate等JSR310的反序列化处理



本以为Get请求和上面一样,加一个@JsonFormat就可以了,但我这么做


    @ApiOperation("测试接受时间类型Get")
    @PostMapping("/test/jsr310")
    Object testJsrGet(@RequestParam @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime localDateTime) {
        System.out.println(localDateTime);
        return localDateTime;
    }


客户端传值:


"startDateFrom" : "2018-11-01 18:00:00"


按照上面的理论,本以为没问题了,但奈何,还是出错了。怎么破?


Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime'; 


杀千刀的,通过打断点跟踪发现,在解析时间的时候。SptingMVC调用的竟然是自己内部的解析器,根本就没有用到fastjson,因此那个注解自然而然没有作用,确实有点坑啊。


这里有一个类:TemporalAccessorParser:parse


  @Override
  public TemporalAccessor parse(String text, Locale locale) throws ParseException {
    DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(this.formatter, locale);
    if (LocalDate.class == this.temporalAccessorType) {
      return LocalDate.parse(text, formatterToUse);
    }
    else if (LocalTime.class == this.temporalAccessorType) {
      return LocalTime.parse(text, formatterToUse);
    }
    else if (LocalDateTime.class == this.temporalAccessorType) {
      return LocalDateTime.parse(text, formatterToUse);
    }
    else if (ZonedDateTime.class == this.temporalAccessorType) {
      return ZonedDateTime.parse(text, formatterToUse);
    }
    else if (OffsetDateTime.class == this.temporalAccessorType) {
      return OffsetDateTime.parse(text, formatterToUse);
    }
    else if (OffsetTime.class == this.temporalAccessorType) {
      return OffsetTime.parse(text, formatterToUse);
    }
    else {
      throw new IllegalStateException("Unsupported TemporalAccessor type: " + this.temporalAccessorType);
    }
  }


我发现JSR310的类型都是交给他解析的,然后它使用的就是默认的模版。

那怎么办?怎么替换成我们自己的时间模版?所以我找到了它注册的地方:


@UsesJava8
public class DateTimeFormatterRegistrar implements FormatterRegistrar {}


看看注册的模版:


@Override
  public void registerFormatters(FormatterRegistry registry) {
    DateTimeConverters.registerConverters(registry);
    DateTimeFormatter df = getFormatter(Type.DATE);
    DateTimeFormatter tf = getFormatter(Type.TIME);
    DateTimeFormatter dtf = getFormatter(Type.DATE_TIME);
    // Efficient ISO_LOCAL_* variants for printing since they are twice as fast...
    registry.addFormatterForFieldType(LocalDate.class,
        new TemporalAccessorPrinter(
            df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df),
        new TemporalAccessorParser(LocalDate.class, df));
    registry.addFormatterForFieldType(LocalTime.class,
        new TemporalAccessorPrinter(
            tf == DateTimeFormatter.ISO_TIME ? DateTimeFormatter.ISO_LOCAL_TIME : tf),
        new TemporalAccessorParser(LocalTime.class, tf));
    registry.addFormatterForFieldType(LocalDateTime.class,
        new TemporalAccessorPrinter(
            dtf == DateTimeFormatter.ISO_DATE_TIME ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : dtf),
        new TemporalAccessorParser(LocalDateTime.class, dtf));
    registry.addFormatterForFieldType(ZonedDateTime.class,
        new TemporalAccessorPrinter(dtf),
        new TemporalAccessorParser(ZonedDateTime.class, dtf));
    registry.addFormatterForFieldType(OffsetDateTime.class,
        new TemporalAccessorPrinter(dtf),
        new TemporalAccessorParser(OffsetDateTime.class, dtf));
    registry.addFormatterForFieldType(OffsetTime.class,
        new TemporalAccessorPrinter(tf),
        new TemporalAccessorParser(OffsetTime.class, tf));
    registry.addFormatterForFieldType(Instant.class, new InstantFormatter());
    registry.addFormatterForFieldType(Period.class, new PeriodFormatter());
    registry.addFormatterForFieldType(Duration.class, new DurationFormatter());
    registry.addFormatterForFieldType(YearMonth.class, new YearMonthFormatter());
    registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter());
    registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory());
  }


这就无需多余解释了,都是采用的ISO标准模版。还好他给我们提供了对应的set方法,因此我想到了自定义


注册的地方DefaultFormattingConversionService:addDefaultFormatters

  public static void addDefaultFormatters(FormatterRegistry formatterRegistry) {
    // Default handling of number values
    formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
    // Default handling of monetary values
    if (jsr354Present) {
      formatterRegistry.addFormatter(new CurrencyUnitFormatter());
      formatterRegistry.addFormatter(new MonetaryAmountFormatter());
      formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
    }
    // Default handling of date-time values
    if (jsr310Present) {
      // just handling JSR-310 specific date and time types
      new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry);
    }
    if (jodaTimePresent) {
      // handles Joda-specific types as well as Date, Calendar, Long
      new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry);
    }
    else {
      // regular DateFormat-based Date, Calendar, Long converters
      new DateFormatterRegistrar().registerFormatters(formatterRegistry);
    }
  }


发现是new出来的,因此我们还不能直接从容器里面注入。确实不太好弄了。。。。


还好,经过我最终的源码跟踪,发现他解析了@DateTimeFormat注解,因此我试试用了这个注解

    @ApiOperation("测试接受时间类型Get")
    @PostMapping("/test/jsr310/get")
    Object testJsrGet(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime localDateTime) {
        System.out.println(localDateTime);
        return localDateTime;
    }


bingo, 没毛病了,完美解决问题。


最后,我们发现。SpringMVC对body体里面的反序列化和对get请求参数的反序列化的机制是不一样的。因此大家使用的时候要倍加注意啊

相关文章
|
5天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
18 2
|
12天前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
35 4
|
1月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
108 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
21天前
|
Java API 数据处理
探索Java中的Lambda表达式与Stream API
【10月更文挑战第22天】 在Java编程中,Lambda表达式和Stream API是两个强大的功能,它们极大地简化了代码的编写和提高了开发效率。本文将深入探讨这两个概念的基本用法、优势以及在实际项目中的应用案例,帮助读者更好地理解和运用这些现代Java特性。
|
24天前
|
搜索推荐 Java 数据库连接
Java|在 IDEA 里自动生成 MyBatis 模板代码
基于 MyBatis 开发的项目,新增数据库表以后,总是需要编写对应的 Entity、Mapper 和 Service 等等 Class 的代码,这些都是重复的工作,我们可以想一些办法来自动生成这些代码。
30 6
|
27天前
|
Java 大数据 API
别死脑筋,赶紧学起来!Java之Steam() API 常用方法使用,让开发简单起来!
分享Java Stream API的常用方法,让开发更简单。涵盖filter、map、sorted等操作,提高代码效率与可读性。关注公众号,了解更多技术内容。
|
1月前
|
存储 Java API
如何使用 Java 中的 API 更改 PDF 纸张大小
如何使用 Java 中的 API 更改 PDF 纸张大小
43 11
|
1月前
|
机器学习/深度学习 算法 Java
通过 Java Vector API 利用 SIMD 的强大功能
通过 Java Vector API 利用 SIMD 的强大功能
42 10
|
1月前
|
分布式计算 Java 大数据
大数据-147 Apache Kudu 常用 Java API 增删改查
大数据-147 Apache Kudu 常用 Java API 增删改查
28 1
|
2月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)