当前位置: 首页 > news >正文

Spring Boot 项目中,同一个版本的依赖,内容却不一样?一次因依赖污染导致 Redis 启动失败的排查

最近修改了一段代码,引入了 Redisson。本地运行正常,但在 Jenkins 打包并部署到 K8s 环境后,服务启动失败,接口提示 Redis 访问异常。

错误信息显示找不到类:org.springframework.data.redis.connection.RedisStreamCommands。我在 IDEA 中搜索了该类,发现当前使用的 Spring Data Redis 版本中确实不存在这个类,但本地程序却能正常启动。

img_6

为了进一步排查,我通过添加 JVM 参数 -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=n 进行远程调试。发现线上环境确实尝试加载了 RedisStreamCommands 类,而本地程序并未触发该行为。

由此怀疑:Jenkins 构建出的 jar 包与本地构建的版本存在差异

于是决定对比两个 jar 包中 BOOT-INF/lib 目录下的依赖。果然发现依赖数量不一致。由于涉及多个依赖项,让AI编写了一个工具类来自动对比。

对比两个 jar 包的依赖

目标是对比两个 jar 包中 BOOT-INF/lib 下的文件,包括:

  • 文件数量;
  • 仅一方存在的 jar;
  • 同名 jar 的内容是否一致(通过 MD5 校验);

之所以校验 MD5,是因为其他部门存在不升级版本号的情况下修改 SNAPSHOT 包的内容的情况。

