昨天在修 log4j 漏洞突然有一点疑问,请各位大佬解惑:
首先自己的服务是 maven 和 spring-boot 构建的,昨天先用 mvn dependency:tree -Dincludes=log4j 排查了自己的微服务 A 用的 log4j 2.11 是在第三方包 elasticsearch-6.43 下面,然后根据以往经验,就在自己的 A 服务的 pom.xml 中显示添加 log4j 2.15.0 版本,之前自己服务的 pom 是没有显示声明的,重新打包构建 mvn package。打好包 A.jar 后又将其解压看了下 BOOT-INFO/lib 下面 log4j 相关包都是 2.15.0 了,然后我又解压同目录下的 elasticsearch-6.43.jar 包,下面都是 classes 文件。一开始感觉很理所当然,但突然有几个问题:

显式的在自己服务的 pom 文件声明使用的依赖包比如 log4j 一定会覆盖下层第三方包依赖的 log4j 吗?这是由 maven 的什么机制决定的?
虽然自己的 A 服务用 pom 里声明的 log4j 2.15 版本构建出来 jar 包,也就是 classes 文件(我理解为二进制),但它依赖的 elasticsearch-6.43 jar 包 classes 不是已经是根据老版本的 log4j 2.11 构建出来的吗?所以 elasticsearch 这个 jar 包不是还是有问题,还是没有根本解决项目的 log4j 的漏洞问题?难道说 java 的 classes 文件是像动态链接库一样,运行时 elasticsearch-6.43 jar 会调用 2.15 的 log4j jar 包?这是 Java 的什么机制呢?

虽然我不是做后台的。 但是很理解你的感受。
有时候这些底层逻辑文档有点不太清楚。 自己得花好长时间走好多弯路才能研究透。

www.runoob.com/maven/maven-manage-dependencies.html

classloader 看命呗
你应该 exclude 掉别的 log4j

maven 有一套依赖版本处理方法,这个不重复,搜下就有很多

你的 pom deps 加了 log4j 2.15.0 ,elasticsearch (依赖 log4j 2.11 )..maven 经过计算..选择 log4j 版本是 2.15.0,那么最终你打包就是 log4j 2.15.0,也就是说 elasticsearch 就是调用 log4j 2.15.0.

这并不是什么 Java 机制..就是暴力直接替换...所以当 api 变化大时,会导致出现各种问题...

所以 maven 配置的本来就不是无脑随意配置...

关于第二点,我的理解不是 java 的机制,而是 spring 的容器化,把所有的对象都注册到了 spring 容器里,然后按需调用。申明的版本只是区别你组件的本身,但是调用对象还是根据 artifaceid 及类的具体名称来定位的,而不是 pom 里面的 version 。
你写代码的时候,都是写 com.demo.application.Test ,不需要区分版本,既然你引入了最新的,其他有使用了的肯定都使用最新的。

是的,之前感觉比较自然的事情突然有点想不通了

谢谢,请问能解答下第二点吗?

不 exclude 的话还会有问题?

谢谢解答,但是 elasticsearch 这个 jar 包不是从 maven 中央仓库下好的官方用 log4j 2.11 打好的 jar 包吗?它的 classes 文件是不是已经把 log4j 2.11 的东西“揉进去”了(原谅我这种不太恰当的表达)?还是说 elasticsearch 的 classes 文件只是填充了 log4j 的函数接口名字什么的,没有规定版本,具体“链接”的时候再去找同目录下面具体版本的 log4j jar 包的 classes 文件?

当你处理所有依赖时,如果发现有重复的依赖,最好的方案就是 exclude,然后在自己项目 pom 明确声明这些依赖的版本..

要不然就是查看最终依赖 tree,观察是否都是自己想要的版本..

你说的第二点.他就是 maven 算出来最终一个版本,然后就打包了..maven 不会关心 elasticsearch 你用新版的 log4j 会不会出问题..

你看看你最终的包里面的 lib 包有什么就知道了,根本没有 2.11 的 jar 包,只剩 2.15 的了

谢谢,这个我理解的。但是 elasticsearch 这个 jar 包不是从 maven 中央仓库下好的官方用 log4j 2.11 打好的 jar 包吗?它的 classes 文件不是已经把 log4j 2.11 的东西“揉进去”了(原谅我这种不太恰当的表达)?还是说 elasticsearch 的 classes 文件只是填充了 log4j 的函数接口名字什么的,没有规定版本,具体“链接”的时候再去找同目录下面具体版本的 log4j jar 包的 classes 文件?

第一个问题:pom 文件是继承的.这是一个优先级的问题只会取一个版本,类似于子类重写了父类的实现.优先使用你的项目的 pom 的版本指定的依赖.一定会覆盖下层第三方包依赖因为你在本地显示声明了.

