导读:系统启动异常日志竟然被JDK吞噬无法定位?同样的加密方法,竟然出现部分数据解密失败?往List里面添加数据竟然提示不支持?日期明明间隔1年却输出1天,难不成这是天上人间?1582年神秘消失的10天JDK能否识别?Stream很高大上,List转Map却全失败……这些JDK8官方挖的坑,你踩过几个? 关注公众号【码大叔】,实战踩坑硬核分享,一起交流!

@

一、Base64:你是我解不开的迷

出于用户隐私信息保护的目的,系统上需将姓名、身份证、手机号等敏感信息进行加密存储,很自然选择了AES算法,外面又套了一层Base64,之前用的是sun.misc.BASE64Decoder/BASE64Encoder,网上的资料基本也都是这种写法,运行得很完美。但这种写法在idea或者maven编译时就会有一些黄色告警提示。到了Java 8后,Base64编码已经成为Java类库的标准,内置了 Base64 编码的编码器和解码器。于是乎,我手贱地修改了代码,改用了jdk8自带的Base64方法

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的神话!然后……然后版本就被打回了。

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, 具体代码如下:

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;
}
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类型,依赖关系图如下:

上面每个module中有一个Class,我们命名为ClassInModuleX。ClassInModule3启动时在注解中使用了ClassInModule2的类,而ClassInModule2这个类的继承了ClassInModule1,这几个类的依赖关系图如下:

如此,其实很容易知道在module运行ClassInModule3时,会出现ClassInModule1的NoClassDefFoundError的,但实际运行时,你能看到的异常将不是NoClassDefFoundError,而是java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,此时,若想要知道具体是何许异常,需通过debug在AnnotationParser中定位具体问题,以下展示两个截图,分别对应系统控制台实际抛出的异常和通过debug发现的异常信息。

控制台异常信息:



注意异常实际在红色圈圈这里,自动收缩了,需要展开才可以看到通过debug发现的异常信息:



如果你想体验这个示例,可关注公众号码大叔和笔者交流。如果你下次遇到莫名的java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy,请记得用这个方法定位具体问题。

三、日期计算:我想留住时间,让1天像1年那么长

Java8之前日期时间操作相当地麻烦,无论是Calendar还是SimpleDateFormat都让你觉得这个设计怎么如此地反人类,甚至还会出现多线程安全的问题,阿里巴巴开发手册中就曾禁用static修饰SimpleDateFormat。好在千呼万唤之后,使出来了,Java8带来了全新的日期和时间API,还带来了Period和Duration用于时间日期计算的两个API。

Duraction和Period,都表示一段时间的间隔,Duraction正常用来表示时、分、秒甚至纳秒之间的时间间隔,Period正常用于年、月、日之间的时间间隔。

网上的大部分文章也是这么描述的,于是计算两个日期间隔可以写成下面这样的代码:

// parseToDate方法作用是将String转为LocalDate,略。
LocalDate date1 = parseToDate("2020-05-12");
LocalDate date2 = parseToDate("2021-05-13");
// 计算日期间隔
int period = Period.between(date1,date2).getDays();

一个是2020年,一个是2021年,你认为间隔是多少?1年?

恭喜你,和我一起跳进坑里了(画外音:里面的都挤一挤,动一动,又来新人了)。

正确答案应该是:1天。

这个单词的含义以及这个方法看起来确实是蛮误导人的,一不注意就会掉进坑里。Period其实只能计算同月的天数、同年的月数,不能计算跨月的天数以及跨年的月数。

正确写法1

 long period = date2.toEpochDay()-date1.toEpochDay();

toEpochDay():将日期转换成Epoch 天,也就是相对于1970-01-01(ISO)开始的天数,和时间戳是一个道理,时间戳是秒数。显然,该方法是有一定的局限性的

正确写法2

long period = date1.until(date2,ChronoUnit.DAYS);

