【Java高级】类加载器核心技术,从自定义加载外部jar说起
本文为原创文章,转载请注明出处
查看[Java]系列内容请点击:https://www.jianshu.com/nb/45938443
我们先举个例子,假如我们有如下的类:
package com.codelifeliwan;
public class Test {
public void test() {
System.out.println("----------------------BEGIN test--------------------");
System.out.println("this is thread:" + Thread.currentThread().getId() +
"\nContextClassLoader:" + Thread.currentThread().getContextClassLoader() +
"\nClassLoader:" + this.getClass().getClassLoader());
}
// 注意这里是静态方法
public static void staticTest() {
System.out.println("----------------------BEGIN staticTest--------------------");
System.out.println("this is thread:" + Thread.currentThread().getId() +
"\nContextClassLoader:" + Thread.currentThread().getContextClassLoader() +
"\nClassLoader:" + Test.class.getClassLoader());
}
}
我们将这个类打成jar包test.jar
,那么我们如何在另外一个程序里面加载这个jar包并使用其中的程序?
双亲委托类加载器
一个比较通用的方法,是自定义一个类加载器来加载这个jar包,这里我们使用URLClassLoader
类加载器:
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
// 全局共享的外部类加载器
private static ClassLoader classLoader = null;
// 初始化类加载器
synchronized public static void setClassLoader() throws Exception {
if (classLoader != null) return;
classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});
Thread.currentThread().setContextClassLoader(classLoader);
}
// 测试类加载器
public static void doTest() throws Exception {
Class clazz = classLoader.loadClass("com.codelifeliwan.Test"); // 使用URLClassLoader类加载器加载jar中的类
clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法
Object obj = clazz.getConstructor(null).newInstance();
clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法
System.out.println("=================================================\n");
}
public static void main(String[] args) throws Exception {
setClassLoader(); // 初始化类加载器
doTest(); // 测试类加载器
}
}
注意,这里测试类加载器使用的是反射的方式。
程序输出:
----------------------BEGIN staticTest--------------------
this is thread:1
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:1
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================
可以看到,我们能够加载对应的jar中的类,并进行实例化和调用等。
我们主要看输出的信息:
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
可以看到,虽然我们用的是自己定义的URLClassLoader
类加载器来进行加载外部的程序的,但是这里实际使用的是应用类加载器AppClassLoader
(在创建classLoader
的时候自动设置这个对象的类加载器为应用类加载器)。我们跟踪进入ClassLoader
类的protected Class<?> loadClass(String name, boolean resolve)
方法中查看,发现在加载的时候执行了:
c = parent.loadClass(name, false);
这里,parent
就是应用类加载器,加载完的c
不为空则直接返回相应的类,对于每一级类加载器,系统会优先使用父级类加载器,只有当父级类加载器加载失败的时候才使用当前的类加载器。这就是著名的类加载的【双亲委托模型】,相关资料请自行查阅。
线程上下文类加载器
现在,我们将测试的程序稍微改一下:
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
// 全局共享的外部类加载器
private static ClassLoader classLoader = null;
// 初始化类加载器
synchronized public static void setClassLoader() throws Exception {
if (classLoader != null) return;
classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});
// 注意这里设置了当前线程的上下文类加载器,这个类加载器会遗传给子线程
Thread.currentThread().setContextClassLoader(classLoader);
}
// 测试类加载器
public static void doTest() throws Exception {
Class clazz = classLoader.loadClass("com.codelifeliwan.Test");
clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法
Object obj = clazz.getConstructor(null).newInstance();
clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法
System.out.println("=================================================\n");
}
public static void main(String[] args) throws Exception {
new Thread(new MyRunnable()).start();
Thread.sleep(1000); // 为了避免打印混乱
new Thread(new MyRunnable()).start();
Thread.sleep(1000); // 为了避免打印混乱
}
public static class MyRunnable implements Runnable {
@Override
public void run() {
try {
setClassLoader();
doTest();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
我们先来看输出结果:
----------------------BEGIN staticTest--------------------
this is thread:14
ContextClassLoader:java.net.URLClassLoader@24912ef7
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:14
ContextClassLoader:java.net.URLClassLoader@24912ef7
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================
----------------------BEGIN staticTest--------------------
this is thread:15
ContextClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:15
ContextClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================
我们分别开了14和15两个线程,在14线程中设置了线程的上下文类加载器,那么可以看到,两个线程的ContextClassLoader
结果是不一样的,线程设置的上下文类加载器可以遗传到子线程中,然后在子线程中可以使用对应的类加载器来加载其他的程序。
注意:其实,线程上下文类加载器更应该叫做线程下文类加载器
,加载器只会遗传给子线程,对父线程和兄弟线程没有影响。上面的输出结果说明了这一点。
为了说明线程上下文类加载器会遗传到子线程,我们把初始化类加载器的工作放到主线程中:
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
// 全局共享的外部类加载器
private static ClassLoader classLoader = null;
// 初始化类加载器
synchronized public static void setClassLoader() throws Exception {
if (classLoader != null) return;
classLoader = new URLClassLoader(new URL[]{new File("D:\\test.jar").toURI().toURL()});
// 注意这里设置了当前线程的上下文类加载器,这个类加载器会遗传给子线程
Thread.currentThread().setContextClassLoader(classLoader);
}
// 测试类加载器
public static void doTest() throws Exception {
Class clazz = classLoader.loadClass("com.codelifeliwan.Test");
clazz.getMethod("staticTest").invoke(null, null); // 调用静态的staticTest方法
Object obj = clazz.getConstructor(null).newInstance();
clazz.getMethod("test").invoke(obj, null); // 调用非静态的test方法
System.out.println("=================================================\n");
}
public static void main(String[] args) throws Exception {
setClassLoader(); // 初始化类加载器放在主线程
new Thread(new MyRunnable()).start();
Thread.sleep(1000); // 为了避免打印混乱
new Thread(new MyRunnable()).start();
Thread.sleep(1000); // 为了避免打印混乱
}
public static class MyRunnable implements Runnable {
@Override
public void run() {
try {
doTest();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
然后会输出结果:
----------------------BEGIN staticTest--------------------
this is thread:14
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:14
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================
----------------------BEGIN staticTest--------------------
this is thread:15
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
----------------------BEGIN test--------------------
this is thread:15
ContextClassLoader:java.net.URLClassLoader@b4c966a
ClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
=================================================
JDBC的驱动就是使用线程上下文类加载器,读者可自行查阅资料