项目场景:
漏洞修复及深入解析Java类加载机制的思考
安全漏洞修复,需要升级指定版本的jar,其中snakeyaml版本升级到2.0(吐血)
环境:内网环境+内网maven云仓库
springboot版本:2.6.3
默认snakeyaml版本:1.29
问题描述
需要升级指定版本的jar。使用原来springboot版本,只升级snakeyaml版本到2.0
- 在阿里云maven仓库没有2.0版本,手动拉取版本
- springboot2.6.3与snakeyaml2.0不兼容
原因分析:
snakeyaml2.0与springboot2.6.3不兼容
解决方案:
在springcloud项目中,使用父pom管理子pom,在父pom中指定版本。
-
1.下载指定版本依赖
-
找到一台连接互联网的电脑**,手动拉取jar版本,打开maven仓库地址
链接: link
- 复制url
- 在idea中,执行maven命令
-`mvn dependency:get
-DremoteRepositories=url 依赖的URL
-DgroupId=groupId
-DartifactId=artifactId
-Dversion=version`
- 在idea终端或者maven插件中输入
- 替换刚才复制的链接、DgroupId等参数
mvn dependency:get -DremoteRepositories=https://mvnrepository.com/artifact/org.yaml/snakeyaml/2.0 -DgroupId=org.yaml -DartifactId=snakeyaml -Dversion=2.0
- 在本地maven中,打开路径:org.yaml,找到对应版本,复制到内网maven云仓库
- 在父pom指定snakeyaml2.0版本
<snakeyaml.version>2.0</snakeyaml.version>
...
<dependencyManagement>
<dependencies>
<!-- snakeyaml版本 -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
2.启动项目报错问题处理–利用 Java 父子加载器原理覆盖 jar 中类,保证一样的路径和类名
- 把报错的类复制到common公共服务中,补充报错的构造方法。例如复制jar包的Representer,在common服务中创建一样的路径,把Representer放到该路径下
补充无参构造方法
/**
* 新增一个无参构造器
*/
public Representer() {
super(new DumperOptions());
this.representers.put(null, new org.yaml.snakeyaml.representer.Representer.RepresentJavaBean());
}
- 报错2
Correct the classpath of your application so that it contains compatible versions of the classes com.fasterxml.jackson.dataformat.yaml.YAMLParser and org.yaml.snakeyaml.parser.ParserImpl
- 依赖分析,redisson使用了snakeyaml,版本不兼容,一样处理方式
复制YAMLParser 类到公共服务中,处理报错的方法。因为1.29版本不用传LoaderOption对象,2.0+版本要传,故重载这个类
思考
在 Java 应用程序中,如果在类路径中存在相同的包路径和类名,那么 Java 虚拟机会根据类路径中的先后顺序加载类。通常情况下,类路径的顺序决定了哪个版本的类会被加载,这也是为什么你没有创建自定义类加载器,但还是能够覆盖原有类的原因。
在 Java 类加载过程中,具体的加载行为取决于类加载器的类型以及类路径的配置。下面将详细解释类路径顺序和双亲委派模型的工作机制。
双亲委派模型
Java 的类加载器体系结构采用了双亲委派模型,其中类加载器层次分为三种主要类型:
- Bootstrap ClassLoader:负责加载 Java 核心类库,如
java.lang.*
等。 - Extension ClassLoader:负责加载扩展库中的类(
JAVA_HOME/lib/ext
)。 - Application ClassLoader:负责加载应用程序类路径(classpath)中的类。
类路径顺序
当一个类加载器需要加载一个类时,双亲委派模型规定它应首先委派给父加载器进行加载。如果父加载器无法找到该类,那么当前加载器才会尝试加载。如果类路径(classpath)配置中包含多个路径,那么类加载器会按照配置顺序逐个查找类文件。一旦找到匹配的类文件,它就会加载该类,而不会继续查找后面的路径。
具体工作流程
- 请求加载类:当应用程序请求加载一个类时,Application ClassLoader 会收到该请求。
- 委派给父加载器:Application ClassLoader 首先会将加载请求委派给它的父加载器,即 Extension ClassLoader。
- 继续向上委派:如果 Extension ClassLoader 也无法加载该类,则会继续向上委派给 Bootstrap ClassLoader。
- Bootstrap ClassLoader 尝试加载:Bootstrap ClassLoader 尝试加载核心类库中的类。如果成功,则返回类的引用。如果失败,则向下返回,允许 Application ClassLoader 尝试加载。
- Application ClassLoader 查找类路径:如果双亲加载器链都无法加载该类,Application ClassLoader 会按照类路径(classpath)顺序查找类文件。
- 找到类文件并加载:一旦 Application ClassLoader 在类路径中找到匹配的类文件,它会加载该类,并返回类的引用。不会继续查找后面的路径。
实际例子
假设类路径配置如下:
custom_classes:lib/library.jar
在 library.jar
中的类:
// library.jar 中的 ExampleClass
public class ExampleClass {
public void printMessage() {
System.out.println("Original message from library.jar");
}
}
在 custom_classes
目录中的类:
// custom_classes/ExampleClass.java
public class ExampleClass {
public void printMessage() {
System.out.println("Custom message from custom_classes");
}
}
编译自定义类并运行程序:
javac -d custom_classes custom_classes/ExampleClass.java
java -cp custom_classes:lib/library.jar CustomClassLoaderTest
测试类加载的代码:
public class CustomClassLoaderTest {
public static void main(String[] args) {
ExampleClass example = new ExampleClass();
example.printMessage();
}
}
运行结果:
Custom message from custom_classes
总结
- 双亲委派模型:类加载请求首先由父加载器处理,一直到 Bootstrap ClassLoader。
- 类路径顺序:在类路径中配置多个路径时,Application ClassLoader 会按照顺序查找类文件。找到匹配类文件后立即加载,不会继续查找后续路径。
- 覆盖机制:通过在类路径中优先放置自定义类路径,可以覆盖原有 jar 文件中的类。