使用这个写法,一定要注意一下date1和date2前后顺序:date1 until date2。

正确做法3(推荐)

 long period = ChronoUnit.DAYS.between(date1, date2);

ChronoUnit:一组标准的日期时间单位。这组单元提供基于单元的访问来操纵日期,时间或日期时间。 这些单元适用于多个日历系统。这是一个最终的、不可变的和线程安全的枚举。

看到”适用于多个日历系统“这句话,我一下子想起来历史上1582年神秘消失的10天,在JDK8上是什么效果呢?1582-10-15和1582-10-04你觉得会相隔几天呢?11天还是1天?有兴趣的小伙伴自己去写个代码试试吧。



打开你的手机,跳转到1582年10月,你就能看到这消失的10天了。

四、List:一如你我初见,不增不减

这个问题其实在JDK里存在很多年了,JDK8中依然存在,也是很多人最容易跳的一个坑!直接上代码:

public List<String> allUser() {
// 省略
List<String> currentUserList = getUser();
currentUserList.add("码大叔");
// 省略
}

就是上面这样一段代码,往一个list里添加一条数据,你觉得结果是什么呢?“码大叔”成功地添加到了List里?天真,不报个错你怎么能意识到JDK存在呢。

Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)

原因

因为在getUser方法里,返回的List使用的是Arrays.asList生成的,示例:

    private List<String> getUser(){
return Arrays.asList("剑圣","小九九");
}

我们来看看Arrays.asList的源码

    @SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
 private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private final E[] a;
// 部分代码略
ArrayList(E[] array) {
// 返回的是一个定长的数组
a = Objects.requireNonNull(array);
}
// 部分代码略
}

很明显,返回的实际是一个定长的数组,所以只能“一如你我初见”,初始化什么样子就什么样子,不能新增,不能减少。如果你理解了,那我们就再来一个栗子

   int[] intArr  = {1,2,3,4,5};
Integer[] integerArr = {1,2,3,4,5};
String[] strArr = {"1", "2", "3", "4", "5"};
List list1 = Arrays.asList(intArr);
List list2 = Arrays.asList(integerArr);
List list3 = Arrays.asList(strArr);
System.out.println("list1中的数量是:" + list1.size());
System.out.println("list2中的数量是:" + list2.size());
System.out.println("list3中的数量是:" + list3.size());

你觉得答案是什么?预想3秒钟,揭晓答案,看跟你预想的是否一致呢?

list1中的数量是:1
list2中的数量是:5
list3中的数量是:5

是不是和你预想又不一样了?还是回到Arrays.asList方法,该方法的输入只能是一个泛型变长参数。基本类型是不能泛型化的,也就是说8个基本类型不能作为泛型参数,要想作为泛型参数就必须使用其所对应的包装类型,那前面的例子传递了一个int类型的数组,为何程序没有报编译错误呢?在Java中,数组是一个对象,它是可以泛型化的,也就是说我们的例子是把一个int类型的数组作为了T的类型,所以在转换后在List中就只有1个类型为int数组的元素了。除了int,其它7个基本类型的数组也存在相似的问题。

JDK里还为我们提供了一个便捷的集合操作工具类Collections,比如多个List合并时,可以使用Collections.addAll(list1,list2), 在使用时也同样要时刻提醒自己:“请勿踩坑”!

五、Stream处理:给你,独一无二

Java8中新增了Stream流 ,通过流我们能够对集合中的每个元素进行一系列并行或串行的流水线操作。当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结 果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以 像链条一样排列,变成一个管道。



项目上千万不要使用Stream,因为一旦用起来你会觉得真屏蔽词爽,根本停不下来。当然不可避免的,还是有一些小坑的。

假设我们分析用户的访问日志,放到list里。

list.add(new User("码大叔", "登录公众号"));
list.add(new User("码大叔", "编写文章"));

因为一些原因,我们要讲list转为map,Steam走起来,

