天天看点

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

相关阅读

【小家java】java5新特性(简述十大新特性) 重要一跃

【小家java】java6新特性(简述十大新特性) 鸡肋升级

【小家java】java7新特性(简述八大新特性) 不温不火

【小家java】java8新特性(简述十大新特性) 饱受赞誉

【小家java】java9新特性(简述十大新特性) 褒贬不一

【小家java】java10新特性(简述十大新特性) 小步迭代

【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本

【小家java】java8新特性之—Base64加密和解密原理

【小家java】java8新特性之—反射获取方法参数名

【小家java】java8新特性之—全新的日期、时间API(完全实现了JSR 310规范)

【小家java】java8新特性之—Optional的使用,避免空指针,代替三目运算符

【小家java】java8新特性之—lambda表达式的的原理

【小家java】java8新特性之—函数式接口(Supplier、Consumer、Predicate、Function、UnaryOperator,通往高阶设计的好工具)

【小家java】java8新特性之—方法引用

【小家java】java8新特性之—Stream API 详解 (Map-reduce、Collectors收集器、并行流)

【小家java】java8新特性之—外部迭代和内部迭代(对比性能差异)

每篇一句

男人要么帅一点,要么努力一点。如果你又帅又努力,那就可以拽一点

Java8之前的日期、时间现状

Tiago Fernandez做了一个很有意思的投票,统计对Java API的不满意程度,最终Java Date/Time/Calendar API被评为最烂API第二名(第一为XML/DOM)。

Java三次引入处理时间的API,JDK1.0中包含了一个Date类,但大多数方法在java1.1引入Calendear类之后被弃用了。

它的实例都是可变的,而且它的API很难使用,比如月份是从0开始这种反人类的设置。不止如此,还有如下的一些使用不方便的地方

其实JSR310的规范领导者Stephen Colebourne,同时也是Joda-Time的创建者,JSR310是在Joda-Time的基础上建立的,参考了绝大部分的API,但并不是说JSR310=JODA-Time,还是有好些区别的
  • Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期累,此外用于格式化和解析的类在java.text包中定义。
  • Java 8之前老版的 java.util.Date 类以及其他用于建模日期时间的类有很多不一致及 设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名
  • java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql并不合理。
public static void main(String[] args) {
         java.util.Date date = new Date(System.currentTimeMillis());
         java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
         System.out.println(date); //Sat Aug 04 10:35:40 CST 2018
         System.out.println(sqlDate); //2018-08-04
    }
           
  • 对于时间、时间戳、格式化以及解析,并没有明确定义的类。对于格式化和解析的需求,有java.text.DateFormat抽象类,但通常情况下,SimpleDateFormat类被用于此类需求(关键是它还不是线程安全的)。
  • 日期类国际化支持的并不是很好

关于日期定义的一些常识

现实生活的世界里,时间是不断向前的,如果向前追溯时间的起点,可能是宇宙出生时,又或是是宇宙出现之前, 但肯定是我们目前无法找到的,我们不知道现在距离时间原点的精确距离。所以我们要表示时间, 就需要人为定义一个原点。

原点被规定为,格林威治时间(GMT)1970年1月1日的午夜 为起点,之于为啥是GMT时间,大概是因为本初子午线在那的原因吧。

Java8中日期、时间类的概述

Java8时间API最重要的几个类:

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

所有类都实现了 Temporal 接口, Temporal 接口定义了如何读取和操纵

java8引入了一套全新的时间日期API。java.time包中的是类是不可变且线程安全的。新的时间及日期API位于java.time中,下面是一些关键类

●Instant——它代表的是时间戳(另外可参考Clock类)

●LocalDate——不包含具体时间的日期,比如2014-01-14。它可以用来存储生日,周年纪念日,入职日期等。

●LocalTime——它代表的是不含日期的时间

●LocalDateTime——它包含了日期及时间,不过还是没有偏移信息或者说时区。

●ZonedDateTime——这是一个包含时区的完整的日期时间还有时区,偏移量是以UTC/格林威治时间为基准的。

●Timezones——时区。在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of来获取到。 时区定义了到UTS时间的时间差,在Instant时间点对象到本地日期对象之间转换的时候是极其重要的。

