博客
关于我
ClassLoader、双亲委派机制、自定义类加载器实践
阅读量:395 次
发布时间:2019-03-05

本文共 7413 字,大约阅读时间需要 24 分钟。

ClassLoader、双亲委派机制、自定义类加载器

双亲委派模型

如果一个类加载器收到了要加载某个类的请求,它自己不会立即加载,而是委托给上一级的类加载器,一直委托到顶层,从顶层向下依次加载这个类,加载成功就跳出,否则一直往下,最终加载失败就会抛出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)方法。

为什么要自定义ClassLoader?

  • 给.class文件加密,使得无法反编译

通常生成的.class文件很容易被反编译,我们可以利用生成的.class文件(未加密)读成字节流,然后堆字节流做一些转换,然后再写成.class文件,name这个.class文件就是加密的了,无法反编译。但是加密后的文件无法被默认的类加载器加载,就需要自己定义类加载器,在findClass()方法中得到字节流后进行还原

  • 需要读取特定目录的class类(例如Tomcat就定义了自己的类加载器)

自定义ClassLoader的详细方法

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文件的位置,通过类路径,

  • name不能随便乱写
    • com/test/TestEntity错,不能用/
    • com.test.TestEntity2错,类名错误,NoClassDefFoundError异常
    • com.test2.TestEntity错,包名错误,NoClassDefFoundError异常
    • com.test.TestEntity.class错,不能带.class扩展名,native方法抛出异常
    • "",错,虽然看源码检查这个name这里是可以的,可能是native方法抛出异常了
    • null,正确,等价于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()方法

关于覆盖ext中的类

假设如下代码打包成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/

你可能感兴趣的文章
课程总结(第一周)
查看>>
【Java基础】 Java面向对象之抽象类、接口详解
查看>>
【IDEA系列】HelloWorld 项目创建及相关配置文件介绍
查看>>
我要偷偷的学C语言,然后惊呆所有人(第八天)
查看>>
关于LeetCode刷题及题目列表归纳
查看>>
【大数据Spark系列】Spark Transformation和Action算子
查看>>
【大数据Flink系列】Flink 核心概念综述
查看>>
【大数据Kafka系列】深入理解Kafka副本机制
查看>>
【Django系列】Django模板所有知识点总结
查看>>
【Java系列】Spring常见知识点
查看>>
wxPython中TextCtrl的输入上限问题
查看>>
Python进程间共享内存(版本3.8+)
查看>>
行为树 --- [1] BehaviorTree.CPP的编译及使用
查看>>
安装和使用google-perftools工具
查看>>
使用Python+yolov3实现对帧数不等长视频进行批处理
查看>>
Java队列Queue
查看>>
专业mac数据恢复软件Tenorshare UltData Mac
查看>>
城市天际线 for Mac城市建造类游戏
查看>>
专业视频剪辑软件Final Cut Pro X Mac
查看>>
alienskineyecandy mac
查看>>