private static void convert2MapByStream(List<User> list) {
Map<String, String> map = list.stream().collect(Collectors.toMap(User::getName, User::getValue));
System.out.println(map);
}

咣当,掉坑里了,程序将抛出异常:

Exception in thread "main" java.lang.IllegalStateException: Duplicate key 码大叔

使用Collectors.toMap() 方法中时,默认key值是不允许重复的。当然,该方法还提供了第三个参数:也就是出现 duplicate key的时候的处理方案

如果在开发的时候就考虑到了key可能重复,你需要在这样定义convert2MapByStream方法,声明在遇到重复key时是使用新值还是原有值:

    private static void convert2MapByStream(List<User> list) {
Map<String, String> map = list.stream().collect(Collectors.toMap(User::getName, User::getValue, (oldVal, newVal) -> newVal));
System.out.println(map);
}

关于Stream的坑其实还是蛮多的,比如寻找list中的某个对象,可以使用findAny().get(),你以为是找到就返回找不到就就返回null?依然天真,找不到会抛出异常的,需要使用额外的orElse方法。

六、结尾:纸上得来终觉浅,绝知此事要躬行!

所谓JDK官方的坑,基本上都是因为我们对技术点了解的不够深入,望文生义,以为是怎样怎样的,而实际上我们的自以为是让我们掉进了一个又一个坑里。面对着这些坑,我流下了学艺不精的眼泪!但也有些坑,确实发生的莫名其妙,比如吞噬异常,没有理解JDK为什么这么设计。还有些坑,误导性确实太强了,比如日期计算、list操作等。最后只能说一句:

纸上得来终觉浅,绝知此事要躬行!

编码不易,且行且珍惜!

推荐阅读

Try-Catch包裹的代码异常后,竟然导致了产线事务回滚!

Redis 6.0 新特性-多线程连环13问!

报告老板,微服务高可用神器已祭出,您花巨资营销的高流量来了没?

我成功攻击了Tomcat服务器,大佬们的反应亮了

公众号:码大叔

资深程序员、架构师技术社区

微服务 | 大数据 | 架构设计 | 技术管理

个人微信:itmadashu

这些Java8官方挖过的坑,你踩过几个?的更多相关文章

  1. 论nw.js的坑~~~感觉我所有的前端能遇到的坑都踩了一遍

    先总结:nw.js 真特么的...难用...文档,我得先百度才能看的稍微明白点文档......!!!!!!我感觉我所有的前端能遇到的坑都踩了一遍,此文针对前后端分离项目,别的先不说 一.不需要在项目里 ...

  2. 微信支付官方.net版之坑你没商量

    最近开始弄支付这块,先是支付宝手机网站支付,也是坑了我许久,不过还好,问题不大. 让我们看看微信支付有多少坑 微信商户平台,你们知道么(我前天才知道,别笑我) 登录地址:https://mch.wei ...

  3. 阻塞队列 ArrayBlockingQueue 我给自己挖了一个坑

    说一句MMB, 一下午时间, package cn.tbnb1.seckil.quene; import java.util.concurrent.ArrayBlockingQueue; import ...

  4. JMeter_控制器执行效果_给自己挖过的坑

    线程及循环设置: 数据文件中放在“循环控制器”中的执行效果:每条数据执行5次,取够50条数据时停止 简单逻辑控制器按下面的目录创建后,执行结果效果同上面循环控制器的执行效果 本来想规整下目录结构,结果 ...

  5. 【safari挖的那些坑】iOS safari 浏览器 时间乱码(ios时间显示NaN) 问题解决

    通常 iOS下时间错误表现形式 问题一: 这个界面运用了大量的日期类型的计算,当我们用JavaScript实例化一个日期对象时,我们可以这样用: var date =new Date(); 上面这段代 ...

  6. SQL升级Oracle挖的Null坑,你懂的!

    最近公司做系统数据库升级,主要由原来的SQL数据库升级Oracle数据库,对于拥有千万级数据库的实用系统来说,迁移不是件容易的时,光数据同步就需要很久,更别说要修改升级原来的SQL库使用的存储过程和视 ...

  7. vue经验 - 那些自己给自己挖的深坑

    深坑场景:vue-异步请求数据,数据还没回来,页面却如饥似渴的准备好了的尴尬场景:问题原因和解决如下: 1.先说vuex中的store,一开始我为了偷懒是这么设置的,如下图: 然后我到了组件中直接这么 ...

  8. 记那些年在asp.net mvc上挖过的坑

    表现: IDE是vs2017.是在 A 控制器方法断点后,却怎么也运行不到那个位置,但是又正常返回页面.该方法位于web项目引用的控制器类库上的一个控制器,试过它隔壁的控制器,一切正常. 但每次访问该 ...

  9. Python 官方团队在打包项目中踩过的坑

    花下猫语:这是 packaging 系列的第三篇译文,该系列是全网关于此话题的最详尽(水平也很高)的一个系列.原作者是 Python 官方打包团队成员,是 virtualenv 和 tox 项目的维护 ...

