资讯详情

ClassLoader双亲委派模型与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;
            }
        });

}

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

标签: adtl082armz集成电路

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台