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

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.jar
Extension 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()方法。

猜你喜欢

转载自blog.csdn.net/w8y56f/article/details/89553979