双亲委派机制

159 阅读3分钟

双亲委派机制

JVM 中加载类,需要循序双亲委派机制。即 子加载器 委托父加载器 加载类, 当且仅当父加载器加载不了时,才由 子加载器 加载类。

JVM 中有以下 默认的 3 个类加载。 Bootstrap、Extention、Application。Bootstrap 是最顶层的类加载

image.png

  • Bootstrap ClassLoader:负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
  • Extention ClassLoader:负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
  • Application ClassLoader:负责加载当前应用的classpath下的所有类
  • 用户自定义 ClassLoader: 用户自定义的类加载器,可加载指定路径的class文件

举个例子:现在加载 User.class

  1. Application ClassLoader 询问 Extension ClassLoader 能不能加载
  2. Extension ClassLoader 询问 Bootstrap ClassLoader 能不能加载
  3. 当 Bootstrap 返回不能加载时,Extension 会看自己能不能加载
  4. 当 Extension 返回不能加载时, Application 则自己加载。

优点

确保类只有 1 个,且避免外部注入不安全的类。

缺陷

当 基础类需要加载 用户类时,则会出现无法加载的情况。

举个例子: java.sql.DriverManager

DriverManager 中,需要加载各个厂商实现的驱动类。但是由于 DriverManager 是被 Bootstrap 类加载加载的。因此,该类当前的类加载就是 Bootstrap, 而 Bootstrap 是无法加载 各个厂商实现的 驱动类的。除非用 Application ClassLoader 加载 驱动类。

如果在 DriverManager 中使用 Application ClassLoader 加载驱动类,则破坏了双亲委派机制。因为 Bootstrap(父加载器) 委托 Application(子加载器)加载类。

破坏双亲委派机制

为什么要破坏双亲委派机制

  1. 需要 基础类加载用户类
  2. 需要实现热部署、热加载

在 JAVA 中要破坏双亲委派机制有 2 种常用的方法。

  1. SPI。经典代表为 DriverManager 加载 java.sql.Driver
  2. 继承 ClassLoader 类,并重写 loadClass 方法。经典代表为 tomcat 实现的热部署

ClassLoader#loadClass

public class ClassLoaderTest extends ClassLoader {

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
        // 因为传入的不是 类的全限定名称,而是 XX.class
        // 因此测试的时候,要加载的类需要与 ClassLoaderTest 同一个目录下
        InputStream is = this.getClass().getResourceAsStream(fileName);
        // 当前 class 获取不到资源,交由 父加载加载
        if (is == null) {
            return super.loadClass(name);
        }
        Class cls;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];

            int len;
            while ((len = is.read(buf)) >= 0) {
                baos.write(buf, 0, len);
            }

            buf = baos.toByteArray();
            int i = name.lastIndexOf(46);
            if (i != -1) {
                String pkgname = name.substring(0, i);
                Package pkg = this.getPackage(pkgname);
                if (pkg == null) {
                    this.definePackage(pkgname, (String) null, (String) null, (String) null, (String) null, (String) null, (String) null, (URL) null);
                }
            }

            cls = this.defineClass(name, buf, 0, buf.length);
        } catch (IOException ex) {
            throw new ClassNotFoundException(name, ex);
        } finally {
            try {
                is.close();
            } catch (IOException ex) {
            }
        }

        return cls;
    }
}

SPI

JAVA SPI 遵循以下步骤

  1. 定义接口
  2. 定义实现类实现 第一步定义的接口
  3. 在 resources 的 META-INF/service 下 用该接口的全限定类名新建文件。 例如 org.csp.learn.java.spi.Hello
  4. 将实现类的全限定类名写入 org.csp.learn.java.spi.Hello
org.csp.learn.java.spi.impl.CatHelloImpl
org.csp.learn.java.spi.impl.DogHelloImpl
  1. 通过 ServiceLoader API 加载类
ServiceLoader<Hello> load = ServiceLoader.load(Hello.class);
Iterator<Hello> iterator = load.iterator();
while (iterator.hasNext()) {
    Hello item = iterator.next();

}

更详细的代码请查阅 Github

参考