天天看點

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請求參數的反序列化的機制是不一樣的。是以大家使用的時候要倍加注意啊