【小家java】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最重要的幾個類
    所有類都實現了 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
            //下面是幾種獲取時間戳(毫秒值)的方法 推薦使用高逼格的toEpochMilli()去做
            System.out.println(now.toEpochMilli());
            System.out.println(System.currentTimeMillis());
            System.out.println(new Date().getTime());
        }
    還有一些對plus、minus、isAfter、isBefore等方法,此處不做多餘講解
    

    下面介紹兩個比較實用的方法:

    public static void main(String[] args) {
            //自帶的解析 若需要自定義格式,可以這麼來
            Instant temp =Instant.parse("2007-12-03T10:15:30.00Z");
            Instant now = Instant.now();
            Instant instant = now.plusSeconds(TimeUnit.HOURS.toSeconds(25));
            //希望得到兩個時間戳,他們相隔了幾個小時、幾天、幾個月?
            System.out.println(now.until(instant,ChronoUnit.HOURS)); //25
            System.out.println(now.until(instant,ChronoUnit.DAYS)); //1(這裏顯示1不是2哦)
            System.out.println(instant.until(now,ChronoUnit.HOURS)); //-25(注意,這裏是負數哦)
        }
    

    以前我們要統計一段程序的運行時間,現在可以採用這種優雅的方式了

    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 c1 = Clock.systemUTC(); //系統默認UTC時鐘(當前瞬時時間 System.currentTimeMillis())
    

    //這麼來會採用系統默認的時區
    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
            //LocaDate的常量
            System.out.println(LocalDate.MIN); //-999999999-01-01
            System.out.println(LocalDate.MAX); //+999999999-12-31
            //LocalTime的常量
            System.out.println(LocalTime.MIN); //00:00
            System.out.println(LocalTime.MAX); //23:59:59.999999999
            System.out.println(LocalTime.MIDNIGHT); //00:00
            System.out.println(LocalTime.NOON); //12:00
            //LocalDateTime的常量
            System.out.println(LocalDateTime.MIN); //-999999999-01-01T00:00
            System.out.println(LocalDateTime.MAX); //+999999999-12-31T23:59:59.999999999
            //ZoneOffset的常量
            System.out.println(ZoneOffset.UTC); //Z
            System.out.println(ZoneOffset.MIN); //-18:00
            System.out.println(ZoneOffset.MAX); //+18:00
    
        <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);
            //日期轉換爲字符串
            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
            String nowStr = now .format(format);
            System.out.println(nowStr); //2018年08月07日 12:15 上午
        }
    
    • 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");
            }
        };
        //和線程綁定 保證安全
        public static String format(Date date) {
            return threadLocal.get().format(date);
    
    • 注意: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點了,所以直接輸出是有時區問題的 需要注意)
            //Date直接轉Instant
            System.out.println(new Date().toInstant()); //2018-08-06T16:26:08.539Z
            //Instant --> Date
            Date.from(Instant.now());
            //Calendar --> Instant(這個用得很少)
            Calendar.getInstance().toInstant();
    

    理論知識就介紹到這了,接下來看一些有意思的案例實現,可以更好的瞭解應用場景


    根據已經瞭解的策略模式,我們可以很好的猜到,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
            ///////////////////////下面的會報錯喲///////////////////////
            //LocalTime localTime = LocalTime.now();
            //LocalDate localDate = LocalDate.from(localTime); //這樣轉是會報錯的  因爲LocalTime不含有Date元素 Unable to obtain LocalDate from TemporalAccessor: 16:01:47.541 of type java.time.LocalTime
            //LocalDateTime localDateTime = LocalDateTime.from(localTime); //這樣轉也是會報錯的 因爲不含有date元素
            //System.out.println(localTime);
            //System.out.println(localDateTime);
    

    重要:常用: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();
            //以ZoneId.systemDefault轉換成LocalDateTime後,就可以隨意轉換了
            LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
            //方式一:使用LocalDate、LocalTime的from
            LocalDate fromLocalDate = LocalDate.from(localDateTime);
            LocalTime fromLocalTime = LocalTime.from(localDateTime);
            System.out.println(fromLocalDate); //2018-08-31
            System.out.println(fromLocalTime); //11:03:19.716
            //方式二:直接to的方式
            LocalDate toLocalDate = localDateTime.toLocalDate();
            LocalTime toLocalTime = localDateTime.toLocalTime();
            System.out.println(toLocalDate); //2018-08-31
            System.out.println(toLocalTime); //11:03:19.716
    

    反向轉換:藉助的中間變量是Instant即可

     public static void main(String[] args) {
            LocalDateTime localDateTime = LocalDateTime.now();
            LocalDate localDate = LocalDate.now();
            LocalTime localTime = LocalTime.now();
            Instant instant = null;
            ZoneId zone = ZoneId.systemDefault();
            //LocalDateTime轉Instant轉Date
            instant = localDateTime.atZone(zone).toInstant();
            System.out.println(Date.from(instant));
            //LocalDate轉Instant轉Date
            instant = localDate.atStartOfDay().atZone(zone).toInstant();
            System.out.println(Date.from(instant));
            //LocalTime轉Instant轉Date(很麻煩 一般杜絕這樣使用吧)
            //必須先借助localDate轉換成localDateTime 在轉成instant 再轉date
            LocalDateTime localDateTimeDate = LocalDateTime.of(localDate, localTime);
            instant = localDateTime.atZone(zone).toInstant();
            System.out.println(Date.from(instant));
        }
    

    時間矯正器(TemporalAdjuster )

    Java8推出了時間矯正器的概念。可以輔助我們更精準的定位到一些日期,比如寫個週日,下個結婚紀念日等等。

    • TemporalAdjuster : 時間校正器。有時我們可能需要獲取例如:將日期調整到“下個週日”等操作。不過它是個接口,並且是函數式接口
    • TemporalAdjusters : 該類通過靜態方法提供了大量的常用TemporalAdjuster 的實現。

    時間矯正,用的都是with語法。可以理解成和set差不多
    時間矯正器工具類

    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中如何檢查重複事件,比如生日

    這是相對比較常用的一個場景:判斷今天是否是某個人的生日。
    這裏寫圖片描述
    通過列子可以看到MonthDay只存儲了月日,對比兩個日期的月日即可知道是否重複,而且使用了equals方法,非常的方便快捷有木有

    獲取一週、一個月、一年、一小時、一分鐘後的日期等

    LocalDate是用來表示無時間的日期,他又一個plus()方法可以用來增加日,星期,月,ChronoUnit則用來表示時間單位
    這裏寫圖片描述

    表示和處理固定的日期,比如信用卡過期時間

    YearMonth是另外一個組合,可以很好處理信用卡有效期只有年、月的問題。LengthOfMonth()這個方法返回的是這個YearMonth實例有多少天,這對於檢查2月是否潤2月很有用
    這裏寫圖片描述

    兩個日期之間包含多少天,多少月(這個非常實用)

    計算兩個日期之間包含多少天、周、月、年。可以用java.time.Period類完成該功能。下面例子中將計算日期與將來的日期之間一共有幾個月
    這裏寫圖片描述

    帶時區的日期與時間(以後處理時區問題,還是用ZoneDateTime吧)

    在java8中,可以使用ZoneOffset來代表某個時區,可以使用它的靜態方法ZoneOffset.of()方法來獲取對應的時區,只要獲得了這個偏移量,就可以用這個偏移量和LocalDateTime創建一個新的OffsetDateTime
    這裏寫圖片描述
    說明:OffsetDateTime主要是用來給機器理解的,平時使用就用前面結束的ZoneDateTime類就可以了

    如何在兩個日期之間獲得所有日期

    這個需求其實是比較常見的需求,所有很有必要在這裏實現一把。因爲其實實現起來並不見得那麼簡單,還有不少誤區:所以我這裏展開說一下

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

    因此我們先造出兩個日期出來,然後求出他們的差值如下:

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

    有的人可能第一眼可能會想到用Period來做:

     Period period = Period.between(start, end);
            System.out.println(period); //P1Y4M9D
            System.out.println(period.getYears()); //1
            System.out.println(period.getMonths()); //4
            System.out.println(period.getDays()); //9
    

    //備註:Period period = start.until(end); //效果同上

    我們會發現,根本就就不是我們想要的。其實這裏需要注意一點:從輸出的值可以看出,Period得到的是差值的絕對值,而並不表示真正的區間距離。因爲它表示一個時段,所以肯定是絕對值含義。

    所以我們想到可以如下處理(方法一):

    //先計算出兩個日期的像個
    long distance = ChronoUnit.DAYS.between(start, end);
    //for循環往裏面處理
    for(int i = 0; i <= distance; i++){
        start.plusDays(i); //...do the stuff with the new date...
    }
    

    下面介紹一種更優雅的方案(方案二)

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

    採用迭代流來生成,顯得逼格滿滿。

    這裏面穿插一下,ChronoUnit類。它像是一個單位類

    start.plus(1,ChronoUnit.DAYS);
            //等價於
            start.plusDays(1);
    

    下面這個需要注意,LocalDate本身具備的一種能力:

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

    大讚Java8 時間API的設計,條條大路通羅馬啊

    如何在兩個日期之間獲得所有的月份

    有了上面的額例子,這個自然不在話下。那麼就繼續來上代碼:

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

    照葫蘆畫瓢,只是簡單的把單位換一下就ok了。


    ZoneOffset 於 ZoneId

    ZoneOffset 表示與UTC時區偏移的固定區域。
    ZoneOffset不隨着由夏令時導致的區域偏移的更改。

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

    我們可以用小時,分鐘和秒的組合創建 ZoneOffset 。

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

    以下代碼顯示如何從偏移創建區域偏移。

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

    API支持-18:00到+18:00之間的區域偏移。

    ZoneId 表示區域偏移及其用於更改區域偏移的規則夏令時。
    每個時區都有一個ID,可以用三種格式定義:

    • 在區域偏移中,可以是“Z”,“+ hh:mm:ss”或“-hh:mm:ss”,例如“+01:00”。
    • 前綴爲“UTC”,“GMT”或“UT”,後跟區域偏移量,例如“UTC + 01:00”。
    • 在區域名稱中,例如,“美洲/芝加哥”。(比較常用)

    以下代碼顯示如何使用of()工廠方法創建ZoneId。

        public static void main(String[] args) {
            //備註:此字符串必須合法   否則報錯
            ZoneId usChicago = ZoneId.of("Asia/Shanghai"); //Asia/Shanghai
            System.out.println(usChicago);
            ZoneId fixedZoneId = ZoneId.of("+01:00");
            System.out.println(fixedZoneId); //+01:00
        }
    

    ZoneId 中的 getAvailableZoneIds()返回所有已知時區ID。

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

    使用java8我們知道使用ZoneId.default()可以獲得系統默認值ZoneId,但如何獲取默認值ZoneOffset?我看到一個ZoneId有一些“規則”而且每個規則都有一個ZoneOffset,這意味着一個ZoneId可能有一個以上ZoneOffset嗎?答案如下:

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

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

    首先你需要引入對應的Jar(這是很多人不知道怎麼支持的最重要原因)

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

    備註:

    1. 如果你是SpringBoot環境,SpringMVC依賴的版本號version都可以省略,而且建議省略。SpringBoot2.0以上版本,不需要自己再額外導入SpringMVC的那個JSR310依賴的jar,因爲默認就自帶了
    2. 如果你的Mybatis版本在3.4.0以上,導包就支持。如果在以下,就需要自己手動配置文件裏註冊

    建議以後放棄使用Date和Timestamp類型。
    DB的entiry使用LocalDateTime對應sql的datetime、LocalDate對應date、LocalTime對應time 足夠你用的了,而且安全性更高

    爲何能夠處理這些時間?看到下面截圖一目瞭然:
    在這裏插入圖片描述
    導入之後: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 = getFormatter(Type.DATE);
    		DateTimeFormatter tf = getFormatter(Type.TIME);
    		DateTimeFormatter dtf = getFormatter(Type.DATE_TIME);
    		// Efficient ISO_LOCAL_* variants for printing since they are twice as fast...
    		registry.addFormatterForFieldType(LocalDate.class,
    				new TemporalAccessorPrinter(
    						df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df),
    				new TemporalAccessorParser(LocalDate.class, df));
    		registry.addFormatterForFieldType(LocalTime.class,
    				new TemporalAccessorPrinter(
    						tf == DateTimeFormatter.ISO_TIME ? DateTimeFormatter.ISO_LOCAL_TIME : tf),
    				new TemporalAccessorParser(LocalTime.class, tf));
    		registry.addFormatterForFieldType(LocalDateTime.class,
    				new TemporalAccessorPrinter(
    						dtf == DateTimeFormatter.ISO_DATE_TIME ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : dtf),
    				new TemporalAccessorParser(LocalDateTime.class, dtf));
    		registry.addFormatterForFieldType(ZonedDateTime.class,
    				new TemporalAccessorPrinter(dtf),
    				new TemporalAccessorParser(ZonedDateTime.class, dtf));
    		registry.addFormatterForFieldType(OffsetDateTime.class,
    				new TemporalAccessorPrinter(dtf),
    				new TemporalAccessorParser(OffsetDateTime.class, dtf));
    		registry.addFormatterForFieldType(OffsetTime.class,
    				new TemporalAccessorPrinter(tf),
    				new TemporalAccessorParser(OffsetTime.class, tf));
    		registry.addFormatterForFieldType(Instant.class, new InstantFormatter());
    		registry.addFormatterForFieldType(Period.class, new PeriodFormatter());
    		registry.addFormatterForFieldType(Duration.class, new DurationFormatter());
    		registry.addFormatterForFieldType(YearMonth.class, new YearMonthFormatter());
    		registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter());
    		registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory());
    	}
    

    這就無需多餘解釋了,都是採用的ISO標準模版。還好他給我們提供了對應的set方法,因此我想到了自定義
    註冊的地方DefaultFormattingConversionService:addDefaultFormatters

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

    發現是new出來的,因此我們還不能直接從容器裏面注入。確實不太好弄了。。。。

    還好,經過我最終的源碼跟蹤,發現他解析了@DateTimeFormat註解,因此我試試用了這個註解

        @ApiOperation("測試接受時間類型Get")
        @PostMapping("/test/jsr310/get")
        Object testJsrGet(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime localDateTime) {
            System.out.println(localDateTime);
            return localDateTime;
        }
    

    bingo, 沒毛病了,完美解決問題。

    最後,我們發現。SpringMVC對body體裏面的反序列化和對get請求參數的反序列化的機制是不一樣的。因此大家使用的時候要倍加註意啊

    知識交流

    在這裏插入圖片描述
    若羣二維碼失效,請加微信號(或者掃描下方二維碼):fsx641385712。
    並且備註:“java入羣” 字樣,會手動邀請入羣

    在這裏插入圖片描述

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章