Java8日期、时间API特点和使用的设计模式

  • 不变性:新的日期/时间API中,所有的类都是不可变的,这对多线程环境有好处。
  • **关注点分离(这点个人认为在设计中非常非常重要):**新的API将人可读的日期时间和机器时间(unix timestamp)明确分离,它为日期(Date)、时间(Time)、日期时间(DateTime)、时间戳(unix timestamp)以及时区定义了不同的类。
  • 清晰:在所有的类中,方法都被明确定义用以完成相同的行为。举个例子,要拿到当前实例我们可以使用now()方法,在所有的类中都定义了format()和parse()方法,而不是像以前那样专门有一个独立的类。为了更好的处理问题,所有的类都使用了工厂模式和策略模式(策略模式在设计一整套东西的时候,特别有效,可以对开发者友好),一旦你使用了其中某个类的方法,与其他类协同工作并不困难。
  • 实用操作(相当于很多工具方法,不再需要我们自己封装了):所有新的日期/时间API类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分,等等。
  • TemporalAdjuster 让你能够用更精细的方式操纵日期,不再局限于一次只能改变它的 一个值,并且你还可按照需求定义自己的日期转换器

Java8日期、时间API包介绍

  • **java.time包:**这是新的Java日期/时间API的基础包,所有的主要基础类都是这个包的一部分,如:LocalDate, LocalTime, LocalDateTime, Instant, Period, Duration等等。所有这些类都是不可变的和线程安全的,在绝大多数情况下,这些类能够有效地处理一些公共的需求。
  • **java.time.chrono包:**这个包为非ISO的日历系统定义了一些泛化的API,我们可以扩展AbstractChronology类来创建自己的日历系统。
  • **java.time.format包:**这个包包含能够格式化和解析日期时间对象的类,在绝大多数情况下,我们不应该直接使用它们,因为java.time包中相应的类已经提供了格式化和解析的方法。
  • **java.time.temporal包:**这个包包含一些时态对象,我们可以用其找出关于日期/时间对象的某个特定日期或时间,比如说,可以找到某月的第一天或最后一天。你可以非常容易地认出这些方法,因为它们都具有“withXXX”的格式。
  • **java.time.zone包:**这个包包含支持不同时区以及相关规则的类

Java8常用的类介绍

Instant和Clock

Instant它是精确到纳秒的(而不是象旧版本的Date精确到毫秒,System.nanoTime是精确到纳秒级别了),如果使用纳秒去表示一个时间则原来使用一位Long类型是不够的,需要占用更多一点的存储空间,所以它内部是用两个字段去存储的。第一个部分保存的是自标准Java计算时代(就是1970年1月1日开始)到现在的秒数,第二部分保存的是纳秒数(永远不会超过999,999,999)

在新的时间API中,Instant表示一个精确的时间点,Duration和Period表示两个时间点之间的时间量(所以我们比较两个时间差,用新API更方便了,后面会有示例)。

Instant表示一个精确的时间,时间数轴就是由无数个时间点组成,数轴的原点就是上面提 到的1970-1-1 00:00:00,Instant由两部分组成,一是从原点开始到指定时间点的秒数s(用long存储), 二是距离该秒数s的纳秒数(用int存储)。源码:

private static final long MIN_SECOND = -31557014167219200L;
private static final long MAX_SECOND = 31556889864403199L;
//还定义了两个最大、最小时间的常量,我们以后可以直接使用
public static final Instant MIN = Instant.ofEpochSecond(MIN_SECOND, 0);
public static final Instant MAX = Instant.ofEpochSecond(MAX_SECOND, 999_999_999);
//引用一个long和一个int来存储秒和距离秒的纳秒
private final long seconds;
private final int nanos;
//我们会发现 now的底层,调用的其实是Clock的方法
public static Instant now() {
   return Clock.systemUTC().instant();
}
//策略模式,自带parse方法,把字符串解析成Instant
public static Instant parse(final CharSequence text) {
        return DateTimeFormatter.ISO_INSTANT.parse(text, Instant::from);
    }
//优雅的比较方案:
 public boolean isAfter(Instant otherInstant) {
     return compareTo(otherInstant) > 0;
 }
 public boolean isBefore(Instant otherInstant) {
     return compareTo(otherInstant) < 0;
 }
           

下面看获取当前时间戳的几个方法:

