這些Java8官方挖過的坑,你踩過幾個?
阿新 • • 發佈:2020-06-01
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200601081243255.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70)
> 導讀:系統啟動異常日誌竟然被JDK吞噬無法定位?同樣的加密方法,竟然出現部分資料解密失敗?往List裡面新增資料竟然提示不支援?日期明明間隔1年卻輸出1天,難不成這是天上人間?1582年神祕消失的10天JDK能否識別?Stream很高大上,List轉Map卻全失敗……這些JDK8官方挖的坑,你踩過幾個? 關注公眾號【碼大叔】,實戰踩坑硬核分享,一起交流!
@[TOC](目錄)
# 一、Base64:你是我解不開的迷
出於使用者隱私資訊保護的目的,系統上需將姓名、身份證、手機號等敏感資訊進行加密儲存,很自然選擇了AES演算法,外面又套了一層Base64,之前用的是sun.misc.BASE64Decoder/BASE64Encoder,網上的資料基本也都是這種寫法,執行得很完美。但這種寫法在idea或者maven編譯時就會有一些黃色告警提示。到了Java 8後,Base64編碼已經成為Java類庫的標準,內建了 Base64 編碼的編碼器和解碼器。於是乎,我手賤地修改了程式碼,改用了jdk8自帶的Base64方法
```java
import java.util.Base64;
public class Base64Utils {
public static final Base64.Decoder DECODER = Base64.getDecoder();
public static final Base64.Encoder ENCODER = Base64.getDecoder();
public static String encodeToString(byte[] textByte) {
return ENCODER.encodeToString(textByte);
}
public static byte[] decode(String str) {
return DECODER.decode(str);
}
}
```
程式設計師的職業操守咱還是有的,構造新老資料、自測、通過,提交測試版本。信心滿滿,我要繼續延續我 0 Bug的神話!然後……然後版本就被打回了。
```java
Caused by: java.lang.IllegalArgumentException: Illegal base64 character 3f
at java.util.Base64$Decoder.decode0(Base64.java:714)
at java.util.Base64$Decoder.decode(Base64.java:526)
at java.util.Base64$Decoder.decode(Base64.java:549)
```
關鍵是**這個錯還很詭異,部分資料是可以解密的,部分解不開**。
Base64依賴於簡單的編碼和解碼演算法,使用65個字元的US-ASCII子集,其中前64個字元中的每一個都對映到等效的6位二進位制序列,第65個字元(=)用於將Base64編碼的文字填充到整數大小。後來產生了3個變種:
- RFC 4648:Basic
此變體使用RFC 4648和RFC 2045的Base64字母表進行編碼和解碼。編碼器將編碼的輸出流視為一行; 沒有輸出行分隔符。解碼器拒絕包含Base64字母表之外的字元的編碼。
- RFC 2045:MIME
此變體使用RFC 2045提供的Base64字母表進行編碼和解碼。編碼的輸出流被組織成不超過76個字元的行; 每行(最後一行除外)通過行分隔符與下一行分隔。解碼期間將忽略Base64字母表中未找到的所有行分隔符或其他字元。
- RFC 4648:Url
此變體使用RFC 4648中提供的Base64字母表進行編碼和解碼。字母表與前面顯示的字母相同,只是-替換+和_替換/。不輸出行分隔符。解碼器拒絕包含Base64字母表之外的字元的編碼。
| S.N. | 方法名稱 & 描述 |
|--|:--|
| 1 | **static Base64.Decoder getDecoder()**
返回Base64.Decoder解碼使用基本型base64編碼方案。| |2|**static Base64.Encoder getEncoder()**
返回Base64.Encoder編碼使用的基本型base64編碼方案。| |3|**static Base64.Decoder getMimeDecoder()**
返回Base64.Decoder解碼使用MIME型別的base64解碼方案。| |4|**static Base64.Encoder getMimeEncoder()**
返回Base64.Encoder編碼使用MIME型別base64編碼方案。| |5|**static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator)**
返回Base64.Encoder編碼使用指定的行長度和線分隔的MIME型別base64編碼方案。| |6|**static Base64.Decoder getUrlDecoder()**
返回Base64.Decoder解碼使用URL和檔名安全型base64編碼方案。| |7|**static Base64.Encoder getUrlEncoder()**
返回Base64.Decoder解碼使用URL和檔名安全型base64編碼方案。| 關於base64用法的詳細說明,可參考:https://juejin.im/post/5c99b2976fb9a070e76376cc 對於上面的錯誤,網上有的說法是,建議使用`Base64.getMimeDecoder()`和`Base64.getMimeEncoder()`,對此我只能建議:老的系統如果已經有資料了,就不要使用jdk自帶的Base64了。JDK官方的Base64和sun的base64是不相容的!不要替換!不要替換!不要替換! # 二、被吞噬的異常:我不敢說出你的名字 這個問題理解起來還是蠻費腦子的,所以我把這個系統異常發生的過程提煉成了一個美好的故事,放鬆一下,吟詩一首! > 最怕相思濃 > 一切皆是你 > 唯獨 > 不敢說出你的名字 > -- 碼大叔 這個問題是在使用springboot的註解時遇到的問題,發現JDK在解析註解時,若註解依賴的類定義在JVM載入時不存在,也就是`NoClassDefFoundError`時,實際拿到的異常將會是`ArrayStoreException`,而不是`NoClassDefFoundError`,涉及到的JDK裡的類是`AnnotationParser.java`, 具體程式碼如下: ```java private static Object parseClassArray(int paramInt, ByteBuffer paramByteBuffer, ConstantPool paramConstantPool, Class paramClass) { Class[] arrayOfClass = new Class[paramInt]; int i = 0; int j = 0; for (int k = 0; k < paramInt; k++){ j = paramByteBuffer.get(); if (j == 99) { // 注意這個方法 arrayOfClass[k] = parseClassValue(paramByteBuffer, paramConstantPool, paramClass); } else { skipMemberValue(j, paramByteBuffer); i = 1; } } return i != 0 ? exceptionProxy(j) : arrayOfClass; } ``` ```java private static Object parseClassValue(ByteBuffer paramByteBuffer, ConstantPool paramConstantPool, Class paramClass) { int i = paramByteBuffer.getShort() & 0xFFFF; try { String str = paramConstantPool.getUTF8At(i); return parseSig(str, paramClass); } catch (IllegalArgumentException localIllegalArgumentException) { return paramConstantPool.getClassAt(i); } catch (NoClassDefFoundError localNoClassDefFoundError) { // 注意這裡,異常發生了轉化 return new TypeNotPresentExceptionProxy("[unknown]", localNoClassDefFoundError); } catch (TypeNotPresentException localTypeNotPresentException) { return new TypeNotPresentExceptionProxy(localTypeNotPresentException.typeName(), localTypeNotPresentException.getCause()); } } ``` 在 `parseClassArray`這個方法中,預期`parseClassValue`返回`Class`物件,但看實際`parseClassValue`的邏輯,在遇到`NoClassDefFoundError`時,返回的是`TypeNotPresentExceptionProxy`,由於型別強轉失敗,最終丟擲的是`java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy`,此時只能通過debug到這行程式碼,找到具體是缺少哪個類定義,才能解決這個問題。 **筆者重現一下發現這個坑的場景**,有三個module,module3依賴module2但未宣告依賴module1,module2依賴module1,但宣告的是optional型別,依賴關係圖如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200531165100528.png) 上面每個module中有一個Class,我們命名為ClassInModuleX。ClassInModule3啟動時在註解中使用了ClassInModule2的類,而ClassInModule2這個類的繼承了ClassInModule1,這幾個類的依賴關係圖如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020053116512919.png) 如此,其實很容易知道在module執行ClassInModule3時,會出現ClassInModule1的`NoClassDefFoundError`的,但實際執行時,你能看到的異常將不是`NoClassDefFoundError`,而是`java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy`,此時,若想要知道具體是何許異常,需通過debug在`AnnotationParser`中定位具體問題,以下展示兩個截圖,分別對應系統控制檯實際丟擲的異常和通過debug發現的異常資訊。 控制檯異常資訊: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200531165224419.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70) 注意異常實際在紅色圈圈這裡,自動收縮了,需要展開才可以看到通過debug發現的異常資訊: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200531165333565.png) 如果你想體驗這個示例,可關注公眾號碼大叔和筆者交流。如果你下次遇到莫名的`java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy`,請記得用這個方法定位具體問題。 # 三、日期計算:我想留住時間,讓1天像1年那麼長 Java8之前日期時間操作相當地麻煩,無論是Calendar還是SimpleDateFormat都讓你覺得這個設計怎麼如此地反人類,甚至還會出現多執行緒安全的問題,阿里巴巴開發手冊中就曾禁用static修飾SimpleDateFormat。好在千呼萬喚之後,使出來了,Java8帶來了全新的日期和時間API,還帶來了Period和Duration用於**時間日期計算**的兩個API。 > Duraction和Period,都表示一段時間的間隔,Duraction正常用來表示時、分、秒甚至納秒之間的時間間隔,Period正常用於年、月、日之間的時間間隔。 **網上的大部分文章也是這麼描述的**,於是計算兩個日期間隔可以寫成下面這樣的程式碼: ```java // parseToDate方法作用是將String轉為LocalDate,略。 LocalDate date1 = parseToDate("2020-05-12"); LocalDate date2 = parseToDate("2021-05-13"); // 計算日期間隔 int period = Period.between(date1,date2).getDays(); ``` 一個是202**0**年,一個是202**1**年,你認為間隔是多少?1年? 恭喜你,和我一起跳進坑裡了(畫外音:裡面的都擠一擠,動一動,又來新人了)。 正確答案應該是:1天。 這個單詞的含義以及這個方法看起來確實是蠻誤導人的,一不注意就會掉進坑裡。**Period其實只能計算同月的天數、同年的月數,不能計算跨月的天數以及跨年的月數。** **正確寫法1**: ```java long period = date2.toEpochDay()-date1.toEpochDay(); ``` toEpochDay():將日期轉換成Epoch 天,也就是相對於1970-01-01(ISO)開始的天數,和時間戳是一個道理,時間戳是秒數。顯然,**該方法是有一定的侷限性的**。 **正確寫法2**: ```java long period = date1.until(date2,ChronoUnit.DAYS); ``` 使用這個寫法,一定要注意一下date1和date2前後順序:date1 until date2。 **正確做法3(推薦)**: ```java long period = ChronoUnit.DAYS.between(date1, date2); ``` ChronoUnit:一組標準的日期時間單位。這組單元提供基於單元的訪問來操縱日期,時間或日期時間。 這些單元適用於多個日曆系統。這是一個最終的、不可變的和執行緒安全的列舉。 看到”適用於多個日曆系統“這句話,我一下子想起來**歷史上1582年神祕消失的10天**,在JDK8上是什麼效果呢?1582-10-15和1582-10-04你覺得會相隔幾天呢?11天還是1天?有興趣的小夥伴自己去寫個程式碼試試吧。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200531140216942.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70#pic_center) 開啟你的手機,跳轉到1582年10月,你就能看到這消失的10天了。 # 四、List:一如你我初見,不增不減 這個問題其實在JDK裡存在很多年了,JDK8中依然存在,也是很多人最容易跳的一個坑!直接上程式碼: ```java pub
返回Base64.Decoder解碼使用基本型base64編碼方案。| |2|**static Base64.Encoder getEncoder()**
返回Base64.Encoder編碼使用的基本型base64編碼方案。| |3|**static Base64.Decoder getMimeDecoder()**
返回Base64.Decoder解碼使用MIME型別的base64解碼方案。| |4|**static Base64.Encoder getMimeEncoder()**
返回Base64.Encoder編碼使用MIME型別base64編碼方案。| |5|**static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator)**
返回Base64.Encoder編碼使用指定的行長度和線分隔的MIME型別base64編碼方案。| |6|**static Base64.Decoder getUrlDecoder()**
返回Base64.Decoder解碼使用URL和檔名安全型base64編碼方案。| |7|**static Base64.Encoder getUrlEncoder()**
返回Base64.Decoder解碼使用URL和檔名安全型base64編碼方案。| 關於base64用法的詳細說明,可參考:https://juejin.im/post/5c99b2976fb9a070e76376cc 對於上面的錯誤,網上有的說法是,建議使用`Base64.getMimeDecoder()`和`Base64.getMimeEncoder()`,對此我只能建議:老的系統如果已經有資料了,就不要使用jdk自帶的Base64了。JDK官方的Base64和sun的base64是不相容的!不要替換!不要替換!不要替換! # 二、被吞噬的異常:我不敢說出你的名字 這個問題理解起來還是蠻費腦子的,所以我把這個系統異常發生的過程提煉成了一個美好的故事,放鬆一下,吟詩一首! > 最怕相思濃 > 一切皆是你 > 唯獨 > 不敢說出你的名字 > -- 碼大叔 這個問題是在使用springboot的註解時遇到的問題,發現JDK在解析註解時,若註解依賴的類定義在JVM載入時不存在,也就是`NoClassDefFoundError`時,實際拿到的異常將會是`ArrayStoreException`,而不是`NoClassDefFoundError`,涉及到的JDK裡的類是`AnnotationParser.java`, 具體程式碼如下: ```java private static Object parseClassArray(int paramInt, ByteBuffer paramByteBuffer, ConstantPool paramConstantPool, Class paramClass) { Class[] arrayOfClass = new Class[paramInt]; int i = 0; int j = 0; for (int k = 0; k < paramInt; k++){ j = paramByteBuffer.get(); if (j == 99) { // 注意這個方法 arrayOfClass[k] = parseClassValue(paramByteBuffer, paramConstantPool, paramClass); } else { skipMemberValue(j, paramByteBuffer); i = 1; } } return i != 0 ? exceptionProxy(j) : arrayOfClass; } ``` ```java private static Object parseClassValue(ByteBuffer paramByteBuffer, ConstantPool paramConstantPool, Class paramClass) { int i = paramByteBuffer.getShort() & 0xFFFF; try { String str = paramConstantPool.getUTF8At(i); return parseSig(str, paramClass); } catch (IllegalArgumentException localIllegalArgumentException) { return paramConstantPool.getClassAt(i); } catch (NoClassDefFoundError localNoClassDefFoundError) { // 注意這裡,異常發生了轉化 return new TypeNotPresentExceptionProxy("[unknown]", localNoClassDefFoundError); } catch (TypeNotPresentException localTypeNotPresentException) { return new TypeNotPresentExceptionProxy(localTypeNotPresentException.typeName(), localTypeNotPresentException.getCause()); } } ``` 在 `parseClassArray`這個方法中,預期`parseClassValue`返回`Class`物件,但看實際`parseClassValue`的邏輯,在遇到`NoClassDefFoundError`時,返回的是`TypeNotPresentExceptionProxy`,由於型別強轉失敗,最終丟擲的是`java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy`,此時只能通過debug到這行程式碼,找到具體是缺少哪個類定義,才能解決這個問題。 **筆者重現一下發現這個坑的場景**,有三個module,module3依賴module2但未宣告依賴module1,module2依賴module1,但宣告的是optional型別,依賴關係圖如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200531165100528.png) 上面每個module中有一個Class,我們命名為ClassInModuleX。ClassInModule3啟動時在註解中使用了ClassInModule2的類,而ClassInModule2這個類的繼承了ClassInModule1,這幾個類的依賴關係圖如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020053116512919.png) 如此,其實很容易知道在module執行ClassInModule3時,會出現ClassInModule1的`NoClassDefFoundError`的,但實際執行時,你能看到的異常將不是`NoClassDefFoundError`,而是`java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy`,此時,若想要知道具體是何許異常,需通過debug在`AnnotationParser`中定位具體問題,以下展示兩個截圖,分別對應系統控制檯實際丟擲的異常和通過debug發現的異常資訊。 控制檯異常資訊: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200531165224419.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70) 注意異常實際在紅色圈圈這裡,自動收縮了,需要展開才可以看到通過debug發現的異常資訊: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200531165333565.png) 如果你想體驗這個示例,可關注公眾號碼大叔和筆者交流。如果你下次遇到莫名的`java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy`,請記得用這個方法定位具體問題。 # 三、日期計算:我想留住時間,讓1天像1年那麼長 Java8之前日期時間操作相當地麻煩,無論是Calendar還是SimpleDateFormat都讓你覺得這個設計怎麼如此地反人類,甚至還會出現多執行緒安全的問題,阿里巴巴開發手冊中就曾禁用static修飾SimpleDateFormat。好在千呼萬喚之後,使出來了,Java8帶來了全新的日期和時間API,還帶來了Period和Duration用於**時間日期計算**的兩個API。 > Duraction和Period,都表示一段時間的間隔,Duraction正常用來表示時、分、秒甚至納秒之間的時間間隔,Period正常用於年、月、日之間的時間間隔。 **網上的大部分文章也是這麼描述的**,於是計算兩個日期間隔可以寫成下面這樣的程式碼: ```java // parseToDate方法作用是將String轉為LocalDate,略。 LocalDate date1 = parseToDate("2020-05-12"); LocalDate date2 = parseToDate("2021-05-13"); // 計算日期間隔 int period = Period.between(date1,date2).getDays(); ``` 一個是202**0**年,一個是202**1**年,你認為間隔是多少?1年? 恭喜你,和我一起跳進坑裡了(畫外音:裡面的都擠一擠,動一動,又來新人了)。 正確答案應該是:1天。 這個單詞的含義以及這個方法看起來確實是蠻誤導人的,一不注意就會掉進坑裡。**Period其實只能計算同月的天數、同年的月數,不能計算跨月的天數以及跨年的月數。** **正確寫法1**: ```java long period = date2.toEpochDay()-date1.toEpochDay(); ``` toEpochDay():將日期轉換成Epoch 天,也就是相對於1970-01-01(ISO)開始的天數,和時間戳是一個道理,時間戳是秒數。顯然,**該方法是有一定的侷限性的**。 **正確寫法2**: ```java long period = date1.until(date2,ChronoUnit.DAYS); ``` 使用這個寫法,一定要注意一下date1和date2前後順序:date1 until date2。 **正確做法3(推薦)**: ```java long period = ChronoUnit.DAYS.between(date1, date2); ``` ChronoUnit:一組標準的日期時間單位。這組單元提供基於單元的訪問來操縱日期,時間或日期時間。 這些單元適用於多個日曆系統。這是一個最終的、不可變的和執行緒安全的列舉。 看到”適用於多個日曆系統“這句話,我一下子想起來**歷史上1582年神祕消失的10天**,在JDK8上是什麼效果呢?1582-10-15和1582-10-04你覺得會相隔幾天呢?11天還是1天?有興趣的小夥伴自己去寫個程式碼試試吧。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200531140216942.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzMxMzA5OA==,size_16,color_FFFFFF,t_70#pic_center) 開啟你的手機,跳轉到1582年10月,你就能看到這消失的10天了。 # 四、List:一如你我初見,不增不減 這個問題其實在JDK裡存在很多年了,JDK8中依然存在,也是很多人最容易跳的一個坑!直接上程式碼: ```java pub