【小家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请求参数的反序列化的机制是不一样的。因此大家使用的时候要倍加注意啊

相关文章
|
4月前
|
JSON API PHP
万年历API接口详解:精准获取指定日期信息
本文介绍接口盒子提供的万年历API,支持获取农历、节气、宜忌、星座等信息,具备完整的请求与返回示例,适用于黄历、日程管理等应用开发。
1274 0
|
5月前
|
SQL Java 数据库
解决Java Spring Boot应用中MyBatis-Plus查询问题的策略。
保持技能更新是侦探的重要素质。定期回顾最佳实践和新技术。比如,定期查看MyBatis-Plus的更新和社区的最佳做法,这样才能不断提升查询效率和性能。
234 1
|
10月前
|
安全 Java 数据库连接
Java使用MyBatis-Plus的OR
通过MyBatis-Plus的条件构造器,Java开发者可以方便地进行复杂的查询条件组合,包括AND和OR条件的灵活使用。熟练掌握这些技巧,可以显著提升开发效率和代码可读性。
389 20
|
10月前
|
算法 Java API
Java 方法注释:规范、实用和高质量的写法
本文深入探讨了如何编写高质量的 Java 方法注释
611 11
|
10月前
|
SQL Java 数据库连接
【潜意识Java】MyBatis中的动态SQL灵活、高效的数据库查询以及深度总结
本文详细介绍了MyBatis中的动态SQL功能,涵盖其背景、应用场景及实现方式。
1088 6
|
10月前
|
SQL Java 数据库连接
【潜意识Java】深入理解MyBatis的Mapper层,以及让数据访问更高效的详细分析
深入理解MyBatis的Mapper层,以及让数据访问更高效的详细分析
1620 1
|
10月前
|
SQL Java 数据库连接
【潜意识Java】深入理解MyBatis,从基础到高级的深度细节应用
本文详细介绍了MyBatis,一个轻量级的Java持久化框架。内容涵盖MyBatis的基本概念、配置与环境搭建、基础操作(如创建实体类、Mapper接口及映射文件)以及CRUD操作的实现。此外,还深入探讨了高级特性,包括动态SQL和缓存机制。通过代码示例,帮助开发者更好地掌握MyBatis的使用技巧,提升数据库操作效率。总结部分强调了MyBatis的优势及其在实际开发中的应用价值。
278 1
|
10月前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
456 2
|
10月前
|
SQL Java 数据库连接
Java MyBatis 面试题
Java MyBatis相关基础面试题
196 5
|
12月前
|
安全 IDE Java
Java常见规范及易忘点
遵循Java编程规范和注意易忘点是提高代码质量和可维护性的关键。通过规范的命名、格式、注释和合理的代码组织,可以让代码更加清晰和易于维护。同时,注意空指针检查、线程安全、集合框架和字符串操作等常见易忘点,可以减少程序错误,提高运行效率。结合单一职责原则、面向接口编程和合理的异常处理,能够编写出高质量的Java代码。希望本文能够帮助Java开发者提升编码水平,写出更高效、更可靠的代码。
129 2