随机推荐

  1. 我在 IntelliJ IDEA 中必有得插件和配置

    最近在陆续写 Java 并发编程系列,好多朋私信问我的不是并发内容本身,而是我的 IDEA 主题配置.我就姑且认为好的主题配置可以写出更好的并发程序吧 即便这种可能性只有万分之一,我也要把我的 IDE ...

  2. 前端——Vue.js学习总结一

    一.什么是Vue.js 1.Vue.js 是目前最火的一个前端框架,React是最流行的一个前端框架 2.Vue.js 是前端的主流框架之一,和Angular.js.React.js 一起,并成为前端 ...

  3. 龟兔赛跑算法 floyed判环算法

    今天写线段树写到要用到这个算法的题目,简单的学习一下. https://blog.csdn.net/javaisnotgood/article/details/89243876 https://blo ...

  4. 网络流二十四题,题解summary

    没有全部写完,有几题以后再补吧. 第一题:最简单的:飞行员配对方案问题 讲讲这个题目为什么可以用网络流? 因为这个题目是要进行两两之间的匹配,这个就可以想到用二分图匹配,二分图匹配又可以用网络流写. ...

  5. 【matlab 基础篇 01】快速开始第一个程序(详细图文+文末资源)

    快速入门matlab,系统地整理一遍,如何你和我一样是一个新手,那么此文很适合你: 文章目录 1 软件安装 2 打开软件 3 编写程序 3.1 基础步骤 3.2 添加PATH 3.3 命令行模式 4 ...

  6. 模板引擎 Thymeleaf 动态渲染 HTML

    1.添加依赖 <!-- Thymeleaf 模板引擎 --> <dependency> <groupId>org.thymeleaf</groupId> ...

  7. python语法学习第六天--字典

    字典:可变容器类型,用键值对的形式采用花括号储存(键唯一) 语法:d={key1:value1,key2:value2} 访问字典中的值: 字典名[键名]#若字典中不存在则报错 更改字典: 添加值:字 ...

  8. go多种uuid生成方式

    package main import ( "fmt" "github.com/chilts/sid" "github.com/kjk/betterg ...

  9. 部署SSL站点 IIS+asp.net

    使用SSL必须要有证书,今天我们就使用IIS内置的证书完成SSL的部署. 1.打开MMC证书管理器,文件->添加/删除管理单元->证书,双击->确定 2.找到:个人->证书下有 ...

  10. java ->IO流_File类

    IO概述 回想之前写过的程序,数据都是在内存中,一旦程序运行结束,这些数据都没有了,等下次再想使用这些数据,可是已经没有了.那怎么办呢?能不能把运算完的数据都保存下来,下次程序启动的时候,再把这些数据 ...