- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章两种实现Java类隔离加载的方法由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
阿里妹导读:java 开发中,如果不同的 jar 包依赖了某些通用 jar 包的版本不一样,运行时就会因为加载的类跟预期不符合导致报错。如何避免这种情况呢?本文通过分析 jar 包产生冲突的原因及类隔离的实现原理,分享两种实现自定义类加载器的方法.
。
只要你 java 代码写的足够多,就一定会出现这种情况:系统新引入了一个中间件的 jar 包,编译的时候一切正常,一运行就报错:java.lang.nosuchmethoderror,然后就哼哧哼哧的开始找解决方法,最后在几百个依赖包里面找的眼睛都快瞎了才找到冲突的 jar,把问题解决之后就开始吐槽中间件为啥搞那么多不同版本的 jar,写代码五分钟,排包排了一整天.
上面这种情况就是 java 开发过程中常见的情况,原因也很简单,不同 jar 包依赖了某些通用 jar 包(如日志组件)的版本不一样,编译的时候没问题,到了运行时就会因为加载的类跟预期不符合导致报错。举个例子:a 和 b 分别依赖了 c 的 v1 和 v2 版本,v2 版本的 log 类比 v1 版本新增了 error 方法,现在工程里面同时引入了 a、b 两个 jar 包,以及 c 的 v0.1、v0.2 版本,打包的时候 maven 只能选择一个 c 的版本,假设选择了 v1 版本。到了运行的时候,默认情况下一个项目的所有类都是用同一个类加载器加载的,所以不管你依赖了多少个版本的 c,最终只会有一个版本的 c 被加载到 jvm 中。当 b 要去访问 log.error,就会发现 log 压根就没有 error 方法,然后就抛异常java.lang.nosuchmethoderror。这就是类冲突的一个典型案例.
类冲突的问题如果版本是向下兼容的其实很好解决,把低版本的排除掉就完事了。但要是遇到版本不向下兼容的那就陷入了“救妈妈还是救女朋友”的两难处境了.
为了避免两难选择,有人就提出了类隔离技术来解决类冲突的问题。类隔离的原理也很简单,就是让每个模块使用独立的类加载器来加载,这样不同模块之间的依赖就不会互相影响。如下图所示,不同的模块用不同的类加载器加载。为什么这样做就能解决类冲突呢?这里用到了 java 的一个机制:不同类加载器加载的类在 jvm 看来是两个不同的类,因为在 jvm 中一个类的唯一标识是 类加载器+类名。通过这种方式我们就能够同时加载 c 的两个不同版本的类,即使它类名是一样的。注意,这里类加载器指的是类加载器的实例,并不是一定要定义两个不同类加载器,例如图中的 pluginclassloadera 和 pluginclassloaderb 可以是同一个类加载器的不同实例.
。
前面我们提到类隔离就是让不同模块的 jar 包用不同的类加载器加载,要做到这一点,就需要让 jvm 能够使用自定义的类加载器加载我们写的类以及其关联的类.
那么如何实现呢?一个很简单的做法就是 jvm 提供一个全局类加载器的设置接口,这样我们直接替换全局类加载器就行了,但是这样无法解决多个自定义类加载器同时存在的问题.
实际上 jvm 提供了一种非常简单有效的方式,我把它称为类加载传导规则:jvm 会选择当前类的类加载器来加载所有该类的引用的类。例如我们定义了 testa 和 testb 两个类,testa 会引用 testb,只要我们使用自定义的类加载器加载 testa,那么在运行时,当 testa 调用到 testb 的时候,testb 也会被 jvm 使用 testa 的类加载器加载。依此类推,只要是 testa 及其引用类关联的所有 jar 包的类都会被自定义类加载器加载。通过这种方式,我们只要让模块的 main 方法类使用不同的类加载器加载,那么每个模块的都会使用 main 方法类的类加载器加载的,这样就能让多个模块分别使用不同类加载器。这也是 osgi 和 sofaark 能够实现类隔离的核心原理.
了解了类隔离的实现原理之后,我们从重写类加载器开始进行实操。要实现自己的类加载器,首先让自定义的类加载器继承 java.lang.classloader,然后重写类加载的方法,这里我们有两个选择,一个是重写 findclass(string name),一个是重写 loadclass(string name)。那么到底应该选择哪个?这两者有什么区别? 下面我们分别尝试重写这两个方法来实现自定义类加载器.
1.重写 findclass 。
首先我们定义两个类,testa 会打印自己的类加载器,然后调用 testb 打印它的类加载器,我们预期是实现重写了 findclass 方法的类加载器 myclassloaderparentfirst 能够在加载了 testa 之后,让 testb 也自动由 myclassloaderparentfirst 来进行加载.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
class
testa {
public
static
void
main(string[] args) {
testa testa =
new
testa();
testa.hello();
}
public
void
hello() {
// https://jinglingwang.cn/archives/class-isolation-loading
system.out.println(
"testa: "
+
this
.getclass().getclassloader());
testb testb =
new
testb();
testb.hello();
}
}
public
class
testb {
public
void
hello() {
system.out.println(
"testb: "
+
this
.getclass().getclassloader());
}
}
|
然后重写一下 findclass 方法,这个方法先根据文件路径加载 class 文件,然后调用 defineclass 获取 class 对象.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public
class
myclassloaderparentfirst
extends
classloader{
private
map<string, string> classpathmap =
new
hashmap<>();
public
myclassloaderparentfirst() {
classpathmap.put(
"com.java.loader.testa"
,
"/users/hansong/ideaprojects/ohmyjava/coderepository/target/classes/com/java/loader/testa.class"
);
classpathmap.put(
"com.java.loader.testb"
,
"/users/hansong/ideaprojects/ohmyjava/coderepository/target/classes/com/java/loader/testb.class"
);
}
// 重写了 findclass 方法 by:jinglingwang.cn
@override
public
class
<?> findclass(string name)
throws
classnotfoundexception {
string classpath = classpathmap.get(name);
file file =
new
file(classpath);
if
(!file.exists()) {
throw
new
classnotfoundexception();
}
byte
[] classbytes = getclassdata(file);
if
(classbytes ==
null
|| classbytes.length ==
0
) {
throw
new
classnotfoundexception();
}
return
defineclass(classbytes,
0
, classbytes.length);
}
private
byte
[] getclassdata(file file) {
try
(inputstream ins =
new
fileinputstream(file); bytearrayoutputstream baos =
new
bytearrayoutputstream()) {
byte
[] buffer =
new
byte
[
4096
];
int
bytesnumread =
0
;
while
((bytesnumread = ins.read(buffer)) != -
1
) {
baos.write(buffer,
0
, bytesnumread);
}
return
baos.tobytearray();
}
catch
(filenotfoundexception e) {
e.printstacktrace();
}
catch
(ioexception e) {
e.printstacktrace();
}
return
new
byte
[] {};
}
}
|
最后写一个 main 方法调用自定义的类加载器加载 testa,然后通过反射调用 testa 的 main 方法打印类加载器的信息.
1
2
3
4
5
6
7
8
|
public
class
mytest {
public
static
void
main(string[] args)
throws
exception {
myclassloaderparentfirst myclassloaderparentfirst =
new
myclassloaderparentfirst();
class
testaclass = myclassloaderparentfirst.findclass(
"com.java.loader.testa"
);
method mainmethod = testaclass.getdeclaredmethod(
"main"
, string[].
class
);
mainmethod.invoke(
null
,
new
object[]{args});
}
|
执行的结果如下:
1
2
|
testa: com.java.loader.myclassloaderparentfirst
@1d44bcfa
testb: sun.misc.launcher$appclassloader
@18b4aac2
|
执行的结果并没有如我们期待,testa 确实是 myclassloaderparentfirst 加载的,但是 testb 还是 appclassloader 加载的。这是为什么呢?
要回答这个问题,首先是要了解一个类加载的规则:jvm 在触发类加载时调用的是 classloader.loadclass 方法。这个方法的实现了双亲委派:
明白了这个规则之后,执行的结果的原因就找到了:jvm 确实使用了myclassloaderparentfirst 来加载 testb,但是因为双亲委派的机制,testb 被委托给了 myclassloaderparentfirst 的父加载器 appclassloader 进行加载.
你可能还好奇,为什么 myclassloaderparentfirst 的父加载器是 appclassloader?因为我们定义的 main 方法类默认情况下都是由 jdk 自带的 appclassloader 加载的,根据类加载传导规则,main 类引用的 myclassloaderparentfirst 也是由加载了 main 类的appclassloader 来加载。由于 myclassloaderparentfirst 的父类是 classloader,classloader 的默认构造方法会自动设置父加载器的值为 appclassloader.
1
2
3
|
protected
classloader() {
this
(checkcreateclassloader(), getsystemclassloader());
}
|
2.重写 loadclass 。
由于重写 findclass 方法会受到双亲委派机制的影响导致 testb 被 appclassloader 加载,不符合类隔离的目标,所以我们只能重写 loadclass 方法来破坏双亲委派机制。代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public
class
myclassloadercustom
extends
classloader {
private
classloader jdkclassloader;
private
map<string, string> classpathmap =
new
hashmap<>();
public
myclassloadercustom(classloader jdkclassloader) {
this
.jdkclassloader = jdkclassloader;
classpathmap.put(
"com.java.loader.testa"
,
"/users/hansong/ideaprojects/ohmyjava/coderepository/target/classes/com/java/loader/testa.class"
);
classpathmap.put(
"com.java.loader.testb"
,
"/users/hansong/ideaprojects/ohmyjava/coderepository/target/classes/com/java/loader/testb.class"
);
}
@override
protected
class
<?> loadclass(string name,
boolean
resolve)
throws
classnotfoundexception {
class
result =
null
;
try
{
//by:jinglingwang.cn 这里要使用 jdk 的类加载器加载 java.lang 包里面的类
result = jdkclassloader.loadclass(name);
}
catch
(exception e) {
//忽略 by:jinglingwang.cn
}
if
(result !=
null
) {
return
result;
}
string classpath = classpathmap.get(name);
file file =
new
file(classpath);
if
(!file.exists()) {
throw
new
classnotfoundexception();
}
byte
[] classbytes = getclassdata(file);
if
(classbytes ==
null
|| classbytes.length ==
0
) {
throw
new
classnotfoundexception();
}
return
defineclass(classbytes,
0
, classbytes.length);
}
private
byte
[] getclassdata(file file) {
//省略 }
}
|
这里注意一点,我们重写了 loadclass 方法也就是意味着所有类包括 java.lang 包里面的类都会通过 myclassloadercustom 进行加载,但类隔离的目标不包括这部分 jdk 自带的类,所以我们用 extclassloader 来加载 jdk 的类,相关的代码就是:result = jdkclassloader.loadclass(name),
测试代码如下:
1
2
3
4
5
6
7
8
9
10
|
public
class
mytest {
public
static
void
main(string[] args)
throws
exception {
//这里取appclassloader的父加载器也就是extclassloader作为myclassloadercustom的jdkclassloader
myclassloadercustom myclassloadercustom =
new
myclassloadercustom(thread.currentthread().getcontextclassloader().getparent());
class
testaclass = myclassloadercustom.loadclass(
"com.java.loader.testa"
);
method mainmethod = testaclass.getdeclaredmethod(
"main"
, string[].
class
);
mainmethod.invoke(
null
,
new
object[]{args});
}
}
|
执行结果如下:
1
2
|
testa: com.java.loader.myclassloadercustom
@1d44bcfa
testb: com.java.loader.myclassloadercustom
@1d44bcfa
|
可以看到,通过重写了 loadclass 方法,我们成功的让 testb 也使用myclassloadercustom 加载到了 jvm 中.
。
类隔离技术是为了解决依赖冲突而诞生的,它通过自定义类加载器破坏双亲委派机制,然后利用类加载传导规则实现了不同模块的类隔离.
以上就是两种实现java类隔离加载的方法的详细内容,更多关于java类隔离加载的资料请关注我其它相关文章! 。
最后此篇关于两种实现Java类隔离加载的方法的文章就讲到这里了,如果你想了解更多关于两种实现Java类隔离加载的方法的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我的公司有一个 Web 应用程序,其中包含纯 JavaScript,它以自己的方式使用 $ -“美元符号”,如下所示: function $(e) { return document.getE
doc说“这意味着对单个节点上单个分区内的行的写入仅对执行操作的客户端可见”。 如果有另一个 client2 在同一个分区和同一个节点上执行操作,那么文档中提到的“THE CLIENT”执行的写入是否
只是一个想法,但在 DIV 上使用 IFRAME 本质上会使该元素与窗口隔离,从而降低 IFRAME 中运行的脚本速度 不会影响其他框架/窗口吗? 最佳答案 是的,对于第一部分,iframe 会“某种
我有以下模型 Inventory [product_name, quantity, reserved_quantity] 有数据 [Shirt, 1, 0] [Shorts, 10, 0] 如果以下代
我面临的情况如下。因为ThreadPool是每个进程1个实例,所以我的问题是是否会在 3秒后取消方法2排队的任务? http request comes in *method 1 gets execu
我希望在 Dart 中创建一个 Isolate,我可以通过编程方式暂停和恢复。这是我使用的代码。 import 'dart:io'; import 'dart:isolate'; void main(
我想编写一个具有隔离作用域的指令,但也希望使该作用域可用于父作用域的 Controller 。我找到了这个解决方案: app.directive('popupbutton', [functi
我有一个像这样的 JSON 对象: [ {"Subject": "Physics", "Active": 48, "Date": "2020-01-22T00:00:00Z"}, {"Su
我正在使用 Elixir 自动执行用 Gherkin 编写的规范中定义的验收测试。一种方法是使用名为 Cabbage 的 ExUnit 插件。 . 现在,ExUnit 似乎提供了一个在任何单个测试之前
我被要求为多个用户配置一个带有 docker 的 ubuntu 18.04 服务器。 目的: 我们有多个编写测试用例的测试人员。但是我们的笔记本电脑速度不够快,无法在 docker 环境中构建项目和运
我一直在网上寻找完整的解决方案,但到目前为止,我只能找到不合适的部分。 我正在寻找一个可以查看图像文件、循环遍历文件并隔离 Sprite 然后保存它们的程序。之后,我需要一种方法来重新访问该 Spri
我想知道如何隔离 JavaScript 函数的执行以避免浏览器崩溃。 示例:如果我想在控制台中输出一个包含大约 10k 元素的关联数组,浏览器将停止响应。我怎样才能避免这种情况? 最佳答案 解决方案是
我必须向我的数据库添加大量信息。添加此信息大约需要 5-7 分钟。我需要添加交易。 我试过这个: try { db.Connection.Open(); db.Transaction
我有 6 个 iframe,它们来自同一个域,但具有不同的 url 和子目录。他们都使用 html header “set-cookie”设置了一个名称相同但值不同的 cookie。我需要将它们的 c
我正在编写一个代码,它基本上读取一个文本文件(表格格式)并检查该文件是否包含预期的数据类型。为此我写了下面的课。 示例文件应该是这样的。 name age abc 20 xyz
我有一个表,线程。我有一个表,thread_participants。我正在尝试隔离与特定 thread_id 和特定 thread_participants.user_id 标识的行。 例如,如果
我有一个非常实际的问题。我的数据库中有大约 400 篇文章,在这些文章中我有其他文章的链接。在转换过程中,链接被破坏。我们在 CMS 中手动插入新的菜单链接项。我想制作一个脚本来查找(文章)id 并将
关闭。这个问题是off-topic .它目前不接受答案。 想改进这个问题吗? Update the question所以它是on-topic用于堆栈溢出。 关闭 10 年前。 Improve thi
我在想是否可以在 postgres 数据库(高于 8.3 的版本)上创建一个只能访问特定指定模式的用户。问题是,在我的数据库中我有一些模式。如果我撤销某个用户对除一个模式之外的所有模式的所有特权,他仍
我有两组点,一组来自分析,另一组用于分析数据的后处理结果。 黑色的分析数据是散乱的。 用于结果的点是红色的。 这是同一地 block 上的两组: 我遇到的问题是:我将插值到红点上,但如您所见,有些红点
我是一名优秀的程序员,十分优秀!