gpt4 book ai didi

两种实现Java类隔离加载的方法

转载 作者:qq735679552 更新时间:2022-09-29 22:32:09 25 4
gpt4 key购买 nike

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类隔离加载的方法

类冲突的问题如果版本是向下兼容的其实很好解决,把低版本的排除掉就完事了。但要是遇到版本不向下兼容的那就陷入了“救妈妈还是救女朋友”的两难处境了.

为了避免两难选择,有人就提出了类隔离技术来解决类冲突的问题。类隔离的原理也很简单,就是让每个模块使用独立的类加载器来加载,这样不同模块之间的依赖就不会互相影响。如下图所示,不同的模块用不同的类加载器加载。为什么这样做就能解决类冲突呢?这里用到了 java 的一个机制:不同类加载器加载的类在 jvm 看来是两个不同的类,因为在 jvm 中一个类的唯一标识是 类加载器+类名。通过这种方式我们就能够同时加载 c 的两个不同版本的类,即使它类名是一样的。注意,这里类加载器指的是类加载器的实例,并不是一定要定义两个不同类加载器,例如图中的 pluginclassloadera 和 pluginclassloaderb 可以是同一个类加载器的不同实例.

两种实现Java类隔离加载的方法

二  如何实现类隔离

  。

前面我们提到类隔离就是让不同模块的 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 方法。这个方法的实现了双亲委派:

  • 委托给父加载器查询
  • 如果父加载器查询不到,就调用 findclass 方法进行加载

明白了这个规则之后,执行的结果的原因就找到了: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的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com