import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;public class ZipLibComparator {private static final String LIB_PATH = "BOOT-INF/lib/";public static void compareZipLibs(String zipPath1, String zipPath2) {System.out.println("=== 比较两个 ZIP 文件中 BOOT-INF/lib/ 下的文件 ===\n");Map<String, String> libFiles1 = extractLibFiles(zipPath1);Map<String, String> libFiles2 = extractLibFiles(zipPath2);if (libFiles1 == null || libFiles2 == null) {System.err.println("无法读取其中一个或两个 ZIP 文件。");return;}System.out.println("📁 文件1: " + zipPath1 + " → " + libFiles1.size() + " 个文件");System.out.println("📁 文件2: " + zipPath2 + " → " + libFiles2.size() + " 个文件\n");Set<String> allFileNames = new HashSet<>();allFileNames.addAll(libFiles1.keySet());allFileNames.addAll(libFiles2.keySet());List<String> onlyIn1 = new ArrayList<>();List<String> onlyIn2 = new ArrayList<>();List<String> differentMd5 = new ArrayList<>();for (String fileName : allFileNames) {if (!libFiles1.containsKey(fileName)) {onlyIn2.add(fileName);} else if (!libFiles2.containsKey(fileName)) {onlyIn1.add(fileName);} else {String md5_1 = libFiles1.get(fileName);String md5_2 = libFiles2.get(fileName);if (!md5_1.equals(md5_2)) {differentMd5.add(fileName + " (MD5不同: " + md5_1 + " vs " + md5_2 + ")");}}}// 输出结果System.out.println("✅ 文件名相同且内容一致的文件数: " +(allFileNames.size() - onlyIn1.size() - onlyIn2.size() - differentMd5.size()));if (!onlyIn1.isEmpty()) {System.out.println("\n🟡 仅在【文件1】中存在的文件 (" + onlyIn1.size() + " 个):");onlyIn1.forEach(System.out::println);}if (!onlyIn2.isEmpty()) {System.out.println("\n🔵 仅在【文件2】中存在的文件 (" + onlyIn2.size() + " 个):");onlyIn2.forEach(System.out::println);}if (!differentMd5.isEmpty()) {System.out.println("\n🔴 文件名相同但内容不同(MD5不同)的文件 (" + differentMd5.size() + " 个):");differentMd5.forEach(System.out::println);}if (onlyIn1.isEmpty() && onlyIn2.isEmpty() && differentMd5.isEmpty()) {System.out.println("\n🎉 两个 ZIP 文件中的 BOOT-INF/lib/ 内容完全一致!");}}private static Map<String, String> extractLibFiles(String zipPath) {Map<String, String> fileMd5Map = new HashMap<>();try (ZipFile zipFile = new ZipFile(zipPath)) {Enumeration<? extends ZipEntry> entries = zipFile.entries();while (entries.hasMoreElements()) {ZipEntry entry = entries.nextElement();String entryName = entry.getName();// 忽略目录,只处理文件,且路径以 BOOT-INF/lib/ 开头(忽略大小写)if (!entry.isDirectory() && entryName.toLowerCase().startsWith(LIB_PATH.toLowerCase())) {String fileName = entryName.substring(LIB_PATH.length());String md5 = computeMd5(zipFile.getInputStream(entry));fileMd5Map.put(fileName, md5);}}} catch (IOException e) {System.err.println("读取 ZIP 文件失败: " + zipPath + " → " + e.getMessage());return null;}return fileMd5Map;}private static String computeMd5(InputStream inputStream) {try {MessageDigest md = MessageDigest.getInstance("MD5");byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {md.update(buffer, 0, bytesRead);}byte[] digest = md.digest();StringBuilder sb = new StringBuilder();for (byte b : digest) {sb.append(String.format("%02x", b));}return sb.toString();} catch (Exception e) {System.err.println("计算 MD5 失败: " + e.getMessage());return "MD5_ERROR";}}// 主方法用于测试public static void main(String[] args) {// 示例用例:请替换为你本地实际存在的两个 ZIP/JAR 文件路径String zipPath1 = "D:\\a.jar";  // ← 替换为你的第一个文件路径String zipPath2 = "E:\\b.jar";  // ← 替换为你的第二个文件路径System.out.println("🔍 开始对比两个 ZIP 文件中 BOOT-INF/lib/ 下的内容...\n");compareZipLibs(zipPath1, zipPath2);}
}

使用该工具对比本地打包和 Jenkins 打包生成的 jar 文件,发现两者差异较大。

接着,我清空了本地仓库中所有公司相关的依赖,并重新拉取。

2025-09-10-09-33-03-image

本以为重新拉取后,本地与 Jenkins 的依赖应保持一致(Jenkins 构建时已添加 -U 参数,理论上会强制更新 SNAPSHOT 依赖),但运行结果仍与之前相同。问题仍未解决。

于是,我决定进一步对比两个 jar 包中某个不同依赖的具体文件内容。

对比整个 jar 包的文件内容

我又编写了一个工具,用于对比两个压缩包中所有文件的路径和 MD5 值。

import java.io.*;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;public class ZipFullComparator {public static void compareAllFilesInZip(String zipPath1, String zipPath2) {System.out.println("🔍 开始全面对比两个 ZIP 压缩包中的所有文件内容...\n");// 提取两个 ZIP 中的 {相对路径 -> MD5} 映射Map<String, String> files1 = extractAllFilesWithMd5(zipPath1);Map<String, String> files2 = extractAllFilesWithMd5(zipPath2);if (files1 == null || files2 == null) {System.err.println("❌ 读取 ZIP 文件失败,请检查路径是否正确或文件是否损坏。");return;}System.out.println("📦 压缩包1: " + zipPath1 + " → 包含 " + files1.size() + " 个文件");System.out.println("📦 压缩包2: " + zipPath2 + " → 包含 " + files2.size() + " 个文件\n");Set<String> allPaths = new HashSet<>();allPaths.addAll(files1.keySet());allPaths.addAll(files2.keySet());List<String> onlyIn1 = new ArrayList<>();List<String> onlyIn2 = new ArrayList<>();List<String> contentDifferent = new ArrayList<>();for (String path : allPaths) {boolean in1 = files1.containsKey(path);boolean in2 = files2.containsKey(path);if (in1 && !in2) {onlyIn1.add(path);} else if (!in1 && in2) {onlyIn2.add(path);} else if (in1 && in2) {String md5_1 = files1.get(path);String md5_2 = files2.get(path);if (!md5_1.equals(md5_2)) {contentDifferent.add(path + " (MD5: " + md5_1 + " ≠ " + md5_2 + ")");}}}// 输出结果System.out.println("✅ 内容完全相同的文件数: " +(allPaths.size() - onlyIn1.size() - onlyIn2.size() - contentDifferent.size()));if (!onlyIn1.isEmpty()) {System.out.println("\n🟡 仅在【第一个压缩包】中存在的文件 (" + onlyIn1.size() + " 个):");onlyIn1.forEach(System.out::println);}if (!onlyIn2.isEmpty()) {System.out.println("\n🔵 仅在【第二个压缩包】中存在的文件 (" + onlyIn2.size() + " 个):");onlyIn2.forEach(System.out::println);}if (!contentDifferent.isEmpty()) {System.out.println("\n🔴 文件路径相同但内容不同(MD5不同)的文件 (" + contentDifferent.size() + " 个):");contentDifferent.forEach(System.out::println);}if (onlyIn1.isEmpty() && onlyIn2.isEmpty() && contentDifferent.isEmpty()) {System.out.println("\n🎉 两个压缩包中所有文件内容完全一致!");} else {System.out.println("\n📌 总结:发现差异文件共 " +(onlyIn1.size() + onlyIn2.size() + contentDifferent.size()) + " 个。");}}/*** 遍历 ZIP 文件,提取所有非目录项的 {相对路径 -> MD5} 映射*/private static Map<String, String> extractAllFilesWithMd5(String zipPath) {Map<String, String> pathToMd5 = new LinkedHashMap<>(); // 保持顺序便于调试try (ZipFile zipFile = new ZipFile(zipPath)) {Enumeration<? extends ZipEntry> entries = zipFile.entries();while (entries.hasMoreElements()) {ZipEntry entry = entries.nextElement();// 跳过目录if (entry.isDirectory()) {continue;}String entryPath = entry.getName();InputStream inputStream = zipFile.getInputStream(entry);String md5 = computeMd5(inputStream);pathToMd5.put(entryPath, md5); // 注意:保留原始路径(含大小写),可用于精确对比}} catch (IOException e) {System.err.println("❌ 无法读取 ZIP 文件: " + zipPath + " → " + e.getMessage());return null;}return pathToMd5;}/*** 计算输入流的 MD5 值*/private static String computeMd5(InputStream inputStream) {try {MessageDigest md = MessageDigest.getInstance("MD5");byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {md.update(buffer, 0, bytesRead);}byte[] digest = md.digest();StringBuilder sb = new StringBuilder();for (byte b : digest) {sb.append(String.format("%02x", b));}return sb.toString();} catch (Exception e) {System.err.println("⚠️  MD5 计算失败: " + e.getMessage());return "ERROR_MD5";}}// ==================== 主方法:内置测试用例 ====================public static void main(String[] args) {// 🧪 === 🔔 替换为你本地实际存在的两个 ZIP/JAR 文件路径 ===String zipFile1 = "D:\\a-SNAPSHOT.jar";   // ← 修改为你的第一个压缩包路径String zipFile2 = "D:\\l\\a.jar";   // ← 修改为你的第二个压缩包路径// 检查文件是否存在File f1 = new File(zipFile1);File f2 = new File(zipFile2);if (!f1.exists()) {System.err.println("❌ 文件不存在: " + zipFile1);return;}if (!f2.exists()) {System.err.println("❌ 文件不存在: " + zipFile2);return;}compareAllFilesInZip(zipFile1, zipFile2);}
}

运行后确认:两个 jar 包的文件内容确实不一致。

2025-09-10-10-10-29-image

最后查看了 MANIFEST.MFpom.properties 文件,发现问题根源:同一个版本的依赖包,由不同人构建,时间相差两个月,且代码内容不同。这正是依赖版本管理混乱的典型后果——不应在不升级版本号的情况下修改 SNAPSHOT 包。

进一步核对私服上的依赖,发现本地拉取的版本与私服一致。因此推测:Jenkins 构建时可能从某个缓存或中间仓库拉取了旧版本依赖,尽管使用了 -U 参数,但仍未能强制更新。

由于 Jenkins 所用 Maven 仓库服务器权限受限,无法直接清理,只能联系运维协助删除旧依赖。

这个问题可能早已存在,只是此次恰好引发了运行时异常。开发过程中规范依赖管理非常重要,否则会耗费大量时间在排查此类问题上。本次经历也算是一次有价值的教训。

http://www.wxhsa.cn/company.asp?id=140

相关文章:

  • 微信机器人开发文档
  • 从0到1:餐饮微信点餐小程序源码解析(含扫码点餐+外卖系统+后台管理)
  • 推荐一款线程or进程间数据同步解决方案
  • part 2
  • Apache服务器自动化运维与安全加固脚本详解
  • 无障碍资源导航
  • The 2022 ICPC Asia Shenyang Regional Contest
  • 还在微信群追问任务进展?领歌看板让逾期工作无处可藏
  • 别再猜了-开始测量吧-一份实用的Web性能指南
  • 你的开发服务器在说谎-热重载与热重启的关键区别
  • 大屏开发
  • 检测域名证书有效期
  • PostgreSQL 内机器学习的关键智能算法研究
  • [node] Linux 环境安装 nvm 并通过 nvm 控制 node 版本
  • Gitee崛起:中国开发者为何纷纷转向本土代码托管平台
  • TCP反向代理:将局域网内部的TCP/HTTP服务暴露在公网上
  • 告别数月等待:数字孪生场景生成从此进入“日级”时代
  • Vue.js:大屏开发实战
  • xtrabackup8.0本地备份和恢复(xbstream+gzip)
  • Docker网络
  • 神器内存分配器(Allocator)设计:从原理到高性能实现的深度探索
  • 后端Coder如何做好代码设计?
  • Symfony学习笔记 - Symfony Documentation - Frontend
  • xtrabackup8.0本地备份和恢复(xbstream+compress)
  • 安装云图解析python模块碰到的问题
  • 计算机使用问题集
  • Docker
  • 前端调试列出方法和属性
  • JDK环境变量配置
  • Gitee DevOps:打造中国开发者专属的全流程效能引擎