public static void main(String[] args) {
        Instant now = Instant.now();
        System.out.println(now); //2018-08-04T06:35:59.354Z
        System.out.println(now.getEpochSecond()); //1533364559
        System.out.println(now.getNano()); //354000000
            
<span class="token comment">//下面是几种获取时间戳(毫秒值)的方法 推荐使用高逼格的toEpochMilli()去做</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>now<span class="token punctuation">.</span><span class="token function">toEpochMilli</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>System<span class="token punctuation">.</span><span class="token function">currentTimeMillis</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
           

还有一些对plus、minus、isAfter、isBefore等方法,此处不做多余讲解

下面介绍两个比较实用的方法:

public static void main(String[] args) {
        //自带的解析 若需要自定义格式,可以这么来
        Instant temp =Instant.parse("2007-12-03T10:15:30.00Z");
            
Instant now <span class="token operator">=</span> Instant<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    Instant instant <span class="token operator">=</span> now<span class="token punctuation">.</span><span class="token function">plusSeconds</span><span class="token punctuation">(</span>TimeUnit<span class="token punctuation">.</span>HOURS<span class="token punctuation">.</span><span class="token function">toSeconds</span><span class="token punctuation">(</span><span class="token number">25</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">//希望得到两个时间戳,他们相隔了几个小时、几天、几个月?</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>now<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>instant<span class="token punctuation">,</span>ChronoUnit<span class="token punctuation">.</span>HOURS<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//25</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>now<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>instant<span class="token punctuation">,</span>ChronoUnit<span class="token punctuation">.</span>DAYS<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//1(这里显示1不是2哦)</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>instant<span class="token punctuation">.</span><span class="token function">until</span><span class="token punctuation">(</span>now<span class="token punctuation">,</span>ChronoUnit<span class="token punctuation">.</span>HOURS<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//-25(注意,这里是负数哦)</span>
<span class="token punctuation">}</span>
           

以前我们要统计一段程序的运行时间,现在可以采用这种优雅的方式了

Instant start = Instant.now();
doSomething();
Instant end = Instant.now();
//计算时间差 采用Duration来处理时间戳的差
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();
System.out.println("millis = " + millis);
           
大概300年的纳秒值会导致long值溢出。所以毫秒值用long存储,永远都不会溢出

java.time.Duration表示一段时间。所以像电影持续多久,要做同步字幕的话,用这个类可以很好的解决问题。Duration可以进行multipliedBy()乘法和dividedBy()除法运算。negated()做取反运算,即1.2秒取反后为-1.2秒。

简单的说下clock:时钟提供给我们用于访问某个特定 时区的 瞬时时间、日期 和 时间的。

//这么来会采用系统默认的时区

Clock c2 = Clock.systemDefaultZone(); //系统默认时区时钟(当前瞬时时间)

//输出那两个能看到效果

System.out.println(c1); //SystemClock[Z] 这个其实用得最多

System.out.println(c2); //SystemClock[Asia/Shanghai]

//可以获取到和时区敏感的对象

Clock c3 = Clock.system(ZoneId.of(“Europe/Paris”)); //巴黎时区

Clock c5 = Clock.offset(c1, Duration.ofSeconds(2)); //相对于系统默认时钟两秒的时钟

LocalDate(本地日期)

上面介绍的Instant是一个绝对的准确时间点,是人类不容易理解的时间,现在介绍人类使用的时间。

API的设计者推荐使用不带时区的时间,除非真的希望表示绝对的时间点。

可以使用静态方法now()和of()创建LocalDate。

public static void main(String[] args) {
        //获取当前日期
        LocalDate now = LocalDate.now();
        //2017-01-01
        LocalDate newYear = LocalDate.of(2017, 1, 1);
        System.out.println(now); //2018-08-04
        System.out.println(newYear); //2017-01-01
    }
//显然,内置很多plus、minus的基本计算。with方法相当于修改,但返回的是一个新的日期对象哦
//三天后
now.plusDays(3);
//一周后
now.plusWeeks(1)
//两天前 
now.minusDays(2)
           

//备注:增加一个月不会出现2017-02-31 而是会返回该月的最后一个有效日期,即2017-02-28,这点特别的人性化有木有

LocalDate.of(2017, 1, 31).plusMonths(1)

LocalDate对应的表示时间段的是Period, Period内部使用三个int值分表表示年、月、日。 Duration和Period都是TemporalAmount接口的实现,该接口表示时间量

LocalDate 也可以增加或减少一段时间(自由度更高):

//2019-02-01
feb.plus(Period.ofYears(2));
//2015-02-01
feb.minus(Period.ofYears(2);
           

//使用until获得两个日期之间的Period对象

feb.until(LocalDate.of(2017, 2, 10));//输出—> P9D

//提供isLeapYear判断是否是闰年,这个太友好了

//DayOfWeek 是个枚举,并且实现了TemporalAccessor/TemporalAdjuster接口,所以也可以直接plus,minus等,非常方便。 Java8还提供了Year MonthDay YearMonth来表示部分日期,例如MonthDay可以表示1月1日。

LocalDate.of(2017, 1, 1).getDayOfWeek();

DayOfWeek.SUNDAY.plus(2); //TUESDAY

LocalTime(本地时间)

LocalTime表示一天中的某个时间,例如18:00:00。LocaTime与LocalDate类似,他们也有相似的API。所以这里不做详细介绍了

public static void main(String[] args) {
        LocalTime now = LocalTime.now();
        LocalTime evning = LocalTime.of(21, 0);
        System.out.println(now); //17:03:13.728
        System.out.println(evning); //10:00
    }
           

LocalDateTime(本地日期和时间)

LocalDateTime表示一个日期和时间,它适合用来存储确定时区的某个时间点。不适合跨时区的问题。

public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime of = LocalDateTime.of(LocalDate.now(), LocalTime.now());
        System.out.println(now); //2018-08-04T18:33:37.478
        System.out.println(of); //2018-08-04T18:33:37.478
    }
           

ZonedDateTime(带时区的 日期和时间)

Java8使用ZoneId来标识不同的时区.

public static void main(String[] args) {
        //获得所有可用的时区  size=600 这个数字不是固定的
        Set<String> allZones = ZoneId.getAvailableZoneIds();
        //获取默认ZoneId对象 系统当前所在时区
        ZoneId defZoneId = ZoneId.systemDefault();
        //获取指定时区的ZoneId对象
        ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
        //ZoneId.SHORT_IDS返回一个Map<String, String> 是时区的简称与全称的映射。下面可以得到字符串 Asia/Shanghai
        String shanghai = ZoneId.SHORT_IDS.get("CTT");
        System.out.println(shanghai); //Asia/Shanghai
    }
           

IANA(Internet Assigned Numbers Authority,因特网拨号管理局)维护着一份全球所有已知的时区数据库,

每年会更新几次,主要处理夏令时规则的改变。Java使用了IANA的数据库。

public static void main(String[] args) {
        //2017-01-20T17:35:20.885+08:00[Asia/Shanghai]
        ZonedDateTime now = ZonedDateTime.now();
        //2017-01-01T12:00+08:00[Asia/Shanghai]
        ZonedDateTime of = ZonedDateTime.of(2017, 1, 1, 12, 0, 0, 0, ZoneId.of("Asia/Shanghai"));
        //使用一个准确的时间点来创建ZonedDateTime,下面这个代码会得到当前的UTC时间,会比北京时间早8个小时
        ZonedDateTime utc = ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC"));
        System.out.println(now); //2018-08-04T18:53:24.686+08:00[Asia/Shanghai]
        System.out.println(of); //2017-01-01T12:00+08:00[Asia/Shanghai]
        System.out.println(utc); //2018-08-04T10:53:24.687Z[UTC]
    }
           

ZonedDateTime的许多方法与LocalDateTime、LocalDate、LocalTime类似

LocalDateTime转换为带时区的ZonedDateTime

//atZone方法可以将LocalDateTime转换为ZonedDateTime,下面的方法将时区设置为UTC。
//假设现在的LocalDateTime是2017-01-20 17:55:00 转换后的时间为2017-01-20 17:55:00[UTC]
LocalDateTime.now().atZone(ZoneId.of("UTC"));
//使用静态of方法创建zonedDateTime
ZonedDateTime.of(LocalDateTime.now(), ZoneId.of("UTC"));
           

实用常量们

public static void main(String[] args) {
        //Instant的常量
        System.out.println(Instant.MIN); //-1000000000-01-01T00:00:00Z
        System.out.println(Instant.MAX); //+1000000000-12-31T23:59:59.999999999Z
            
<span class="token comment">//LocaDate的常量</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>LocalDate<span class="token punctuation">.</span>MIN<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//-999999999-01-01</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>LocalDate<span class="token punctuation">.</span>MAX<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//+999999999-12-31</span>

    <span class="token comment">//LocalTime的常量</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>LocalTime<span class="token punctuation">.</span>MIN<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//00:00</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>LocalTime<span class="token punctuation">.</span>MAX<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//23:59:59.999999999</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>LocalTime<span class="token punctuation">.</span>MIDNIGHT<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//00:00</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>LocalTime<span class="token punctuation">.</span>NOON<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//12:00</span>

    <span class="token comment">//LocalDateTime的常量</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>LocalDateTime<span class="token punctuation">.</span>MIN<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//-999999999-01-01T00:00</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>LocalDateTime<span class="token punctuation">.</span>MAX<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//+999999999-12-31T23:59:59.999999999</span>

    <span class="token comment">//ZoneOffset的常量</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>ZoneOffset<span class="token punctuation">.</span>UTC<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//Z</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>ZoneOffset<span class="token punctuation">.</span>MIN<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//-18:00</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>ZoneOffset<span class="token punctuation">.</span>MAX<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//+18:00</span>

    <span class="token comment">//ZoneId的常量</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>ZoneId<span class="token punctuation">.</span>SHORT_IDS<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//{CTT=Asia/Shanghai, ART=Africa/Cairo, CNT=America/St_Johns, PRT=America/Puerto_Rico</span>
<span class="token punctuation">}</span>
           

新的API 格式化(字符串 -><- 字符串 互转)

public static void main(String[] args) {
        //字符串转化为日期对象
        String dateStr= "2016年10月25日";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
        LocalDate date= LocalDate.parse(dateStr, formatter);
            
<span class="token comment">//日期转换为字符串</span>
    LocalDateTime now <span class="token operator">=</span> LocalDateTime<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    DateTimeFormatter format <span class="token operator">=</span> DateTimeFormatter<span class="token punctuation">.</span><span class="token function">ofPattern</span><span class="token punctuation">(</span><span class="token string">"yyyy年MM月dd日 hh:mm a"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    String nowStr <span class="token operator">=</span> now <span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span>format<span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>nowStr<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//2018年08月07日 12:15 上午</span>
<span class="token punctuation">}</span>
           
  • DateTimeFormatter预定义了一些格式,可以直接调用format方法,方便调用者使用
//2017-01-01
DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.of(2017, 1, 1))
//20170101
DateTimeFormatter.BASIC_ISO_DATE.format(LocalDate.of(2017, 1, 1));
//2017-01-01T09:10:00
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.of(2017, 1, 1, 9, 10, 0)); 
           

根据当前操作系统语言环境,有SHORET MEDIUM LONG FULL 四种不同的风格来格式化。

可以通过DateTimeFormatter的静态方法ofLocalizedDate ofLocalizedTime ofLocalizedDateTime

  • 使用自定义模式格式化
//2017-02-27 22:48:52
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now())
           

当然也可以这么搞

//使用的ISO_LOCAL_DATE格式解析  2017-01-01
LocalDate.parse("2017-01-01");
//使用自定义格式解析  2017-01-01T08:08:08
LocalDateTime.parse("2017-01-01 08:08:08", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
           

在DateTimeFormatter中还有很多定义好的格式,有兴趣的可以自己去看一下

SimpleDateFormat是线程不安全的,所以在高并发环境下,建议这么搞
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
            
<span class="token comment">//和线程绑定 保证安全</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> String <span class="token function">format</span><span class="token punctuation">(</span>Date date<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> threadLocal<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span>date<span class="token punctuation">)</span><span class="token punctuation">;</span>
           
  • 注意:ofPattern(String pattern)和ofPattern(String pattern, Locale locale)还是有区别的。但绝大多数情况下,我们用ofPattern就够了,因为Locale对象根据用户的国家,地区文化差异格式化,不会改变系统时间,只是表达方式变而已,就是数值表示方法不同而已,也是一样的值,这个方法不常用,因为不能覆盖所有语言环境。并且和格式化模版有关,比如我们的最常用yyyy-MM-dd HH:mm:ss会没有效果。但是这种模版“GGGG yyyy/MMMM/dd HH:mm:ss EEE”,Local不同,展示方式是有很大不同的

Date类型和时间戳 转换成新的时间类型

Date在1.8之后提供了几个方法,可以很方便的转换成新的API

//时间戳转instant就很简单了
        Instant instant = Instant.ofEpochMilli(System.currentTimeMillis());
        System.out.println(instant); //2018-08-06T16:26:08.539Z(其实已经24点了,所以直接输出是有时区问题的 需要注意)
            
<span class="token comment">//Date直接转Instant</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toInstant</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//2018-08-06T16:26:08.539Z</span>
    <span class="token comment">//Instant --&gt; Date</span>
    Date<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>Instant<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">//Calendar --&gt; Instant(这个用得很少)</span>
    Calendar<span class="token punctuation">.</span><span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toInstant</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
           

理论知识就介绍到这了,接下来看一些有意思的案例实现,可以更好的了解应用场景

根据已经了解的策略模式,我们可以很好的猜到,LocalDate、LocalTime、LocalDateTime他们之前的互相转换,也是可以走from方法的,如下:

LocalDateTime localDateTime = LocalDateTime.now();
        LocalDate localDate = LocalDate.from(localDateTime);
        LocalTime localTime = LocalTime.from(localDateTime);
        System.out.println(localDate); //2018-08-13
        System.out.println(localTime); //16:04:48.356
            
<span class="token comment">///下面的会报错哟///</span>
    
    <span class="token comment">//LocalTime localTime = LocalTime.now();</span>
    <span class="token comment">//LocalDate localDate = LocalDate.from(localTime); //这样转是会报错的  因为LocalTime不含有Date元素 Unable to obtain LocalDate from TemporalAccessor: 16:01:47.541 of type java.time.LocalTime</span>
    <span class="token comment">//LocalDateTime localDateTime = LocalDateTime.from(localTime); //这样转也是会报错的 因为不含有date元素</span>
    <span class="token comment">//System.out.println(localTime);</span>
    <span class="token comment">//System.out.println(localDateTime);</span>
           

重要:常用:LocalDate和Date类、时间戳之间转换的坑

Date对象表示特定的日期和时间,而LocalDate(Java8)对象只包含没有任何时间信息的日期。 因此,如果我们只关心日期而不是时间信息,则可以在Date和LocalDate之间进行转换

在JDK8以前,我们经常遇到用Date类型来装载时间。有时候只表示日期,有时候是日期+时间,但是我们的选择都只能是Date类型。因此Date类型到LocalDate、LocalTime、Instant等类型的转换 显得尤为重要了。

这里面需要注意一个坑:他们转换的中间桥梁都是时间戳Instant对象,但是转换的时候如果没有考虑时区,就会报错的。

比如下面这个例子,看起来顺滑,其实异常了:

Date date = new Date();
        Instant instant = date.toInstant();
        //看起来非常顺滑 但其实 异常:Unable to obtain LocalDate from TemporalAccessor: 2018-08-31T02:41:28.076Z of type java.time.Instant
        LocalDate from = LocalDate.from(date.toInstant());
           

其实这个也好理解。人家Date是带有日期和时间的,然后突然来一个只需要日期的,LocalDate不知道咋处理(或者说JDK8没考虑到这一点,其实不是,因为时区没定,LocalDate自己不好自己做定论),所以不允许直接转换也可以理解。所以各位使用起一定要小心使用了

糗事Date和LocalDate、LocalTime等互相转化的的思想也很简单 借助LocalDateTime对象就万无一失了。

Date date = new Date();
        Instant instant = date.toInstant();
            
<span class="token comment">//以ZoneId.systemDefault转换成LocalDateTime后,就可以随意转换了</span>
    LocalDateTime localDateTime <span class="token operator">=</span> LocalDateTime<span class="token punctuation">.</span><span class="token function">ofInstant</span><span class="token punctuation">(</span>instant<span class="token punctuation">,</span> ZoneId<span class="token punctuation">.</span><span class="token function">systemDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">//方式一:使用LocalDate、LocalTime的from</span>
    LocalDate fromLocalDate <span class="token operator">=</span> LocalDate<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>localDateTime<span class="token punctuation">)</span><span class="token punctuation">;</span>
    LocalTime fromLocalTime <span class="token operator">=</span> LocalTime<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>localDateTime<span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>fromLocalDate<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//2018-08-31</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>fromLocalTime<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//11:03:19.716</span>

    <span class="token comment">//方式二:直接to的方式</span>
    LocalDate toLocalDate <span class="token operator">=</span> localDateTime<span class="token punctuation">.</span><span class="token function">toLocalDate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    LocalTime toLocalTime <span class="token operator">=</span> localDateTime<span class="token punctuation">.</span><span class="token function">toLocalTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>toLocalDate<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//2018-08-31</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>toLocalTime<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//11:03:19.716</span>
           

反向转换:借助的中间变量是Instant即可

public static void main(String[] args) {
        LocalDateTime localDateTime = LocalDateTime.now();
        LocalDate localDate = LocalDate.now();
        LocalTime localTime = LocalTime.now();
            
Instant instant <span class="token operator">=</span> null<span class="token punctuation">;</span>
    ZoneId zone <span class="token operator">=</span> ZoneId<span class="token punctuation">.</span><span class="token function">systemDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">//LocalDateTime转Instant转Date</span>
    instant <span class="token operator">=</span> localDateTime<span class="token punctuation">.</span><span class="token function">atZone</span><span class="token punctuation">(</span>zone<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toInstant</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>Date<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>instant<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">//LocalDate转Instant转Date</span>
    instant <span class="token operator">=</span> localDate<span class="token punctuation">.</span><span class="token function">atStartOfDay</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">atZone</span><span class="token punctuation">(</span>zone<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toInstant</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>Date<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>instant<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    
    <span class="token comment">//LocalTime转Instant转Date(很麻烦 一般杜绝这样使用吧)</span>
    <span class="token comment">//必须先借助localDate转换成localDateTime 在转成instant 再转date</span>
    LocalDateTime localDateTimeDate <span class="token operator">=</span> LocalDateTime<span class="token punctuation">.</span><span class="token function">of</span><span class="token punctuation">(</span>localDate<span class="token punctuation">,</span> localTime<span class="token punctuation">)</span><span class="token punctuation">;</span>
    instant <span class="token operator">=</span> localDateTime<span class="token punctuation">.</span><span class="token function">atZone</span><span class="token punctuation">(</span>zone<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toInstant</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>Date<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>instant<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token punctuation">}</span>
           

时间矫正器(TemporalAdjuster )

Java8推出了时间矫正器的概念。可以辅助我们更精准的定位到一些日期,比如写个周日,下个结婚纪念日等等。

  • TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下个周日”等操作。不过它是个接口,并且是函数式接口
  • TemporalAdjusters : 该类通过静态方法提供了大量的常用TemporalAdjuster 的实现。
时间矫正,用的都是with语法。可以理解成和set差不多
java8新特性之---全新的日期、时间API(JSR 310规范),附SpringMVC、Mybatis中使用JSR310的正确姿势
public static void main(String[] args) {
        LocalDateTime ldt1 = LocalDateTime.now();
        //本月第一天
        LocalDateTime ldt2 = ldt1.with(TemporalAdjusters.firstDayOfMonth());
        System.out.println(ldt2); //2018-08-01T17:34:42.039
        //本月的第一个周五
        LocalDateTime ldt3 = ldt1.with(TemporalAdjusters.firstInMonth(DayOfWeek.FRIDAY));
        System.out.println(ldt3); //2018-08-03T17:41:07.619
    }
           

接下来一个场景会比较有意思点:下一个信用卡还款日是什么时候。

public static void main(String[] args) {
        LocalDate localDate = LocalDate.now();
        //下一个工作日(不考虑法定节假日的情况)  自己实现一个时间矫正器
        LocalDate with = localDate.with(x -> {
            LocalDate date = LocalDate.class.cast(x);
            DayOfWeek dayOfWeek = date.getDayOfWeek();
            if (dayOfWeek == DayOfWeek.FRIDAY) {
                return date.plusDays(3);
            } else if (dayOfWeek == DayOfWeek.SATURDAY) {
                return date.plusDays(2);
            } else {
                return date.plusDays(1);
            }
        });
        System.out.println(with); //2018-08-10
    }
           

时间矫正器,在很多场景下,还是非常有用的。所以希望读者能够大概掌握

Java中处理日期、时间的经典案例场景

检查两个日期是否相等

LocalDate重写了equals方法来进行日期的比较,如下所示:

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

在java8中如何检查重复事件,比如生日

这是相对比较常用的一个场景:判断今天是否是某个人的生日。

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

通过列子可以看到MonthDay只存储了月日,对比两个日期的月日即可知道是否重复,而且使用了equals方法,非常的方便快捷有木有

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

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

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

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

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

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

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

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

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

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

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

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

说明: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
            
<span class="token keyword">long</span> distance2 <span class="token operator">=</span> ChronoUnit<span class="token punctuation">.</span>DAYS<span class="token punctuation">.</span><span class="token function">between</span><span class="token punctuation">(</span>start<span class="token punctuation">,</span> end<span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>distance2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//496</span>
           

大赞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
            
<span class="token comment">//获取系统的默认值==================推荐使用</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>OffsetDateTime<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getOffset</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//+08:00</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>ZoneId<span class="token punctuation">.</span><span class="token function">systemDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//Asia/Shanghai</span>
<span class="token punctuation">}</span>
           

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>
           

备注:

  1. 如果你是SpringBoot环境,SpringMVC依赖的版本号version都可以省略,而且建议省略。SpringBoot2.0以上版本,不需要自己再额外导入SpringMVC的那个JSR310依赖的jar,因为默认就自带了
  2. 如果你的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 足够你用的了,而且安全性更高

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

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

导入之后: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;
    }
           

若注册了此bean,LocalDate的输出正常了,Date类型等也不再输出时间戳了。但是,但是,但是:

在进行一些全局性设置设计的时候,一定一定要考虑到向下兼容性,不要因为你的一个序列化器的加入,之前的序列化都乱套了,导致前端展示错乱的现象
提示相关注解解决问题:@JsonFormat @JsonComponent(非framework提供的,而是boot提供的)

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 <span class="token operator">=</span> <span class="token function">getFormatter</span><span class="token punctuation">(</span>Type<span class="token punctuation">.</span>DATE<span class="token punctuation">)</span><span class="token punctuation">;</span>
	DateTimeFormatter tf <span class="token operator">=</span> <span class="token function">getFormatter</span><span class="token punctuation">(</span>Type<span class="token punctuation">.</span>TIME<span class="token punctuation">)</span><span class="token punctuation">;</span>
	DateTimeFormatter dtf <span class="token operator">=</span> <span class="token function">getFormatter</span><span class="token punctuation">(</span>Type<span class="token punctuation">.</span>DATE_TIME<span class="token punctuation">)</span><span class="token punctuation">;</span>

	<span class="token comment">// Efficient ISO_LOCAL_* variants for printing since they are twice as fast...</span>

	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>LocalDate<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorPrinter</span><span class="token punctuation">(</span>
					df <span class="token operator">==</span> DateTimeFormatter<span class="token punctuation">.</span>ISO_DATE <span class="token operator">?</span> DateTimeFormatter<span class="token punctuation">.</span>ISO_LOCAL_DATE <span class="token operator">:</span> df<span class="token punctuation">)</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorParser</span><span class="token punctuation">(</span>LocalDate<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> df<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>LocalTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorPrinter</span><span class="token punctuation">(</span>
					tf <span class="token operator">==</span> DateTimeFormatter<span class="token punctuation">.</span>ISO_TIME <span class="token operator">?</span> DateTimeFormatter<span class="token punctuation">.</span>ISO_LOCAL_TIME <span class="token operator">:</span> tf<span class="token punctuation">)</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorParser</span><span class="token punctuation">(</span>LocalTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> tf<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>LocalDateTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorPrinter</span><span class="token punctuation">(</span>
					dtf <span class="token operator">==</span> DateTimeFormatter<span class="token punctuation">.</span>ISO_DATE_TIME <span class="token operator">?</span> DateTimeFormatter<span class="token punctuation">.</span>ISO_LOCAL_DATE_TIME <span class="token operator">:</span> dtf<span class="token punctuation">)</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorParser</span><span class="token punctuation">(</span>LocalDateTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> dtf<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>ZonedDateTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorPrinter</span><span class="token punctuation">(</span>dtf<span class="token punctuation">)</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorParser</span><span class="token punctuation">(</span>ZonedDateTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> dtf<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>OffsetDateTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorPrinter</span><span class="token punctuation">(</span>dtf<span class="token punctuation">)</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorParser</span><span class="token punctuation">(</span>OffsetDateTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> dtf<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>OffsetTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorPrinter</span><span class="token punctuation">(</span>tf<span class="token punctuation">)</span><span class="token punctuation">,</span>
			<span class="token keyword">new</span> <span class="token class-name">TemporalAccessorParser</span><span class="token punctuation">(</span>OffsetTime<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> tf<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>Instant<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">InstantFormatter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>Period<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">PeriodFormatter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>Duration<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">DurationFormatter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>YearMonth<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">YearMonthFormatter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldType</span><span class="token punctuation">(</span>MonthDay<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">MonthDayFormatter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

	registry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldAnnotation</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Jsr310DateTimeFormatAnnotationFormatterFactory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
           

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

注册的地方DefaultFormattingConversionService:addDefaultFormatters

public static void addDefaultFormatters(FormatterRegistry formatterRegistry) {
		// Default handling of number values
		formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
            
<span class="token comment">// Default handling of monetary values</span>
	<span class="token keyword">if</span> <span class="token punctuation">(</span>jsr354Present<span class="token punctuation">)</span> <span class="token punctuation">{</span>
		formatterRegistry<span class="token punctuation">.</span><span class="token function">addFormatter</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">CurrencyUnitFormatter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
		formatterRegistry<span class="token punctuation">.</span><span class="token function">addFormatter</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">MonetaryAmountFormatter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
		formatterRegistry<span class="token punctuation">.</span><span class="token function">addFormatterForFieldAnnotation</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Jsr354NumberFormatAnnotationFormatterFactory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>

	<span class="token comment">// Default handling of date-time values</span>
	<span class="token keyword">if</span> <span class="token punctuation">(</span>jsr310Present<span class="token punctuation">)</span> <span class="token punctuation">{</span>
		<span class="token comment">// just handling JSR-310 specific date and time types</span>
		<span class="token keyword">new</span> <span class="token class-name">DateTimeFormatterRegistrar</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">registerFormatters</span><span class="token punctuation">(</span>formatterRegistry<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
	<span class="token keyword">if</span> <span class="token punctuation">(</span>jodaTimePresent<span class="token punctuation">)</span> <span class="token punctuation">{</span>
		<span class="token comment">// handles Joda-specific types as well as Date, Calendar, Long</span>
		<span class="token keyword">new</span> <span class="token class-name">JodaTimeFormatterRegistrar</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">registerFormatters</span><span class="token punctuation">(</span>formatterRegistry<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
	<span class="token keyword">else</span> <span class="token punctuation">{</span>
		<span class="token comment">// regular DateFormat-based Date, Calendar, Long converters</span>
		<span class="token keyword">new</span> <span class="token class-name">DateFormatterRegistrar</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">registerFormatters</span><span class="token punctuation">(</span>formatterRegistry<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
           

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