1. ClassLoader 的 parent 层级
jvm 中类由类加载器加载, 类加载器本身也是一个对象,所以我们说 ClassLoader 可加载类对象
。jvm 此外,用户还可以使用三个过程中唯一的类加载器对象 new
实例化自己的类加载器对象。jvm 自身维护了三类内置加载器对象
和自己的实例类加载器对象
之间的 parent 层次关系表现为:
- 由 C 实现的顶级
BootStrap加载器
: 因为是 C 如果打印此类加载器对象,则表示为 null。负责加载$JAVA_HOME/lib
目录下的 jar 包中的 class - Java 实现的
Extension 加载器
: 用 sun.misc.Launcher 的内部类ExtClassLoader
负责加载$JAVA_HOME/lib/ext
目录下的jar包 - Java 实现的 Application 加载器: 用 sun.misc.Launcher 的内部类
AppClassLoader
表示, 负责加载$classpath
里面的类ClassLoader#getSystemClassLoader()
方法返回是 AppClassLoader 对象也叫系统加载器
ClassLoader 对象的层次关系不是通过继承实现的,而是通过组合实现的 parent 属性
以下代码验证了这种层次关系
public static void testLoaderLevel(){ // MyClassLoader 是一个
自定义类加载器,自定义方法后面介绍 ClassLoader myLoader1 = new MyClassLoader(); System.out.println(myLoader1); // MyClassLoader@548c4f57 ClassLoader parent1 = myLoader1.getParent(); System.out.println(parent1); // sun.misc.Launcher$AppClassLoader@18b4aac2 ClassLoader parent2 = parent1.getParent(); System.out.println(parent2); // sun.misc.Launcher$ExtClassLoader@1218025c ClassLoader parent3 = parent2.getParent(); System.out.println(parent3); // null System.out.println(ClassLoader.getSystemClassLoader() == parent1); // true (反映了三个内置加载器的全局唯一) }
2. 类加载器的 parent 委派机制
java 设计了类加载器对象的基类 ClassLoader
,是上述 ext, app, 定制类加载器的基类,定义类加载的双亲委派模式。 首先,jvm 用类加载对象加载类时 ClassLoader#loadClassInternal()
方法是入口, 该方法只调用了 ClassLoader#loadClass()
方法,该方法是线程安全的。
// ClasLoader.java
// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
接着,在 ClassLoader#loadClass()
方法中,实现了类加载器的双亲委派机制: 递归调用 parent 的 loadClass() 方法:
- 先看用类加载对象,看该类名是否已经被加载
- 如果未被加载,则递归调用 parent 类加载器加载,否则调用自身的
findClass()
进行加载。
// ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
这里就暴露了自定义类加载器的实现方法:
- 首先,让自定义的类加载器类
extends ClassLoader
- 如果想保留双亲委派机制,只
覆盖 findClass()
方法即可。 protected final Class<?> defineClass(String name, byte[] b, int off, int len)
方法将字节数组转换为 Class<?>
对象的方法,所以只需在 findClass 中获取字节数组,再调用 ClassLoader#defineClass() 方法即可。
- 如果想打破双亲委派机制,就去覆盖
loadClass()
方法。
如下,自定义类加载器的方法
class MyClassLoader extends ClassLoader{
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if ("ClassA".equals(name)){
try{
InputStream is = getClass().getResourceAsStream("/ClassA.class");
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name,bytes,0,bytes.length);
}catch (IOException ignored){
}
}else {
return super.loadClass(name);
}
return null;
};
}
3. 子类加载器可以访问 parent 类加载加载的类,parent 类加载器加载的类不能访问子类加载器加载的类
首先记住一个事实, 这个事实能够成立是因为 ClassLoader 基类的 loadClass() 方法中的 parent 委派。下面用代码进行验证. 我们知道, 被装载的类由类加载器对象
和类全名
共同唯一标识, 换种说法就是, jvm所有加载的类, 被其类加载器分隔到不同的命名空间, 每个类加载器对象都有自己的命名空间, 子 classLoader 对象命名空间可以访问 parent classLoader 对象命名空间中的类, 反过来 parent 不能访问子 classLoader 对象命名空间中的类.
- 先让 MyClass1 由 AppClassLoader 加载, MyClass2 由自定义类加载器加载. 然后在 MyClass2 构造器中调用
new MyClass1()
, 会报错, 表示 parent 类加载器加载的类找不到子 classLoader 加载的类. 报错如下:
自定义findClass被调用...
MyTest2 classLoader is: ClassLoaderTest@119d7047
MyTest1 classLoader is: sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: MyTest2
at MyTest1.<init>(MyTest1.java:5)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.lang.Class.newInstance(Class.java:442)
at ClassLoaderTest.main(ClassLoaderTest.java:37)
Caused by: java.lang.ClassNotFoundException: MyTest2
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
... 7 more
- 实例代码如下:
// MyTest1.java
public class MyTest1 {
public MyTest1() {
// 让 MyTest1 由 AppClassLoader 加载
System.out.println("MyTest1 classLoader is: " + this.getClass().getClassLoader());
new MyTest2(); // parent 类加载器加载的 MyTest1 调用子 classLoader 加载的 MyTest2
}
}
// MyTest2.java
public class MyTest2 {
// 让 MyTest2 由自定义加载器加载
public MyTest2() {
System.out.println("MyTest2 classLoader is: " + this.getClass().getClassLoader());
}
}
// ClassLoaderTest.java
public class ClassLoaderTest extends ClassLoader {
public String baseUrl;
// findClass() 方法只有在 AppClassLoader 找不到时才调用
// 所以要想自定义类加载器生效, class 文件不能放在 classPath 下
@Override
public Class<?> findClass(String className) {
System.out.println("自定义findClass被调用...");
String path = baseUrl + className.replace(".", "\\") + ".class";
try {
InputStream is = new FileInputStream(path);
byte data[] = new byte[is.available()];
is.read(data);
return defineClass(className, data, 0, data.length);
} catch (IOException ignored) {
}
return null;
}
private void setPath(String baseUrl) {
this.baseUrl = baseUrl;
}
public static void main(String[] args) throws Exception {
ClassLoaderTest loader2 = new ClassLoaderTest();
loader2.setPath("/Users/liujie02/IdeaProjects/Codes/spring-boot-demo/target/myTest/");
Class<?> c2 = loader2.loadClass("MyTest2");
Object o2 = c2.newInstance();
System.out.println();
ClassLoaderTest loader1 = new ClassLoaderTest();
loader1.setPath(".");//设置自定义类加载器的加载路径
//被类加载器加载后,得到Class对象
Class<?> c1 = loader1.loadClass("MyTest1");
Object o1 = c1.newInstance();//实例化MyTest1
System.out.println();
}
}
4. 什么是 SPI
SPI 是jdk规定接口,厂商面向接口编程提供实现类,使用时,只要将厂商的实现 jar 包加入 classPath,就能自动判断使用接口包含实现类。方法为用 ServiceLoader
获取所有 classPath jar 包内的 META-INF/services/fileName
文件中定义的实现类全名。有两个问题:
- (1)ServiceLoader 如何获取 classPath 下所有包含 META-INF/services/fileName 的 url? 使用 classLoader 对象的
getResources()
方法,返回可迭代的 url 枚举 public static void test () throws IOException {
// jar:file:/home/lj/.m2/repository/mysql/mysql-connector-java/8.0.25/mysql-connector-java-8.0.25.jar!/META-INF/services/java.sql.Driver
// jar:file:/home/lj/.m2/repository/org/postgresql/postgresql/42.2.22/postgresql-42.2.22.jar!/META-INF/services/java.sql.Driver
String url = "META-INF/services/java.sql.Driver";
// 用某个 classLoader 对象, 在其查找范围内查找 url 枚举数组
Enumeration<URL> enums = Thread.currentThread().getContextClassLoader().getResources(url);
while(enums.hasMoreElements()){
System.out.println(enums.nextElement());
}
}
- (2)ServiceLoader 用的是哪个 classLoader 对象来获取 url 的? 使用
Thread.currentThread().getContextClassLoader()
,这个方法一般返回 AppClassLoader 对象。contextClassLoader
其实是 Thread 类的一个属性,这个属性在 jvm 创建线程时自动赋值,或者自己手动调用 setContextClassLoader
方法更改当前线程的 contextClassLoaderclass Thread implements Runnable {
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
... ...
this.contextClassLoader = cl;
}
}
ServiceLoader 迭代器 LazyIterator 的 next 方法
class ServiceLoader {
// 关键属性
private LazyIterator lookupIterator = new LazyIterator(service, loader); // 所有文件中配置的所有行的迭代器
private ClassLoader loader = Thread.currentThread().getContextClassLoader()
private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 类名和实例化对象的缓存
private class LazyIterator{
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 用 Thread 中的 contextClassLoader 加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
// ... 省略 fail 异常处理
}
// ... 省略 fail 异常处理
try {
// contextClassLoader 加载好的类实例化出一个对象加入 ServiceLoader 的 providers 属性中
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
// ... 省略 fail 异常处理
}
}
}
}
【测试用例】:当 classPath 下同时加入 mysql 和 postgreSql 的驱动后,jar 包中配置的 spi 文件就能通过 ServiceLoader 访问到
// 测试代码
public static void main(String[] args) throws ClassNotFoundException, IOException {
ServiceLoader<Driver> driverServices = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = driverServices.iterator();
try{
while(driversIterator.hasNext()) {
// ServiceLoader 暴露的迭代器在调用 next() 方法时,内部会执行 Class.forName
Driver d = driversIterator.next();
System.out.println(d.getClass().newInstance());
}
} catch(Throwable t) {
}
}
5. jdbc4.0 以后使用 SPI 跳过 Class.forName
首先,之前使用 jdbc 需要调用 Class.forName("com.mysql.cj.jdbc.Driver")
,主要是为了调用 com.mysql.cj.jdbc.Driver 类的静态代码块,向 DriverManager 中注册自己的 Driver 化对象, 之后 DriverManager 的 getConnection,走的都是厂商自己 Driver 的方法
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
JDBC4.0以后,可以省略这句 Class.forName(),直接调用 rt.jar 包中的 DriverManager
方法就能获取连接
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/adtl_test" ,
"lj" ,"123456" );
原因是, DriverManager 的静态代码块中,使用 ServiceLoader 加载了 Driver.class 的厂商实现类, 并在 ServiceLoader 的迭代器迭代时,自动对文件中配置的实现类类名执行 Class.forName() ,执行注册方法。
public class DriverManager {
static {
loadInitialDrivers(); // SPI
println("JDBC DriverManager initialized");
}
// SPI 方法
private static void loadInitialDrivers() {
... // 省略 System.getProperty("jdbc.drivers") 环境变量中获取
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
// ServiceLoader 迭代器在调用 next() 时,会对文件中配置的类名执行 Class.forName()
driversIterator.next();
}
} catch(Throwable t) {
}
return null;
}
});
}
比如 jdbc4.0 中,DriverManager 类位于 rt.jar 包,由 BootStrapClassLoader 加载,理应无法加载 classpath 下的类,也就无法使用厂商自己实现的 Driver 对象。但是 ServiceLoader 使用线程中的 contextClassLoader 加载 classPath 下的 Driver 类,加载后执行静态代码块向 DriverManager 注册了自己的 Driver 对象,建立了对象见得引用关系,跳过了类加载的时机