ClassLoader双亲委派模型与SPI

本文深入探讨Java中类加载器的工作原理,包括类加载器的层级结构与双亲委派机制,并详细解析如何利用自定义类加载器实现特定需求。此外,还介绍了Service Provider Interface (SPI) 的工作流程及其如何在不破坏类加载机制的情况下实现类的自动发现。
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 方法更改当前线程的 contextClassLoader
    class 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;
            }
        });

}

为什么说 SPI 打破了双亲委派模型?
比如 jdbc4.0 中,DriverManager 类位于 rt.jar 包,由 BootStrapClassLoader 加载,理应无法加载 classpath 下的类,也就无法使用厂商自己实现的 Driver 对象。但是 ServiceLoader 使用线程中的 contextClassLoader 加载 classPath 下的 Driver 类,加载后执行静态代码块向 DriverManager 注册了自己的 Driver 对象,建立了对象见得引用关系,跳过了类加载的时机

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值