Java8中DateTimeFormatter真的是线程安全的吗?
答案是否定的
1.背景
由于之前写了一个旷世的ocr的服务,接入了旷世的FaceID的人脸比对的接口,然后就在写代码的过程中就遇到了这个奇怪的bug,旷世的FaceId的人脸识别的接口文档如下:
https://faceid.com/document/faceid-guide-docs/v5_get_result
说实话,旷世的产品真的是太难用了,光说这个接口接入后端也没有一个像样的SDK还得自己去写http各种封装接口、参数和解析返回结果,代码量有点的,用起来不像大厂的产品,都是给一个SDK,给使用者降低了接入的门槛有降低、接入的效率有提高和产品的质量也是没有啥缺陷的,相关的ocr的产品比如说是识别新能源和油车的车牌驾驶证还是行驶证上的车牌新能源比油车多一位就识别不出来,这个问题提给他们,他们也修复不了,以后的版本在修复,还是我们做了业务调整处理了这个识别不了的问题,还有就是副页相关的识别也是识别不出来,比起阿里的ocr来说,只能说是一个天上的一个地下,然后我就写了一段代码如下:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
String fileName = dtf.format(LocalDateTime.now());
这段代码是在一个工具类的静态方法中用于解析base64的图片(这里的业务是活体检测返回最好的一张人脸照片)和拉取Oss临时身份证正面的图片URL文件到本地,然后调用旷世的接口需要这两个图片作为参数调用人脸比对的接口,活体检测和人脸比对的大体流程简单的说下:
1.接口授权认证(接口的加解密和参数的签名验签啥的,appkey,secret,)
2.前端请求业务后端获取biz_token,然后前端的sdk拿着这个biz_token唤醒相机进行活体检测,活体检测会采集一张人脸的最好的一张base64的图片
3.最后一步就是去调用业务后端封装的一个活体检测和人脸比对的接口,该接口里面做了两件事情:
第一点:根据前端传来的biz_token去调用旷世的获取活体检测结果的接口,然后解析返回结果
第二点:根据前端传来的oss的 身份证(ocr识别传到oss上的临时身份证URL)的参数解析为本地的file2,然后根据第一点返回来的最好了的一张活体检测采集到的最好的一张人脸base64的图片解析为本的文件file2,就是这两个文件的解析,调用了同一个工具类的方法,该方法里面写了上面那段神仙代码,然后生成的file1和file2的名字都会有概率是相同的,然后传到旷世那边,找他们排查对接,他们说我们这边穿过去的这两个文件的MD5的值是一样的,瞬间我就感到了不不可思议,于是乎,疯狂review自己的代码和看旷世的接口文档,调整各种姿势修改代码调试和那边对接,那边排查的结果始终是两个文件的MD5值是相同的,最后在一番修改和调试后发现本地生成的文件的名字是一样的并且把两个文件在解析到本地的文件的MD5的值打印出来了,调用旷世的时候又把两个文件对应的MD5的值打印出来了:
// 打印文件的MD5代码
//DigestUtils.md5Hex() 这个方法是这个包里面的 package org.apache.commons.codec.digest;
FileInputStream fis1 = new FileInputStream(xxx); // xxx是传入的File
String idCardMD5 = DigestUtils.md5Hex(fis1);
经过一番调试和观察以后还以为是流没有关闭导致文件被占用两个文件的引用都指向同一个对象的地址导致的或者是两个文件的复制搞错了,结果发现两个文件在生成名字的时候会有一定的打概率生成的是相同的名字,这样两调用的地方就拿到的是同一个文件,也就是两个调用的地方指向了这个名字相同的文件导致最后两个文件的MD5的值一样了,两个文件的MD5的值一样就导致身份证的本地图片解析被活体检测采集到的最好的一张照片覆盖了,这种就相当于活体检测的人脸跟活体检测的人脸比对通过,而不是身份证照片和活体检测采集到的最好的一张人脸图片作比对,跟我们的预期不符合了,我们的预期是只有活体检测结果通过并且人脸比对身份证正面照片和活体检测采集的最好的一张图片比对成功,都是通一个人刷脸的操作而不是登录的ocr身份证认证通过的账号和刷脸不是通一个人的这种操作,给我搞了一下午到晚上9点多,把这个问题记录复盘,也是奇葩问题遇到的多,解决的也是那么酸爽的,废话不多说,接下来看如何解决吧。
2.解决办法
2.1办法一:换姿势或者升级JDK的版本
升级JDK的版本在这里就不采用这种方式了,采用更换姿势的方式
换姿势代码如下:
private static final DateTimeFormatter dtf = new DateTimeFormatterBuilder().appendPattern("yyyyMMddHHmmss").appendValue(ChronoField.MILLI_OF_SECOND, 3).toFormatter();
//格式化的地方使用这个全局df来格式化
String fileName = df.format(LocalDateTime.now());
在网上看到了一篇文章,链接如下:
https://zhuanlan.zhihu.com/p/144372694
这篇文章说Jdk8 DateTimeFormatter 解析 yyyyMMddHHmmssSSS 有问题,然后我就类比猜测了下:Jdk8 DateTimeFormatter 解析 yyyyMMddHHmmss也是有问题的,结果用上面的姿势证明它确实是有bug的,所以以后在回答和使用这个Jdk8 DateTimeFormatter的时候就不能说这个Jdk8 DateTimeFormatter类一定是线程安全的了,这个例子就是一个很好的坑,这个问题在Jdk9中修复,在jdk9及其以上的版本有没有修复,这个可以去官方找或者,升级下jdk试下就知道了,具体jdk8的DateTimeFormatter在解析yyyyMMddHHmmssSSS和yyyyMMddHHmmss格式的时候为啥会有bug?这个问题就不去深究了,根据上面正确的姿势来看估计跟解析格式的精度或者是缓存啥的有关系的。文章来源:https://www.toymoban.com/news/detail-437735.html
2.1办法二:更换文件名称字生成策略
使用其它方式生成唯一的文件名字,可以使用UUID、美团的Leaf、雪花算法(这个也会重复的,就拿mybatisPlus的id生成器来说,默认使用的是雪花算法,会有一定的重复的,所以需要设置机房id(datacenter-id)和work-id),自定义使用时间戳字符串在加随机字符啥的,或者是自己写个分布式ID生成的算法等等,方法还很多的,这里就不在啰嗦了文章来源地址https://www.toymoban.com/news/detail-437735.html
# mybatisPlus的id生成器来说,默认使用的是雪花算法 防止id生成重复的配置如下:
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
type-aliases-package: com.xxxx.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
datacenter-id: ${random.int(1,31)}
worker-id: ${random.int(1,31)}
到了这里,关于Java8中DateTimeFormatter真的是线程安全的吗?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!