当前位置: 首页 > 科技观察

Java类隔离加载如何实现?

时间:2023-03-16 19:17:18 科技观察

在Java开发中,如果不同的jar包依赖了一些普通jar包的不同版本,运行时会因为加载的类不符合预期而报错。如何避免这种情况?本文通过分析jar包冲突的原因和类隔离的实现原理,分享两种实现自定义类加载器的方法。1、什么是类隔离技术?只要你写的Java代码够多,肯定会出现这种情况:系统新引入了一个中间件jar包,编译时一切正常,但运行时会报错:java.lang.NoSuchMethodError,然后开始寻找解决方案,在查找了数百个依赖包后终于找到了冲突的jar。解决问题后开始抱怨为什么中间件jar版本那么多,五分钟写代码,一整天整理包。上述情况是Java开发过程中常见的情况,原因也很简单。不同的jar包依赖于一些常用的jar包(比如日志组件)的不同版本。加载的类不符合预期,导致报错。例如:A和B分别依赖于C的v1和v2版本。与v1版本相比,v2版本的Log类比增加了报错方法。现在项目引入了A和B两个jar包,还有C的v0.1,v0.2版本,maven打包的时候只能选择一个C版本,假设选择的是v1版本。到了运行的时候,默认情况下一个项目的所有类都是用同一个类加载器加载的,所以不管你依赖了多少个版本的C,最终JVM只会加载一个版本的C。当B要访问Log.error时,会发现Log根本没有error方法,然后抛出异常java.lang.NoSuchMethodError。这是典型的阶级矛盾。如果版本向后兼容,类冲突的问题其实很容易解决,排除低版本就可以了。但如果遇到不向下兼容的版本,就会陷入“救妈妈还是救女朋友”的两难境地。为了避免这种困境,有人提出类隔离技术来解决类冲突问题。类隔离的原理也很简单,就是让每个模块加载一个独立的类加载器,这样不同模块之间的依赖就不会互相影响。如下图所示,不同的模块加载不同的类加载器。为什么这样做可以解决阶级矛盾呢?这里使用了Java的一种机制:由不同的类加载器加载的类被JVM看成是两个不同的类,因为一个类在JVM中的唯一标识是类加载器+类名。这样我们就可以同时加载两个不同版本的C类,即使它们的类名相同。注意这里的类加载器指的是类加载器的实例,并不需要定义两个不同的类加载器。比如图中的PluginClassLoaderA和PluginClassLoaderB可以是同一个类加载器的不同实例。2如何实现类隔离前面我们提到类隔离就是用不同的类加载器加载不同模块的jar包。为此,我们需要允许JVM使用自定义类加载器来加载我们编写的类及其关联类。那么如何实现呢?一个很简单的方式就是JVM提供了一个全局类加载器设置接口,这样我们就可以直接替换全局类加载器,但是这并不能解决同时多个自定义类加载器的问题。其实JVM提供了一种非常简单有效的方式,我称之为类加载传导规则:JVM会选择当前类的类加载器来加载该类引用的所有类。例如,如果我们定义了两个类,TestA和TestB,那么TestA将引用TestB。只要我们使用自定义的类加载器来加载TestA,那么在运行时,当TestA调用TestB时,TestB也会被JVM使用。加载程序加载。以此类推,只要是TestA关联的所有jar包的类及其引用的类,都会被自定义类加载器加载。这样,我们只需要用不同的类加载器加载模块的main方法类,那么每个模块都会加载main方法类的类加载器,这样多个模块就可以加载不同的类设备了。这也是OSGi和SofaArk能够实现类隔离的核心原理。了解了类隔离的实现原理后,我们开始通过重写类加载器来实践。要实现自己的类加载器,首先让自定义的类加载器继承java.lang.ClassLoader,然后重写类加载的方法。这里我们有两种选择,一种是重写findClass(Stringname),另一种是OverrideloadClass(Stringname)。那么你应该选择哪一个?两者有什么区别?下面我们尝试重写这两个方法来实现一个自定义的类加载器。1重写findClass首先,我们定义两个类。TestA会打印自己的类加载器,然后调用TestB打印自己的类加载器。我们期望重写了findClass方法的类加载器MyClassLoaderParentFirst能够在TestA之后加载,这样TestB也由MyClassLoaderParentFirst自动加载。publicclassTestA{publicstaticvoidmain(String[]args){TestAtestA=newTestA();testA.hello();}publicvoidhello(){System.out.println("TestA:"+this.getClass().getClassLoader());TestBtestB=newTestB();testB.hello();}}publicclassTestB{publicvoidhello(){System.out.println("TestB:"+this.getClass().getClassLoader());}}然后重写findClass方法,这样方法首先根据文件路径加载class文件,然后调用defineClass获取Class对象。publicclassMyClassLoaderParentFirstendsClassLoader{privateMapclassPathMap=newHashMap<>();publicMyClassLoaderParentFirst(){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方法@OverridepublicClassfindClass(Stringname)throwsClassNotFoundException{StringclassPath=classPathMap.get(name);Filefile=newFile(classPath);if(!file.exists()){thrownewClassNotFoundException();}byte[]classBytes=getClassData(file);if(classBytes==null||classBytes.length==0){thrownewClassNotFoundException();}returndefineClass(classBytes,0,classBytes.length);}privatebyte[]getClassData(Filefile){try(InputStreamins=newFileInputStream(file);ByteArrayOutputStreambaos=newByteArrayOutputStream()){byte[]buffer=newbyte[4096];intbytesNumRead=0;while((bytesNumRead=ins.read(buffer))!=-1){baos.write(buffer,0,bytesNumRead);}returnbaos.toByteArray();}catch(FileNotFoundExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}returnnewbyte[]{};}}最后写一个main方法调用自定义类加载器LoadTestA,然后调用TestA的main方法打印类加载器信息MethodmainMethod=testAClass.getDeclaredMethod("main",String[].class);mainMethod.invoke(null,newObject[]{args});}执行结果如下:TestA:com.java.loader.MyClassLoaderParentFirst@1d44bcfaTestB:sun.misc.Launcher$AppClassLoader@18b4aac2的执行结果不符合预期,TestA确实是被MyClassLoaderParent加载的首先,但TestB仍然由AppClassLoader加载。为什么是这样?要回答这个问题,首先要了解一个类加载规则:JVM在触发类加载时,会调用ClassLoader.loadClass方法。该方法实现了父委托:委托给父加载器进行查询。如果父加载器找不到,调用findClass方法加载。理解了这条规则后,发现执行结果的原因是:JVM确实使用MyClassLoaderParentFirst来加载TestB,但是由于双亲委派机制,TestB被委托给MyClassLoaderParentFirst的父加载器AppClassLoader来加载。你可能还会疑惑为什么MyClassLoaderParentFirst的父加载器是AppClassLoader?因为我们定义的main方法类默认是由JDK自带的AppClassLoader加载的。根据类加载传导规则,主类引用的MyClassLoaderParentFirst也被主类的AppClassLoader加载。由于MyClassLoaderParentFirst的父类是ClassLoader,所以ClassLoader的默认构造函数会自动将父加载器的值设置为AppClassLoader。protectedClassLoader(){this(checkCreateClassLoader(),getSystemClassLoader());}2重写loadClass由于重写findClass方法会受到双亲委托机制的影响,TestB将由AppClassLoader加载,不符合类隔离的目标,所以我们只能重写loadClass方法来打破双亲委派机制。代码如下所示:publicclassMyClassLoaderCustomextendsClassLoader{privateClassLoaderjdkClassLoader;privateMapclassPathMap=newHashMap<>();publicMyClassLoaderCustom(ClassLoaderjdkClassLoader){this.jdkClassLoader=jdkClassLoader;classPathMap.put("com.java.,"loader"/.TestAUsers/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");}@OverrideprotectedClassloadClass(Stringname,booleanresolve)throwsClassNotFoundException{Classresult=null;try{//这里要使用JDK的类加载器加载java.lang包里的类result=jdkClassLoader.loadClass(name);}catch(Exceptione){//忽略}if(result!=null){returnresult;}StringclassPath=classPathMap.get(name);Filefile=newFile(classPath);if(!file.exists()){thrownewClassNotFoundException();}byte[]classBytes=getClassData(file);我f(classBytes==null||classBytes.length==0){thrownewClassNotFoundException();}returndefineClass(classBytes,0,classBytes.length);}privatebyte[]getClassData(Filefile){//省略}}这里注意,我们重写了loadClass方法,也就是说所有的类包括java.lang包中的类都会通过MyClassLoaderCustom加载,但是类隔离的目标是不包括这些JDK自带的类,所以我们使用ExtClassLoader来加载JDK类,相关代码是:result=jdkClassLoader.loadClass(name);测试代码如下:publicclassMyTest{publicstaticvoidmain(String[]args)throwsException{//这里取的是AppClassLoader的父加载器,即ExtClassLoaderasjdkClassLoaderMyClassLoaderCustommyClassLoaderCustom=newMyClassLoaderCustom(readThread.currentThread().getContextClassLoader().getParent());ClasstestAClass=myClassLoaderCustom.loadClass("com.java.loader.TestA");MethodmainMethod=testAClass.getDeclaredMethod("main",String[].class);mainMethod。invoke(null,newObject[]{args});}}执行结果如下:TestA:com.java.loader.MyClassLoaderCustom@1d44bcfaTestB:com.java.loader.MyClassLoaderCustom@1d44bcfa改写loadClass可以看到在s方法中,我们使用MyClassLoaderCustom成功地将TestB加载到JVM中。3.总结类隔离技术是为解决依赖冲突而诞生的。它通过自定义的类加载器破坏了双亲委托机制,然后利用类加载传导规则实现对模块不同类的隔离。深入了解Java类加载器的资源(https://www.ibm.com/developerworks/en/java/j-lo-classloader/index.html)