这周有同事在部署一个系统的时候出了生产事故,原因是别人系统的 jar 包有很多,原本是需要 jar 包在自然顺序按照 a-z 排序的,不然就会出问题(就很离谱),暂且不谈这个的合理性吧,我们只讨论如何将一些自然排序是乱序的文件改成自然排序是 a-z
于是我给 ChatGPT 提问了,我告诉他我有三个文件 c.jar b.jar a.jar ,他们的自然排序是 a.jar c.jar b.jar ,怎么再不修改文件内容和文件名等信息的前提下改变他们的自然排序,结果 ChatGPT 给我一段扯,搞了半天也改不了,一直在给我改文件名,或者使用链接,反正没有给有效办法。
各位可以试一下:首先在任意目录执行 touch c.jar b.jar a.jar 创建 3 个文件,然后执行 ls -fi 就是查看自然排序,只要不是 a-z 就行,此时你需要想办法让 ls -fi 得到的自然排序是 a-z 排序
![]() |
1
darkengine 99 天前
恕在下孤陋寡闻,什么叫自然排序?
|
![]() |
2
Hopetree OP @darkengine 就是文件不经过排序 -f 就是不排序,不排序就是系统给文件的自然排序
|
![]() |
3
lonelykid 99 天前
头一次听说“自然顺序”,文件排序的展示顺序不就是看上层应用的排序策略吗?如按创建时间、修改时间、文件名、文件类型、文件权限等进行排序。
按理说文件在文件系统里的排序应该和应用加载顺序解耦,外部 jar 包应该在应用内部自己处理加载顺序,解决依赖冲突,而不是通过文件接口获取的默认文件顺序加载。 |
![]() |
4
Hopetree OP @lonelykid 可以搜索关键词“tomcat 加载 jar 包的顺序”,去看一下,如果你恰好是一个 Java 程序员,那么你更应该关注,这里有个坑就是关于自然排序的
|
5
NoOneNoBody 99 天前
自己写脚本“猜”原来的排序改回去,我 py 写过一个,但不好用,又懒得再搞
@darkengine #1 @lonelykid #3 a1b, a2b, ... a10b 这是自然排序,因为按数目 10>1 ,当数目位数不同时,按人类习惯位数多的数目更大,应排在后面 a10b, a1b, a2b, ... 这是字符串排序,因为第三个字符 b>0 另外还有大小写问题,字符串排序 Z<a ,但自然排序则是 a<Z(忽略大小写) |
![]() |
6
geelaw 99 天前 via iPhone
官方名称叫做“目录序”( directory order ),该问题显然没有合理的答案,因为我可以写一个文件系统让它每次枚举的时候都随机一个顺序出来。
|
![]() |
7
darkengine 99 天前
我在 macOS 试了下,这个“自然排序”跟文件创建顺序有关系,跟修改时间没关系:
顺序执行: $ touch 1.txt $ touch 2.txt $ touch 3.txt 看顺序 $ ls -fi 32241091 . 3619702 .. 32241108 3.txt 32241105 2.txt 32241101 1.txt 修改 2.txt $ echo 'hello' > 2.txt 再看顺序 $ ls -fi 32241091 . 3619702 .. 32241108 3.txt 32241105 2.txt 32241101 1.txt 如果要达到效果,看来要用脚本顺序把文件写到目录里了。 |
8
Inn0Vat10n 99 天前
怎么我查 nature sorting 的 wiki 解释和 OP 你的解释不一样。。。
|
![]() |
9
mango88 99 天前
啊? 依赖有冲突不是应该去解决冲突吗,为什么要靠排序来决定类加载顺序
|
![]() |
10
lululau 99 天前
还自然排序,有够扯的
man 2 getdirentries getdirentries() reads directory entries from the directory referenced by the file descriptor fd into the buffer pointed to by buf, in a filesystem independent format. Up to nbytes of data will be transferred. Nbytes must be greater than or equal to the block size associated with the file, see stat(2). Some filesystems may not support getdirentries() with buffers smaller than this size. The data in the buffer is a series of dirent structures (see dir(5)) The order of the directory entries vended out via getdirentries() is not specified. Some filesystems may return entries in lexicographic sort order and others may not. |
11
DT27 99 天前
java 不支持动态加载 jar?
|
![]() |
12
Hopetree OP @mango88 因为是别人的系统,本质的确应该是研发就解决冲突,但是别人一直这么玩都没问题,直到包经过了一轮制品管理自然排序变了,把这个问题暴露出来了,但是系统是别人的,别人不管
|
![]() |
13
Hopetree OP @DT27 去搜一下“tomcat 加载 jar 包的顺序”, 有相关坑,我不是搞 Java 的,不是很清楚,但是这次生产事故就是因为 tomcat 加载 jar 包顺序导致的,需要实现我上面的问题才能解决
|
![]() |
14
CEBBCAT 99 天前
> https://man7.org/linux/man-pages/man1/ls.1.html
-f list all entries in directory order -i print the index number of each file (但跟楼主的 case 似乎没很大的关系) > https://unix.stackexchange.com/a/13456 user732 answered May 19, 2011 at 16:48 It depends on the filesystem. For some filesystems (ext3 among them), a directory is actually a file with a well-known format, and the 'd' bit set in its permissions or mode. In that case, the history of what length filenames have gotten created and deleted can matter. The kernel will fill in the first entry in the directory file that has enough room to hold the new file's name. See http://e2fsprogs.sourceforge.net/ext2intro.html for more detail, the section titled "Physical Description". (这里略去了一部分原文,大意是另外还有一些文件系统,其实是 ls 按字典序排的) 机译:这取决于文件系统。对于某些文件系统(其中包括 ext3 ),目录实际上是具有众所周知格式的文件,并且在其权限或模式中设置了“d”位。在这种情况下,创建和删除文件名长度的历史记录可能很重要。内核将填充目录文件中的第一个条目,该条目有足够的空间来保存新文件的名称。有关更多详细信息,请参阅 http://e2fsprogs.sourceforge.net/ext2intro.html ,标题为“物理描述”的部分。 似乎只要了解使用的文件系统及 ls 的内部实现就可以找到反向操纵 ls -f 的方法 另外需要提醒⚠️ 根据 https://stackoverflow.com/questions/5474765/order-of-loading-jar-files-from-lib-directory 部分人提到 Tomcat 6 到 8 之间的行为有所不同,需要参照对应的文档。 拓展阅读: https://stackoverflow.com/questions/67997151/spring-boot-inner-jar-files-loading-order-embedded-tomcat --- 总结:可以把文件剪去一个新目录,或者按照期望的顺序,mv 到临时文件名再 mv 回来试试看 最后:亲娘嘞,还有这样的翻车方式?真是……Java EE !佩服佩服🙏 /手动狗头 |
15
Inn0Vat10n 99 天前
@Hopetree 不用想了,如果你不从根本解决还想着 ad-hoc ,早晚还得故障,不知道这种东西怎么过你们 QA 的
|
![]() |
16
IDAEngine 99 天前
GPT:
一般来说,您不应该依赖 Tomcat 的 lib 目录中 JAR 文件的特定加载顺序。Java 的类加载机制并不保证特定的加载顺序,试图强制加载顺序可能会导致脆性和不可预测的行为。 但是,如果您绝对必须控制特定 JAR 文件的加载顺序(例如,由于库冲突或非常特定的初始化需求),以下是一些方法及其注意事项: 1.Manifest 中的类路径: 工作原理在网络应用程序 WAR 文件(位于 WEB-INF/classes/META-INF/)的 MANIFEST.MF 文件中,您可以指定一个 Class-Path 条目。该条目列出了应用程序所依赖的其他 JAR ,它们将按照指定的顺序加载。 例如 Manifest-Version: 1.0 类路径:library1.jar library2.jar 注意事项 这种方法只能控制网络应用程序加载其依赖项的顺序。它不会影响 Tomcat 在其全局 lib 目录中加载 JAR 的顺序。 如果管理不慎,它可能会导致 "JAR 地狱",尤其是复杂的依赖关系树。 2.Tomcat 的共享类加载器: 如何工作 Tomcat 有一个 "共享类加载器 "的概念,它从 $CATALINA_HOME/lib 目录中的 JAR 中加载类。从该目录加载 JAR 的顺序会受到操作系统文件系统中 JAR 列出顺序的影响。 注意事项 不建议使用这种方法,因为它会破坏部署在同一 Tomcat 实例上的其他应用程序。 它完全依赖于环境(依赖于文件系统顺序),因此不具备可移植性。 3.自定义类加载器(高级且有风险): 工作原理:您可以在网络应用程序中创建自定义类加载器,以受控顺序从特定 JAR 中加载类。这是一种非常高级的技术,需要深入了解 Java 的类加载机制。 注意事项 这种方法非常复杂,如果不仔细实施,很容易导致微妙的类加载问题。 它会使应用程序的架构和调试变得非常复杂。 为什么依赖加载顺序会产生问题? 易损坏:如果依赖于特定的 JAR 加载顺序,Tomcat 版本、操作系统甚至目录中文件顺序的变化都可能导致应用程序崩溃。 难以调试:当出现类加载问题时,诊断和修复是出了名的困难,尤其是当你以非常规的方式操作类路径时。 违背最佳实践:Java 的类加载系统旨在以稳健、可预测的方式处理依赖关系。试图规避这种机制往往会导致问题多于解决。 推荐方法: 与其试图强制执行特定的加载顺序,不如将重点放在以下方面: 依赖关系管理:使用 Maven 或 Gradle 等构建工具来管理应用程序的依赖关系。这些工具擅长解决冲突,并确保在 WAR 文件中包含正确版本的库。 隔离:将应用程序及其所有必要的依赖项打包到自己的 WAR 文件(或作为 "胖 JAR")中。这样可以确保您的应用程序有自己独立的类路径,避免与同一服务器上运行的其他应用程序发生冲突。 代码设计:如果可能,重构代码以消除对特定 JAR 加载顺序的需求。这可能需要使用接口和依赖注入将代码与具体实现解耦。 通过遵循这些最佳实践,您可以创建更健壮、可维护和可移植的 Java 网络应用程序。 |
![]() |
17
IDAEngine 99 天前
1.了解 Tomcat 类加载:
Tomcat 使用分层类加载模型,不同的类加载器负责从不同位置加载类: Bootstrap 类加载器:加载核心 Java 类。 系统类加载器:从 CLASSPATH 加载类。 公共类加载器:加载所有网络应用程序共享的类。 网络应用程序类加载器:加载每个网络应用程序的特定类。 2.选择方法: 在 Tomcat 中加载自定义类有两种主要方法: a) WEB-INF/lib 中的自定义类加载器: 这种方法是在网络应用程序中创建自定义类加载器,并将其置于 WEB-INF/lib 目录中。这种方法可提供特定于应用程序的控制。 b) catalina.properties 中的自定义类加载器: 这种方法是使用 catalina.properties 文件为 Tomcat 全局配置自定义类加载器。这种方法会影响所有已部署的应用程序。 3.实施自定义类加载器: 无论选择哪种方法,核心逻辑都是一样的: public class MyCustomClassLoader extends URLClassLoader { public MyCustomClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 1. Check if class is already loaded Class<?> loadedClass = findLoadedClass(name); if (loadedClass != null) { return loadedClass; } // 2. Custom logic to prioritize JARs for (URL url : getURLs()) { if (url.getFile().endsWith(".jar")) { try (JarFile jarFile = new JarFile(new File(url.toURI()))) { // 3. Iterate through entries in each JAR Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); if (entry.getName().endsWith(".class") && entry.getName().replace('/', '.').substring(0, entry.getName().length() - 6).equals(name)) { byte[] classBytes = loadClassData(jarFile, entry); return defineClass(name, classBytes, 0, classBytes.length); } } } } } // 4. Delegate to parent if not found return super.loadClass(name, resolve); } // Helper method to load class data from JAR private byte[] loadClassData(JarFile jarFile, JarEntry entry) throws IOException { try (InputStream is = jarFile.getInputStream(entry)) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int nRead; byte[] data = new byte[16384]; while ((nRead = is.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } return buffer.toByteArray(); } } } 4.配置 Tomcat (针对方法): a) WEB-INF/lib 方法: 编译自定义类加载器并将其打包为 JAR 文件。 将 JAR 文件放到网络应用程序的 WEB-INF/lib 目录中。 b) catalina.properties 方法: 编译自定义类加载器并将其打包为 JAR 文件。 将 JAR 文件放到 Tomcat 的 lib 目录中。 编辑 catalina.properties 文件(位于 Tomcat 的 conf 目录中)并添加以下一行,用自定义类加载器的完全合格名称替换 com.example.MyCustomClassLoader: loader.path=com.example.MyCustomClassLoader 5.自定义加载逻辑: 在自定义类加载器的 loadClass 方法中,可以实现优先加载 JAR 文件的逻辑。这可能包括 根据预定义顺序检查 JAR 文件名或路径。 根据 JAR 属性实施加权系统。 使用配置文件或系统属性确定加载顺序。 这种方法允许您通过自己的逻辑和顺序来控制类加载过程,从而自定义 Tomcat 如何加载目录中的 JAR 文件。 |
18
whileFalse 99 天前 via Android
先 mv 走再按顺序 mv 回来不就完了。
顺便@ Livid 这里一堆 chatgpt |
![]() |
19
msg7086 99 天前
首先你说的这个不叫自然排序。自然排序是另一个功能,是一个专有名词。你这个是返回底层文件系统驱动的原式顺序,所以就是「不排序」而非自然排序。
不排序时得到的文件顺序是底层文件系统驱动返回的顺序,这就意味着这个顺序是跟驱动程序代码有关的。如果我写一个 FUSE ,每次返回文件列表的顺序都是随机的,你怎么办? 所以你这个问题是需要严格限定所使用的文件系统和驱动版本的。 如果驱动返回的文件顺序是 inode 在目录项目里建立的顺序,那只要先把文件全部 mv 到其他地方,然后按照你期望的排序顺序一个一个 mv 回目录就行了。 |
![]() |
20
msg7086 99 天前 ![]() 另外如果只要按照 shell 的默认顺序排列的话,直接
mv * ../tmp mv ../tmp/* . 就行了,shell 在展开*通配符的时候会做排序。 |
![]() |
22
Hopetree OP @CEBBCAT 的确是很离谱,说白了是别人系统的问题,但是别人说他们手动部署没问题啊,我们自动部署改变了这个排序,所以导致依赖冲突,然后出问题,本质虽然是研发层的问题,但是的确是运维层把问题引爆的,所以运维背锅,其实方案就是使用 zip 按照 a-z 的顺序循环将文件打包进去,然后解压开就是 a-z 的排序了,这也是他们之前的方法。我们由于需要把文件上传到制品库一次,然后下载到服务器,所以文件的顺序就变了,就出问题了
|
![]() |
23
Hopetree OP @whileFalse 这个方案然后是可行的,现在是使用的 zip 按 a-z 打包进去,然后解压开就是 a-z 排序,我理解原理应该跟 mv 一样吧
|