第二个问题 classes 文件并不是二进制只是字节码.jar 包的加载过程是去加载器中查找依赖的包.那么你的 pom 已经定义了新的包只会使用哪个版本的包.这就是为什么安全升级有时候你发现升级的版本不兼容过新.因为这个构建是不知道版本的它只认完整的包名+类名,你不声明版本.它自带的 pom 会引入依赖.你声明了就会使用你显示声明的版本的包. 建议看下 JVM 加载 jar 的流程.

如何去验证?
你完全可以排掉依赖的包.加载的时候就会报错 notfoundclass.

这么说吧..java 不存在什么链接...
在 elasticsearch 自己编译时,他依赖的是 log4j 2.11...然后用到了 2.11 种的各种类.接口等等...当编译完成时.实际 elasticsearch 种不会存在任何 log4j 2.11 的文件等等...

当你自己项目用了一个高版本的 log4j..那么 elasticsearch 运行就是直接调用这个高版本

==========
当然这里说的都是常规的..java 可以内部直接包含依赖,然后不暴露出来,比如 java module

喔,相当于运行时代码会根据类名去调用目录下面同名的 jar 包,感觉也就像运行期动态链接一样,maven 只是简单的把包放在了那里仅此而已?

其实这就是 class 与 classloader 的内容了....学下就知道了

应该是调用包的时候,调用的地方其实只有一个指针(package 的全路径)指向要访问调用的代码,有没有是加载起来的, 应该同样的 package 都是一样的

好的,谢谢大佬!

谢谢解答。昨天修完就在想如果把 log4j 降到一个很低且不兼容 elasticsearch 的版本应该就会报错,大佬这么说感觉应该就是这样,我去补一下加载这块。

嗯谢谢解答,我感性理解也是这样

by the way, 如果如你所说, 你的 log4j 的依赖引入来自 elasticsearch , 我应该假设你使用的的是 org.elasticsearch:elasticsearch, org.elasticsearch.client:elasticsearch-rest-high-level-client, org.elasticsearch.client:elasticsearch-rest-client 的 api 相关的几个依赖, 这里面依赖的只有 log4j-api, 如果后面的 impl 不是 log4j (对应的 应该是 log4j-core 里面, 这次出漏洞的地方就在这里面)目前看是不受影响的

看你这个问题,你对 Java 的理解还不够深入,你以前应该是写 C/C++为主的把?

惭愧,写 java 和 go 的 java 现在用得少了,之前看过八股文没用就忘了。

的确,一开始看白帽上 nosec.org/home/detail/4917.html 说可能都有影响保险起见就全换了

  1. maven 指定版本优先,没指定按最短路径优先。
  2. 双亲委派原则,不同的类加载器有顺序

看了评论,学习了

学习了

maven 的依赖 resolve 是协作式的, 同一个包 (groupId:artifactId) 只能导入一个版本, 具体 resolve 到的哪一个版本在编译时是确定的, 规则虽然不是特别的复杂, 但也不是一句话能说的清楚, 可以用工具帮助解决冲突

并且实际上, groupId:artifactId 和 package FQN 没有严格对应关系这使得问题会更加复杂一些, 同一个 package 可能会被不同的 artifact 导入了超过了, 那么这时候编译以及运行时使用的是哪一个 jar 就变的不确定了, 这样的情况要尽量避免出现

作为想对面, npm 采取的是完全相反的方式, 就是每个 package 及其依赖都是尽量内蕴的, 两个一级依赖之所以用到了同一个次级依赖, 只不过是因为这个二级依赖的版本号恰巧是相同 (npm 会在条件允许下让它们尽量恰巧相同)

表面看起来 npm 的依赖管理简单易懂, 但实际上却复杂的多的多, 并造成了一大堆遗留到现在都很无语的问题

怎么这么多扯淡的回复,这和 class loader 有个卵关系。java 的 jar 包是没有版本号的 module 也没有,这也是为什么两个不同版本的包会冲突,原因是有相同全限定名的 class (包前缀加类名)。加载时根据 class path 查找对应名字的 class 文件,只要文件结构没改,大体上是兼容的。比如早期的 slf4j 编译需要一个 logger 的实现,slf4j 里有一个空的实现,但是编译完成之后就把它删除了,但你依赖中只要有其他的实现一样可以运行。

谢谢大佬们回复,现在已经大致明白了,编译好的 class 文件只是有对依赖的类的引用(类似只保留了依赖的类名、函数名而已,感觉理解成动态链接库也差不多?),没有具体的版本号。由于此前已经在 pom 指定了 2.15 版本的 log4j ,也就是解压后 BOOT-INFO 下唯一存在的 log4j 2.15 jar 包,因此在程序启动时会进行类加载,会根据程序中引用了哪些类,根据其完整的类名去查找、加载依赖的类 log4j ,这时候加载的实际就是 BOOT-INFO 下的 log4j 2.15 jar 包,这时候不管是自己的程序还是依赖的其它第三方包比如 elasticseach 等都会使用加载的 2.15 log4j 。

elasticsearch 调用的只是接口吧 用的是新的实现...我猜的