dkq7foFaUqi 发表于 3 天前

经由同个文件多次压缩的文件MD5都不一样问题排查,感慨AI的强大!

开心一刻

今天点了个外卖:牛肉炒饭
外卖到了后,发现并没有牛肉,我找商家理论
我:老板,这个牛肉炒饭的配菜是哪些?
商家:青菜 豆芽 火腿 鸡蛋 葱花
我:没有牛肉?
商家:亲,没有的哦
我:我点的牛肉炒饭没有牛肉,你这不是虚假宣传?
商家:亲,你误会了,牛肉是我们的厨师名字!

问题描述

先跟大家统一一个概念:文件的MD5,它是一种用于验证文件完整性的哈希值,一个文件的MD5值是固定的。文件的MD5值获取方式有多种,Linux 下可以通过 md5sum 命令获取
# md5sum run.sh
Win 下则通过 certutil 命令获取
D:\>certutil -hashfile run.sh MD5
各个开发语言也有对应的获取方式,例如 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;      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
/** * 通过 Apache Commons Codec 获取文件的MD5 * @author 青石路 */public static String getFileMd5ByCodec(Path path) throws IOException {    try(InputStream is = Files.newInputStream(path)) {      return DigestUtils.md5Hex(is);    }}
[*]Guava
/** * 通过 Guava 获取文件的MD5 * @author 青石路 */public static String getFileMd5ByGuava(Path path) throws IOException {    return com.google.common.io.Files.hash(path.toFile(), Hashing.md5()).toString();}
[*]Hutool
/** * 通过 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
/** * 通过 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 压缩

[*]引入依赖
<dependency>    <groupId>net.lingala.zip4j</groupId>    <artifactId>zip4j</artifactId>    <version>2.11.3</version></dependency>
[*]实现 zip 压缩
/** * 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就应该是一致的,对不对?我们来看一个案例
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,实例代码如下
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:\\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是否与其时间戳有关?

[*]压缩包中文件的时间戳
这个很好验证,源文件打包进压缩包的时候,保留其修改时间即可,代码如下
/** * 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);            }      }    }}删除旧压缩包后重新进行打包测试
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);    // 压缩间隔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 这个坎会持续提前!!!
页: [1]
查看完整版本: 经由同个文件多次压缩的文件MD5都不一样问题排查,感慨AI的强大!