文章内容
本篇是对ClassLoader中的namespace做个直观的介绍和验证。这个知识点可以帮助解决日常工作中遇到的疑难杂症,如果你尚未认真研究过ClassLoader,但懵懂的认知让你觉得这个应该很简单,那请看下图,若看不懂则表明你可能并不了解ClassLoader中得一些关键逻辑。
![ClassLoader隔离性的基石namespace插图 ClassLoader隔离性的基石namespace插图](https://static.ntan520.com/blog-media/2022/12/6525b596891883cfc9a10467d82c67f3.png?x-oss-process=style/watermark)
一、Classloader的分治和委派机制
ClassLoader 是个抽象类,其子类 UrlClassLoader 引入 URL 资源概念,以此方式来约束自己只加载 URL 覆盖范围内的类文件;ExtClassLoader、AppClassLoader 都是 UrlClassLoader 的子类,各自分管的资源路径不同,即给定的 URL 不同则管辖范围不同,并通过委派执行类加载来保障这种分治能力,进而达到了类资源的隔离性。
![ClassLoader隔离性的基石namespace插图2 ClassLoader隔离性的基石namespace插图2](https://static.ntan520.com/blog-media/2022/12/d6c04cf6c77842b54bfaa043d8dd9343.jpg?x-oss-process=style/watermark)
上图是标准的委派机制,总结为2个方面:
- 1、父加载器能加载 父加载器来加载:
- 2、自己在加载资源之前,先让父类加载器去加载。父类再找其父类,直到BootStrapClassLoader(它没有父类加载器)。
- 3、保证了等级越高,加载的优先权越高
- 4、父加载器不加载 我就来加载(findClass);我加载不了的子加载器来加载:
- 5、若父类加载器没有加载成功,才逐级下放这个加载权。
- 6、子类加载器不能加载父类加载器能加载的类,如 java.lang.String ,即使用户自己编造一份这个类型,启动类加载器优先将 java.lang.String 加载成功后,应用类加载器就不会再加载用户自己编造的。
下图描述了 SkyWalking Agent 通过自定义的类加载器AgentClassLoader 加载插件中的类,不会对宿主应用中的类产生污染。
![ClassLoader隔离性的基石namespace插图4 ClassLoader隔离性的基石namespace插图4](https://static.ntan520.com/blog-media/2022/12/f78756bd0ab386eaea48478236399951.png?x-oss-process=style/watermark)
二、namespace是什么?
每个类加载器都对应一个namespace,汉化叫命名空间(我个人其实更喜欢汉化为名字空间),命名空间由该加载器及所有父类加载器所加载的类组成。这样的介绍很抽象,网络资料中也多是这么几句话,我会从更多的细粒度视角带您进一步了解它。
1、同一个命名空间中,类只加载一份
比如AppClassLoader加载程序中编写的类。无论加载多少次,只要是被AppClassLoader加载的,其Class信息的hashcode都是相同的。
2、子加载器可见父加载器加载的类
这个更容易举例,核心类库的类由BootStrapClassLoder 加载的,比如String;由AppClassLoader所加载的类,都能使用String对吧?
3、父加载器不可见子加载器所加载的类
SPI技术的诞生也是这个原因,什么是SPI:程序运行过程中要用到的类,通过当前类加载器的自动加载,加载不到(不在当前类加载器的类资源管辖范围),如果要使用这个类,必须指定一个能够加载这个类的加载器去加载,而怎么获取这个加载器是个问题。
程序都是在线程中执行,那么从线程的上下文中去拿最合理,所以就诞生了线程上下文类加载器,这中场景下加载器就得采用非自动加载,即通过 forName 或者 loadClass 的方式去加载类。
下边通过示例+科技来验证加载不到的情况:
示例中有Parent类,Son类。Parent类中有个getSon方法中,会使用到Son类。 编译后,在classpath下,把Son类移到自定义子加载器的资源目录中,让AppClassLoader无法加载。
达到我们的运行环境要求:
- 1. Parent由AppClassLoader加载
- 2. Son由自定义的子加载器加载.
- 3. 调用Parent的getSon方法的时候要new Son(),这样会触发Son类的加载,但是加载不到Son类,报错信息为:java.lang.NoClassDefFoundError: com/rock/Son
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | public class Parent { private Object son; public Object getSon(){ return new Son(); } public Object setSon(Object son){ this .son = son; return this .son; } } public class Son { } |
01 02 03 04 05 06 07 08 09 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 | /** * 父加载器加载不了,子加载器所加载的类, * 父加载器加载Parent * 子加载器加载son * Parent#getSon 方法里 new Son()对象.//报错,找不到Son的类定义. */ @Test public void testParentCanntFindSon(){ CustomClassLoader01 customClassLoader01 = new CustomClassLoader01(ClassLoader.getSystemClassLoader()); try { Class<?> classParent = customClassLoader01.loadClass( "com.rock.Parent" ); System.out.println( "classParent:" + classParent.getClassLoader()); Class<?> classSon = customClassLoader01.loadClass( "com.rock.Son" ); System.out.println( "classSon:" + classSon.getClassLoader()); Object objParent = classParent.newInstance(); Object objSon = classSon.newInstance(); Method setSon = classParent.getMethod( "setSon" ,Object. class ); Object resultSon = setSon.invoke(objParent, objSon); System.out.println(resultSon.getClass()); System.out.println(resultSon.getClass().getClassLoader()); try { Method getSon = classParent.getMethod( "getSon" ); Object invoke = getSon.invoke(objParent); System.out.println(invoke.getClass().getClassLoader()); } catch (Exception exp){ //java.lang.NoClassDefFoundError: com/rock/Son exp.printStackTrace(); } } catch (Exception exp){ exp.printStackTrace(); } } |
4、不同命名空间的类互相不可见
这个特征也通过实例来验证一下,自定义类加载器 new 出来两个实例,每个实例各自加载一次Man类,通过newInstance方式实例化出两个对象,对象之间互相调用setFather赋值就会报错。即a空间的不识别b空间的类,即使全限定名相同也不识别。这种特性所导致的坑你踩到过嘛?
1 2 3 4 5 6 | public class Man { private Man father; public void setFather(Object obj){ father = (Man)obj; } } |
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | @Test public void testSifferentNamespaceClass(){ CustomClassLoader01 customClassLoader01 = new CustomClassLoader01(ClassLoader.getSystemClassLoader()); CustomClassLoader01 customClassLoader02 = new CustomClassLoader01(ClassLoader.getSystemClassLoader()); try { Class<?> aClass1 = customClassLoader01.loadClass( "com.rock.Man" ); System.out.println( "class1 : " + aClass1.getClassLoader()); System.out.println( "class1 : " + aClass1.); Class<?> aClass2 = customClassLoader02.loadClass( "com.rock.Man" ); System.out.println( "class2 : " + aClass1.getClassLoader()); System.out.println( "class2 : " + aClass2); Object man1 = aClass1.newInstance(); Object man2 = aClass2.newInstance(); Method setFather = aClass1.getMethod( "setFather" , Object. class ); setFather.invoke(man1,man2); } catch (Exception exp){ exp.printStackTrace(); } } |
1 2 3 4 5 6 7 8 | class1 : com.rock.classLoader.CustomClassLoader01@1f28c152 class1 : 2006034581 class2 : com.rock.classLoader.CustomClassLoader01@1f28c152 class2 : 488044861 ... Caused by: java.lang.ClassCastException: com.rock.Man cannot be cast to com.rock.Man at com.rock.Man.setFather(Man.java:6) ... 27 more |