前言
在深入openjdk源码全面理解Java类加载器(上 – JVM源码篇)我们分析了JVM是如何启动,并且初始化BootStrapClassLoader的,也提到了sun.misc.Launcher被加载后会创建ExtClassLoader和AppClassLoader。关于类加载的基础知识请参考虚拟机类加载机制(上)。这篇文章主要从Java源码层面总结一下双亲委派、TCCL的应用等,然后再聊聊自定义类加载器的注意事项。
一、双亲委派
1.1 类加载器结构
1.2 双亲委派
加载类的核心方法是loadClass,默认实现在ClassLoader中:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
//同步锁,可能是一个和name对应的Object,也可能是this //取决于类加载器是否具备并行能力 //首先检查类是否被本类加载器加载了 Class<?> c = findLoadedClass(name); if (c == null) {
//如果没有找到需要加载的类 long t0 = System.nanoTime(); try {
//使用父类加载器加载类 //如果parent不为null,说明设置了父加载器,直接用parent if (parent != null) {
c = parent.loadClass(name, false); } else {
//如果parent为null,使用BootStrapClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) {
//如果父类加载器没能加载到类,使用本类加载器加载 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) {
//是否需要立即解析 resolveClass(c); } return c; } }
注:关于getClassLoadingLock,可参考:关于类加载的并发控制锁。
二、自定义类加载器
自定义类加载器需要直接或间接继承ClassLoader,最简单的一个自定义类加载器就是继承ClassLoader,重写其findClass方法,通过ClassLoader.defineClass方法创建一个Class类(defineClass最终会调用ClassLoader的native方法):
public class MyClassLoader extends ClassLoader {
private URLClassPath ucp; public MyClassLoader(String path, ClassLoader parent) throws Exception {
super(parent); this.ucp = new URLClassPath(new URL[]{
new File(path).toURI().toURL()}); } static {
ClassLoader.registerAsParallelCapable(); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException {
String usePath = name.replace('.', File.separatorChar).concat(".class"); Resource resource = ucp.getResource(usePath, false); if (resource != null) {
try {
byte[] bytes = resource.getBytes(); return defineClass(name, bytes, 0, bytes.length); } catch (IOException var) {
return null; } } else {
return null; } } }
public class MyClassLoader2 extends ClassLoader {
private URLClassPath ucp; public MyClassLoader2(String path, ClassLoader parent) throws MalformedURLException {
super(parent); this.ucp = new URLClassPath(new URL[]{
new File(path).toURI().toURL()}); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("com.demo")) {
Resource resource = ucp.getResource(name.replace('.', File.separatorChar).concat(".class"), false); if (resource != null) {
try {
byte[] bytes = resource.getBytes(); Class clazz = defineClass(name, bytes, 0, bytes.length); if (resolve) {
resolveClass(clazz); } return clazz; } catch (IOException e) {
throw new ClassNotFoundException(e.getMessage()); } } else {
throw new ClassNotFoundException(); } } else {
return super.loadClass(name, resolve); } } }
这个类加载器对于com.demo包的类都由自己加载,其余的才委托给父类。测试一下:
public class JavaMain {
public static void main(String[] args) throws Exception {
String classPath = JavaMain.class.getClassLoader().getResource("").getPath(); MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader()); Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false); System.out.println(clazz.getClassLoader()); } } //输出 com.demo.classloader.MyClassLoader2@5cad8086
2.1 全盘委派
在我们的这个工程中,有一个问题,如果运行以下代码:
public static void main(String[] args) throws Exception {
String classPath = JavaMain.class.getClassLoader().getResource("").getPath(); MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader()); Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false); System.out.println(clazz.getClassLoader()); System.out.println(TestClass1.class.getClassLoader()); TestClass1 testClass1 = (TestClass1) clazz.newInstance(); } //输出: com.demo.classloader.MyClassLoader2@5cad8086 sun.misc.Launcher$AppClassLoader@18b4aac2 ClassCastException
类型强转操作会抛出java.lang.ClassCastException异常。造成这个的原因已经在输出结果中体现了,clazz是由自定义类加载加载的,而TestClass1.class是由AppClassLoader加载的。可以打印看看AppClassLoaer的加载目录:
System.out.println(System.getProperty("java.class.path"));
在mac下结果如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/tools.jar:/Users/loren/work/github/test/out/production/test:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar
输出的目录包含了当前项目目录,所以目录中的class可以被AppClassLoader加载。
注:除了当前项目目录,还有很多系统jar包,包括rt.jar、jce.jar等,当然由于AppClassLoader遵循双亲委派,路径包含这些jar包也不会有什么问题。
那么TestClass1是什么时候被AppClassLoader加载的呢?对于上述代码中的:
...... 1.Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false); 2.System.out.println(clazz.getClassLoader()); 3.System.out.println(TestClass1.class.getClassLoader()); ......
public class TestClass2 {
public TestClass2() {
System.out.println("testClass2.classLoader:" + this.getClass().getClassLoader()); } }
然后在TestClass1中创建一个方法触发TestClass2的实例化:
public class TestClass1 {
public void run() {
new TestClass2(); } }
由于main方法中使用TestClass1会被AppClassLoader加载,所以我们不能强转类型,只能通过反射调用该方法:
public class JavaMain {
public static void main(String[] args) throws Exception {
String classPath = JavaMain.class.getClassLoader().getResource("").getPath(); MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader()); Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false); Object obj = clazz.newInstance(); Method method = obj.getClass().getDeclaredMethod("run", null); method.setAccessible(true); method.invoke(obj, null); } }
输出如下:
testClass2.classLoader:com.demo.classloader.MyClassLoader2@5cad8086
2.2 覆盖核心类?
package java.util; public class HashMap {
}
然后创建一个自定义类加载器,这个和之前类似:
public class MyClassLoader3 extends ClassLoader {
private URLClassPath ucp; public MyClassLoader3(String path, ClassLoader parent) throws MalformedURLException {
super(parent); this.ucp = new URLClassPath(new URL[]{
new File(path).toURI().toURL()}); } static {
ClassLoader.registerAsParallelCapable(); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Resource resource = ucp.getResource(name.replace('.', File.separatorChar).concat(".class"), false); if (resource != null) {
try {
byte[] bytes = resource.getBytes(); Class clazz = defineClass(name, bytes, 0, bytes.length); if (resolve) {
resolveClass(clazz); } return clazz; } catch (IOException e) {
throw new ClassNotFoundException(e.getMessage()); } } else {
throw new ClassNotFoundException(); } } }
在main方法中创建自定义类加载器,加载路径为当前项目路径,然后尝试加载java.util.HashMap:
public static void main(String[] args) throws Exception {
String classPath = JavaMain.class.getClassLoader().getResource("").getPath(); MyClassLoader3 myClassLoader = new MyClassLoader3(classPath, JavaMain.class.getClassLoader()); Class clazz = myClassLoader.loadClass("java.util.HashMap", false); }
当然不出意外的是,有异常堆栈抛出:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.util at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655) at java.lang.ClassLoader.defineClass(ClassLoader.java:754) at java.lang.ClassLoader.defineClass(ClassLoader.java:635) at com.demo.classloader.MyClassLoader3.loadClass(MyClassLoader3.java:29) at com.demo.classloader.JavaMain.main(JavaMain.java:13)
提示禁止加载包:java.util,看堆栈信息异常是ClassLoader.preDefineClass抛出来的,看看相应的代码:
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd){
...... if ((name != null) && name.startsWith("java.")) {
throw new SecurityException ("Prohibited package name: " + name.substring(0, name.lastIndexOf('.'))); } ...... }
protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain) throws ClassFormatError {
protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; }
该方法是一个final方法,我们无法重写,那能在自定义类加载器中调用defineClass1方法吗?defineClass1方法定义在ClassLoader中,是一个private native方法:
private native Class<?> defineClass0(String name, byte[] b, int off, int len,ProtectionDomain pd); private native Class<?> defineClass1(String name, byte[] b, int off, int len,ProtectionDomain pd, String source); private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,int off, int len, ProtectionDomain pd,String source);
所以我们只能通过父类的defineClass创建Class,也就没法绕过preDefineClass方法的检查。既然如此,那能不能从本地方法入手呢?理论上是可行的,但是需要修改动态链接文件。但是都能操作dll了,还需要费尽心思去覆盖核心类库吗?
三、TCCL
/* The context ClassLoader for this thread */ private ClassLoader contextClassLoader;
通过相应的set方法:
Thread.currentThread().setContextClassLoader(classloader);
将一个类加载器和线程绑定。这样在一个线程中,需要加载当前类加载器无法加载的类的时候,可以从当前线程中获取TCCL进行加载:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
TCCL默认为AppClassLoader,初次在sun.misc.Launcher的构造方法中设置:
public Launcher() {
...... try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9); } //TCCL默认为AppClassLoader Thread.currentThread().setContextClassLoader(this.loader); ...... }
四、spring的类加载
ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) {
//spring在webapp下,类加载器相同 currentContext = this.context; } else if (ccl != null) {
//加载spring的类加载器和TCCL不同,将classLoader和WebApplicationContext用map //保存起来,用的时候根据classLoader获取context currentContextPerThread.put(ccl, this.context); }
当然,如果在SpringBoot中使用内嵌servlet容器的时候,就不会出现一个servlet容器包含多个应用的情况了,也就不用再用map维护不同的context了,直接使用TCCL即可:
ClassLoader cl = null; try {
cl = Thread.currentThread().getContextClassLoader(); } catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back... } if (cl == null) {
// No thread context class loader -> use class loader of this class. cl = ClassUtils.class.getClassLoader(); if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader try {
cl = ClassLoader.getSystemClassLoader(); } catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null... } } } return cl;
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/222598.html原文链接:https://javaforall.net
