本文共 7443 字,大约阅读时间需要 24 分钟。
如果一个类加载器
收到了要加载某个类的请求,它自己不会立即加载,而是委托给上一级的类加载器,一直委托到顶层,从顶层向下依次加载这个类,加载成功就跳出,否则一直往下,最终加载失败就会抛出ClassNotFoundException
。如果加载过,则不需要再次走这个流程。
注意:有些文章描述的父类加载器
,我觉得表述为上级类加载器
更加贴切,因为他们没有Java里的继承关系,只是人为划分的层级。
Bootstrap ClassLoader(启动类加载器,或者叫引导类加载器) |Extension ClassLoader(扩展类加载器) |Application ClassLoader(应用类加载器) |User ClassLoader(用户类加载器,或者自定义类加载器,可以有多个,比如MyClassLoader1,MyClassLoader2)注意:1、注意断句:启动.类加载器,扩展.类加载器...2、User ClassLoader是程序员自己定义的,自己写的ClassLoader代码,通过继承`java.lang.ClassLoader`抽象类3、Bootstrap ClassLoader是获取不到ClassLoader的,得到`null`,因为其是C++写的,即`String.class.getClassLoader()`返回`null`4、Extension和Application的类加载器,分别是Java类:`sun.misc.Launcher$ExtClassLoader`和`sun.misc.Launcher$AppClassLoader`Java虚拟机中可以安装多个类加载器,系统默认三个主要的类加载器,每个类负责加载特定位置的类Bootstrap ClassLoader:/jre/lib/rt.jar,写死了加载名为rt.jarExtension ClassLoader:/jre/lib/ext/*.jar,该目录所有jar,包括可以把自己的打的jar包丢在这个目录页能加载Application ClassLoader:加载classpath指定的jar包或目录User ClassLoader:加载我们指定的目录中的class加载顺序:Bootstrap ClassLoader加载是否成功(rt.jar里是否存在指定的类),成功则跳出,否则交给Extension ClassLoader加载是否成功,成功则跳出,否则交给Application ClassLoader加载是否成功,成功则跳出,否则交给User ClassLoader,加载是否成功,成功则跳出,否则抛出ClassNotFoundException
例如要加载java.lang.String,首先让Bootstrap ClassLoader来加载,找到了,加载成功,跳出;如果要加载com.some.MyTest,则Bootstrap加载失败,ExtClassLoader失败,APPClassLoader成功;如果要加载com.wyf.test.Tool,假设这个类打成jar包并丢到`E:\DevFolder\jdk\jdk1.8.0_152\jre\lib\ext`里,则Bootstrap加载失败,ExtClassLoader加载成功,跳出
安全
比如自己写的java.lang.String类,不能覆盖Java核心的那个,保证了程序运行的稳定性。PS:即使自己定义了类加载器并强行用defineClas()加载java.lang开头的类,也不可能成功,会抛出java.lang.SecurityException:Prohibited package name:java.lang
避免重复加载的混乱
已经加载过的类不会再次被加载,避免了重复加载的混乱。方便管理。
要自定义自己的类加载器,必须继承抽象类
java.lang.ClassLoader
,并覆盖findClass(String name)
方法。
通常生成的.class文件很容易被反编译,我们可以利用生成的.class文件(未加密)读成字节流,然后堆字节流做一些转换,然后再写成.class文件,name这个.class文件就是加密的了,无法反编译。但是加密后的文件无法被默认的类加载器加载,就需要自己定义类加载器,在findClass()方法中得到字节流后进行还原
ClassLoader类:
package com.test;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;public class MyClassLoader2 extends ClassLoader { /** * 整体思路是传入的name是class文件的位置,例如E:/TestEntity.class, 然后得到文件的字节流后传入`defineClass(byte[] b, int off, int len)`得到Class * * 注意这里的name字段传的是class文件的位置,实际上跟父类的方法的name的含义是不一样了 */ @Override protected Class findClass(String name) throws ClassNotFoundException { String classFileFullpath = name; FileInputStream fis = null; byte[] buf = null; try { fis = new FileInputStream(new File(classFileFullpath)); int available = fis.available(); buf = new byte[available]; fis.read(buf);// 将文件全部读入buf中 } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } if (buf == null) { return null; } return defineClass(buf, 0, buf.length); }}
实体类
package com.test;public class TestEntity { public String sayGoodBye() { String msg = "sayonara"; System.out.println("say:" + msg); return msg; }}
测试类
package com.test;import java.lang.reflect.Method;public class Test2 { public static void main(String[] args) { String classFileLocation = "E:/TestEntity.class";// class文件位置 MyClassLoader2 cl = new MyClassLoader2(); try { Class clazz = cl.loadClass(classFileLocation);// 网上有些例子直接调用cl.findClass,是不对的,这样就是直接调用了,不会有loadClass委托的逻辑在里面。另外注意这里传qualifiedName是不对的,会被APPClassLoader加载 Object obj = clazz.newInstance(); Method method = clazz.getMethod("sayGoodBye"); Object ret = method.invoke(obj, null); System.out.println("method_ret:" + ret); // 查看对象是哪个ClasLoader加载的 System.out.println("ClassLoader:" + TestEntity.class.getClassLoader());// 不能用这个查看,这种方式使用的是AppClassLoader,因为如果不显式指定自己的ClassLoader,就会非自定义的那套(AppClassLoader->Ext->Bootstrap System.out.println("ClassLoader2:" + obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } }}
将编译出来的TestEntity.class
放到E:/
下即可。这个类加载器使用了过时的方法defineClass(byte[] b, int off, int len)
(@Deprecated)
思考(很重要) 这里的测试类用了loadClass(),传入了E:/TestEntity.class
的值,这里是不能传入com.test.TestEntity
的,否则就轮不到自定义的加载器了,因为其上级AppClassLoader,会通过入参com.test.TestEntity
在类路径下找到E:\DevFolder\workspaces\sts_workspace\test-classloader\target\classes\com\test\TestEntity.class
,然后加载这个类
补充
这里有点不明白替代过时方法的defineClass(String name, byte[] b, int off, int len)
的 name
字段的含义,传name的意义是什么? 似乎是通过该name获取到要加载的class文件的位置,通过类路径, com/test/TestEntity
错,不能用/com.test.TestEntity2
错,类名错误,NoClassDefFoundError
异常com.test2.TestEntity
错,包名错误,NoClassDefFoundError
异常com.test.TestEntity.class
错,不能带.class扩展名,native方法抛出异常""
,错,虽然看源码检查这个name这里是可以的,可能是native方法抛出异常了defineClass(byte[] b, int off, int len)
com.test.TestEntity
,正确java.lang.Object
吗?可以定义java.lang.Object
,定义如下
package java.lang;public class Object { public void call() { System.out.println("my own java.lang.Object"); }}
测试代码
package com.test;public class Test { public static void main(String[] args) { java.lang.Object o = new java.lang.Object(); o.call(); }}
运行的时候报错,抛出了异常
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.Object.call()V at com.test.Test.main(Test.java:7)
原因:
虽然定义了自己的Object类,包名也是跟rt.jar
里的相同,但是加载的时候不会加载到自己定义的类,因为顶层Bootstrap ClassLoader在rt.jar里找到了这个全限定名的类:java.lang.Object
,于是就用了rt.jar里的,这样的双亲委派模型保证了安全,不会被别人写的相同包名和类名所覆盖。这里也验证了一个问题,Eclipse编译是通过的,因为IDE是按照本地的java.lang.Object进行编译的,所以可以调用call() java.lang.String
吗?代码:
package java.lang;public class String { public void invoke() { System.out.println("my own java.lang.String"); }}// 测试代码package com.test;public class Test { public static void main(String[] args) { java.lang.String s = new java.lang.String(); s.invoke(); }}
一样的结论,抛出
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.String.invoke()V at com.test.Test.main(Test.java:6)
自己写的java.lang.String
依然无法覆盖rt.jar
里头的。这里注意到一个细节,自己写的java.lang.String
继承了自己写的java.lang.Object
,也就是自动获得了call()方法
假设如下代码打包成my-lib.jar并放到E:\DevFolder\jdk\jdk1.8.0_152\jre\lib\ext
中,即有com.wyf.test.Tool类
,有方法sayHelloInJanpanese()
package com.wyf.test;public class Tool { public String sayHelloInJanpanese() { return "konichiwa"; }}
在项目中定义同包同名类,如下
package com.wyf.test;public class Tool { public String sayHelloInJanpanese() { return "my own com.wyf.test.Tool"; }}
测试代码是
package com.test;public class TestExtJar { public static void main(String[] args) { com.wyf.test.Tool a = new com.wyf.test.Tool(); String hello = a.sayHelloInJanpanese(); System.out.println(hello); }}
问题是会打印出什么? 答案是打印出konichiwa
,因为加载的是jre/lib/ext下的类!
这里有一个细节,假设本地的com.wyf.test.Tool
的方法名和jre/lib/ext/my-lib.jar
中不同,如下
package com.wyf.test;public class Tool { public String sayHello() { return "my own com.wyf.test.Tool"; }}
则TestExtJar.java
编译不过,因为它认了本地的Tool类,必须改成
package com.test;public class TestExtJar { public static void main(String[] args) { com.wyf.test.Tool a = new com.wyf.test.Tool(); String hello = a.sayHello(); System.out.println(hello); }}
改成这样后,编译是没问题,运行的时候抛出
Exception in thread "main" java.lang.NoSuchMethodError: com.wyf.test.Tool.sayHello()Ljava/lang/String; at com.test.TestExtJar.main(TestExtJar.java:6)
问题跟前面的一样,因为加载的是jre/lib/ext/my-lib.jar
中的com.wyf.test.Tool
而不是本地的Tool,所以运行时找不到sayHello()
方法。
转载地址:http://lkdzz.baihongyu.com/