开心一刻
今天点了个外卖:牛肉炒饭
外卖到了后,发现并没有牛肉,我找商家理论
我:老板,这个牛肉炒饭的配菜是哪些?
商家:青菜 豆芽 火腿 鸡蛋 葱花
我:没有牛肉?
商家:亲,没有的哦
我:我点的牛肉炒饭没有牛肉,你这不是虚假宣传?
商家:亲,你误会了,牛肉是我们的厨师名字!
问题描述
先跟大家统一一个概念:文件的MD5
,它是一种用于验证文件完整性的哈希值,一个文件的MD5值是固定的 。文件的MD5值获取方式有多种,Linux 下可以通过 md5sum
命令获取
shell
[root@k8s-master opt]# md5sum run.sh
Win 下则通过 certutil
命令获取
bat
D:\>certutil -hashfile run.sh MD5
各个开发语言也有对应的获取方式,例如 Java
java
/**
* 通过 JDK 获取文件的MD5
* @author 青石路
*/
public static String getFileMd5ByJdk(Path path) throws Exception {
MessageDigest digest = MessageDigest.getInstance("MD5");
try (InputStream fis = Files.newInputStream(path)) {
byte[] byteArray = new byte[1024];
int bytesCount = 0;
while ((bytesCount = fis.read(byteArray)) != -1) {
digest.update(byteArray, 0, bytesCount);
}
}
byte[] bytes = digest.digest();
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
但是我们通常会用第三方组件或框架来实现,例如
-
Apache Commons Codec
java/** * 通过 Apache Commons Codec 获取文件的MD5 * @author 青石路 */ public static String getFileMd5ByCodec(Path path) throws IOException { try(InputStream is = Files.newInputStream(path)) { return DigestUtils.md5Hex(is); } }
-
Guava
java/** * 通过 Guava 获取文件的MD5 * @author 青石路 */ public static String getFileMd5ByGuava(Path path) throws IOException { return com.google.common.io.Files.hash(path.toFile(), Hashing.md5()).toString(); }
-
Hutool
java/** * 通过 Spring 获取文件的MD5 * @author 青石路 */ public static String getFileMd5BySpring(Path path) throws IOException { try(InputStream is = Files.newInputStream(path)) { return org.springframework.util.DigestUtils.md5DigestAsHex(is); } }
-
Spring
java/** * 通过 Hutool 获取文件的MD5 * @author 青石路 */ public static String getFileMd5ByHutool(Path path) throws IOException { try(InputStream is = Files.newInputStream(path)) { return DigestUtil.md5Hex(is); } }
这些方式获取的 MD5 值都是一致的,都是 cf51e1e40cd1964827bf02916231be85
至此,相信你们对 文件的MD5
都理解了;接下来回到正题,我先复现下问题,既然是压缩,那就把压缩代码整起来,基于 zip4j
实现 zip 压缩
-
引入依赖
xml<dependency> <groupId>net.lingala.zip4j</groupId> <artifactId>zip4j</artifactId> <version>2.11.3</version> </dependency>
-
实现 zip 压缩
java/** * zip 压缩 * @author 青石路 * @param destFilePath 压缩文件路径 * @param sources 源文件列表 * @throws IOException 压缩异常 */ public static void compressZip(String destFilePath, List<File> sources) throws IOException { try(ZipFile zipFile = new ZipFile(destFilePath)) { for (File sourceFile : sources) { ZipParameters param = new ZipParameters(); param.setCompressionMethod(CompressionMethod.DEFLATE); param.setCompressionLevel(CompressionLevel.NORMAL); param.setFileNameInZip(sourceFile.getName()); try (FileInputStream is = new FileInputStream(sourceFile)) { zipFile.addStream(is, param); } } } }
代码很简单,相信你们都能看懂;照理来说,只要源文件列表(sources
)的顺序是固定的,那么压缩之后得到的zip包文件的MD5就应该是一致的,对不对?我们来看一个案例
java
public static void main(String[] args) throws Exception {
List<File> sources = new ArrayList<>();
sources.add(new File("D:\\run.sh"));
sources.add(new File("D:\\hello.txt"));
String zip1 = "D:\\qsl1.zip";
String zip2 = "D:\\qsl2.zip";
compressZip(zip1, sources);
TimeUnit.SECONDS.sleep(1);
compressZip(zip2, sources);
System.out.println("zip1 MD5:" + getFileMd5ByCodec(Paths.get(zip1)));
System.out.println("zip2 MD5:" + getFileMd5ByCodec(Paths.get(zip2)));
}
这个代码你们肯定都能看懂,但我还是要强调一下
两次压缩间隔了 1 秒,是模拟实际项目中的两次压缩的时间间隔
实际项目中间隔肯定不止 1 秒,设置成 1 秒是为了达到同样效果的同时快速出结果
执行如上代码,结果如下
两个压缩包的 MD5 不一致
这是为什么?
问题排查
源文件列表 sources
是同一个(文件一致、顺序也一致),打包方法也是同一个(compressZip
),为什么得到的压缩包的MD5会不一致?会不会是 Codec
组件(因为用的 getFileMd5ByCodec
方法获取的压缩包的MD5)的问题,后面切成 getFileMd5ByJdk
结果与 getFileMd5ByCodec
一致,这说明获取文件的MD5是没问题的;莫非是压缩包名的问题?这个我们可以反向验证下,同个文件复制一份,验证下复制文件的MD5与源文件的MD5是不是一致
可以看到,复制文件的MD5与源文件的MD5一致,所以问题应该出在 compressZip
上,具体出在哪,我也没有可排查的方向了;此时,换做是你们,你们会怎么排查?现在 AI
这么火热,不得问问它?
讯飞星火
给出了 4 个方向,我们逐一分析下
-
时间戳
是指 ZIP 包的创建时间和修改时间
还是指 ZIP 包中文件的创建时间和修改时间
有待进一步分析
-
压缩算法版本
这个可以排除,因为用的是同个压缩打包方法:
compressZip
,并且从上图可以看出,压缩算法都是:Deflate
,版本都是20
-
文件系统差异
这个也可以排除,都是基于 Win10 的
FAT
文件系统 -
随机数或唯一标识符
这个也可以排除,没有随机数和唯一标识
所以我们需要重点分析下时间戳,时间戳又分两个方向
-
压缩包的时间戳
还记得前面压缩包名的验证吗,复制文件和源文件的MD5一致,也变相验证了文件MD5不受压缩包的
创建时间
的影响
源文件和复制文件的
创建时间
与访问时间
不一致,但文件MD5一致,说明文件MD5与创建时间
、访问时间
无关所以我们只需要验证下文件MD5是不是与压缩包的
修改时间
有关即可;很好验证,只需要修改复制文件的修改时间,然后再比较两个文件的MD5,实例代码如下javapublic static void main(String[] args) throws Exception { /*List<File> sources = new ArrayList<>(); sources.add(new File("D:\\run.sh")); sources.add(new File("D:\\hello.txt"));*/ String zip1 = "D:\\qsl1.zip"; String zip2 = "D:\\qsl1 - 副本.zip"; /*compressZip(zip1, sources); TimeUnit.SECONDS.sleep(1); compressZip(zip2, sources);*/ System.out.println("zip1 MD5:" + getFileMd5ByJdk(Paths.get(zip1))); File file2 = new File(zip2); Path path2 = file2.toPath(); // 将副本文件的修改时间增加1分钟 Files.setLastModifiedTime(path2, FileTime.fromMillis(file2.lastModified() + (60 * 1000))); System.out.println("zip2 MD5:" + getFileMd5ByJdk(path2)); }
执行结果如下
所以我们可以得出结论
压缩文件的MD5与压缩包的
修改时间
无关那么再结合前面的
创建时间
、访问时间
,说明压缩包的MD5与压缩包的时间戳无关!引申一个问题:非压缩文件MD5是否与其时间戳有关?
-
压缩包中文件的时间戳
这个很好验证,源文件打包进压缩包的时候,保留其修改时间即可,代码如下
java/** * zip 压缩 * @author 青石路 * @param destFilePath 压缩文件路径 * @param sources 源文件列表 * @throws IOException 压缩异常 */ public static void compressZip(String destFilePath, List<File> sources) throws IOException { try(ZipFile zipFile = new ZipFile(destFilePath)) { for (File sourceFile : sources) { ZipParameters param = new ZipParameters(); param.setCompressionMethod(CompressionMethod.DEFLATE); param.setCompressionLevel(CompressionLevel.NORMAL); param.setFileNameInZip(sourceFile.getName()); // 保留源文件的修改时间 param.setLastModifiedFileTime(sourceFile.lastModified()); try (FileInputStream is = new FileInputStream(sourceFile)) { zipFile.addStream(is, param); } } } }
删除旧压缩包后重新进行打包测试
javapublic static void main(String[] args) throws Exception { List<File> sources = new ArrayList<>(); sources.add(new File("D:\\run.sh")); sources.add(new File("D:\\hello.txt")); String zip1 = "D:\\qsl1.zip"; String zip2 = "D:\\qsl2.zip"; compressZip(zip1, sources); // 压缩间隔1分钟 TimeUnit.MINUTES.sleep(1); compressZip(zip2, sources); System.out.println("zip1 MD5:" + getFileMd5ByJdk(Paths.get(zip1))); File file2 = new File(zip2); Path path2 = file2.toPath(); System.out.println("zip2 MD5:" + getFileMd5ByJdk(path2)); }
执行结果如下
所以我们可以得出结论
压缩包的MD5与压缩包中文件的
修改时间
有关压缩包的MD5否与压缩包中文件的
创建时间
、访问时间
有关,这个交由你们去验证了!
问题修复
如何修复,前面已经讲过了,就是增加一个压缩参数
param.setLastModifiedFileTime(sourceFile.lastModified());
保留源文件的 修改时间
即可;既然前面已经讲过了,为什么还要单独拿个章节来讲?仅仅只是强调下,你们要是不服,来打我呀!
小插曲
Win10 文件夹和文件的 修改日期
只显示到分钟,不显示秒
这也导致解压工具打开压缩包的界面也只显示到分钟
这很容易让我们产生错觉
两次压缩的修改时间(压缩包以及压缩包中的文件)为什么是一样的?
修改时间一致,怎么压缩包的MD5还不一致?
然后就开始自我质疑了,到底哪个环节出了问题?
如果你们看的比较细致的话,会发现我将压缩间隔时间从之前的 1 秒调整成了 1 分钟,因为我就产生了错觉,不怕你们笑话,我在这个错觉上还折腾了挺长时间!!!
另外,7z工具可以查看压缩包中文件的修改时间到秒级别
至于Win10,我始终没有折腾出文件夹和文件的 修改日期
显示到秒
总结
- 非压缩文件的MD5与文件的时间戳无关
- 压缩文件的MD5与压缩文件的时间戳无关,但与压缩包中文件的
修改时间
有关 - Win10 文件夹和文件的
修改日期
只显示到分钟,不显示秒,不显示秒,不显示秒!!! - AI 的愈发成熟,带来了便利的同时也带来了挑战,工作经验的优势会越来越弱,
35
这个坎会持续提前!!!