双亲委派 & URLClassLoader & SPI

  • ClassLoader & 双亲委派
  • URLClassLoader
  • SPI(Service Provider Interface)

ClassLoader & 双亲委派

ClassLoader 编程语言 加载内容 Parent ClassLoader
BootstrapClassLoader C++ jre/lib;jre/classes Null Java虚拟机启动后初始化
ExtClassLoader Java jre/lib/ext;java.ext.dirs BootstrapClassLoader
AppClassLoader Java classpath指定位置 ExtClassLoader ClassLoader.getSystemClassLoader 返回值

ClassLoader的基本模型:

Java默认ClassLoader委派模型
载录:https://www.cnblogs.com/doit8791/p/5820037.html

URLClassLoader

  • ClassLoader.loadClass
  • URLClassLoader.findClass
  • ExtClassLoader & AppClassLoader 的URLClassPath

ClassLoader.loadClass

    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) {
                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.
                    c = findClass(name);
                }
            }
            if (resolve) { resolveClass(c); }
            return c;
        }
    }

    // ClassLoader method
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

其中findClass是关键:一来没有具体实现,二来修饰符是protected,所以 很明显是为了“开闭”。

URLClassLoader.findClass

// URLClassLoader overwrite
    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

这里要关注的点是: ucp.getResource

所有继承至URLClassLoader的子类(不overwrite findClass的情况下),都是 【只能通过ucp来进行资源的加载】。

URLClassPath做到了两点:
- 指定资源的来源:可以来自于Local,来自于Remote;可以是Jar,也可以是War等等(指定URL即可)。
- 限制资源的来源:当指定了该ucp,那么该ClassLoader的资源来源也就被【限制】只能是来自于这里。

ExtClassLoader & AppClassLoader

AppClassLoader&ExtClassLoader继承关系图
相比之下ExtClassLoader和AppClassLoader有何区别呢?通过源码对比一下URLClassPath

  • ExtClassLoader
    static class ExtClassLoader extends URLClassLoader {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            final File[] var0 = getExtDirs();

            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                    public Launcher.ExtClassLoader run() throws IOException {
                        int var1 = var0.length;

                        for(int var2 = 0; var2 < var1; ++var2) {
                            MetaIndex.registerDirectory(var0[var2]);
                        }

                        return new Launcher.ExtClassLoader(var0);
                    }
                });
            } catch (PrivilegedActionException var2) {
                throw (IOException)var2.getException();
            }
        }

        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

        private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }

        private static URL[] getExtURLs(File[] var0) throws IOException {
            Vector var1 = new Vector();

            for(int var2 = 0; var2 < var0.length; ++var2) {
                String[] var3 = var0[var2].list();
                if (var3 != null) {
                    for(int var4 = 0; var4 < var3.length; ++var4) {
                        if (!var3[var4].equals("meta-index")) {
                            File var5 = new File(var0[var2], var3[var4]);
                            var1.add(Launcher.getFileURL(var5));
                        }
                    }
                }
            }

            URL[] var6 = new URL[var1.size()];
            var1.copyInto(var6);
            return var6;
        }
    }

(通过构造函数反推)结论:通过getExtClassLoader来获取ExtClassLoader时,直接采用环境变量中的“java.ext.dirs”作为资源

  • AppClassLoader
    static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
    }

(通过构造函数反推)结论:通过getAppClassLoader来获取AppClassLoader时,直接采用环境变量中的“java.class.path”作为资源

URLClassLoader 总结

对于自定义ClassLoader 只需要关心两点

  • 一:URLClassPath – 通常是构造函数或参数
  • 二:委派过程 – findClass

SPI(Service Provider Interface)

SPI机制简介
SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
SPI具体约定
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。
载录:https://blog.csdn.net/sigangjun/article/details/79071850

首先,ServiceLoader的构造函数 是private的,那么自然就需要定位static方法了。
其中load是c位,它有两个重载

  1. public static <S> ServiceLoader<S> load(Class<S> service) 即不提供ClassLoader
  2. public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) 即提供ClassLoader

其中 1中的逻辑 通过Thread.currentThread().getContextClassLoader()获取ClassLoader,并调用2。

所以 想要说清楚ServiceLoader,就不得不提到 Thread中的ContextClassLoader

问题引出

想象一下:如果让BootstrapClassLoader(或者ExtClassLoader)去load一个MySQL的Driver,正常情况下会成功吗?
答案是,不会成功的。

其实,细想也是能理解的,毕竟每一个ClassLoader都是有自己负责和管辖的URLClassPath,而MySQL的Driver并不在 BootstrapClassLoader(或者ExtClassLoader)所管辖的URLClassPath下,自然 也就不能成功load到了。
那么,该如何是好呢?(其实,其中还隐藏了一些问题:load事件何时发生?发生时候的上下文环境又是如何?这是JVM的范畴,这里不讨论)
通过上面的描述,可以猜到 解决的方法也是简单的,就是将MySQL的Driver放置到“正确”的URLClassPath下即可。 但是,如果真的是这样,那岂不是把所有的都放在BootstrapClassLoader下面进行load就万无一失了?

这里笔者会想到一个问题:(不考虑说BootstrapClassLoader的压力,因为这点笔者也说不清楚),由于纯理论 可能会描述不清,所以举例说明:
如 Kafka Connect中,当集群启动之后,会在集群上启动不同的任务(A任务,B任务),这时如果多任务之间都依赖于一个小模块,但是又正好,它们依赖的模块的版本不同且不兼容,那可如何是好?
这里就需要考虑到Class Resource的隔离了,这也是双亲委派设计的优点。(细节不与说明)

以上都是题外话(不过 不是废话,笔者觉得 提出的问题还是挺有趣的),回到刚刚的问题:如果让BootstrapClassLoader(或者ExtClassLoader)去load一个MySQL的Driver,正常情况下会成功吗?

其实这个问题吧 本身也是有问题的,问题就是:为什么非要让BootstrapClassLoader(或者ExtClassLoader)去load一个MySQL的Driver,让对应的ClassLoader去load不就好了吗?例如AppClassLoader。 – 这个问题的本质 就是SPI(Service Provider Interface)了。

回到SPI

SPI的关键字:接口编程(不对实现类进行硬编码)、可拔插、服务发现机制、动态注入。
简单通过DriverManager来说明ServiceLoader所提供的功能和使用方式:

首先:装载DriverManager,并触发静态逻辑:通过ServiceLoader装载所有Driver的实现类
接着:通过DriverManager.getConnection()获取匹配的Driver实现类
最后:由于是面向接口编程,所以直接使用Connection接口即可操作后续逻辑

以上过程看似简单,但是仔细看 可以提出一些问题:

  1. DriverManager 是在哪里,被哪个ClassLoader加载?
  2. Driver的实现类 是在哪里,被哪个ClassLoader加载?

从Coding的角度看,DriverManager总是在AppClassLoader或者更上层被加载(调用)的,那么自然 DriverManager会按照双亲委派的方式 不断的向上 委托加载,直到ExtClassLoader(因为DriverManager是在rt.jar的java.sql包下)。到此,第一个问题就解答完毕了。

接着,第二个问题的第一小问。Driver的实现类 是在哪里被加载的?

public class DriverManager {
    static { loadInitialDrivers(); }

    private static void loadInitialDrivers() {
        String drivers;
        ......
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                ......
            }
        });
		......
    }
}

答案就是这个,Driver的实现类 是在DriverManager的静态方法中 通过ServiceLoader进行加载的。

然后,第二个问题的第二小问。Driver的实现类 是被哪个ClassLoader加载?
观察加载逻辑,会发现 Driver的实现类 的加载 是通过 ServiceLoader实现的。而ServiceLoader在加载时,则是通过获取Thread中的ContextClassLoader进行加载的 – Thread.currentThread().getContextClassLoader()。
那么 这个ContextClassLoader到底是什么呢? – 默认情况下(可以set进行修改)是 AppClassLoader。这样就会形成如下的图:
在这里插入图片描述
所以:如果让BootstrapClassLoader(或者ExtClassLoader)去load一个MySQL的Driver,会发现 并不能成功的load到这个Driver实现类,但是,完全可以通过这种"破坏"的方式 来成功的回到上流的ClassLoader来load这个Driver实现类。
这就是 所谓的 “对双亲委派的破坏”,这也正是SPI的实现原理。

ServiceLoader使用方式 & SPI使用案例

ServiceLoader的使用需要三步操作

  1. 创建META-INF/services/service.name文件
  2. 将service的实现类的全限定名称写入该文件中
  3. 使用ServiceLoader.load(Service.class)来获取实现类

SPI的使用案例很多,有兴趣的可以好好看看:

  • org.slf4j.spi.SLF4JServiceProvider
  • com.facebook.presto.spi.Plugin
  • org.apache.flink.table.factories.TableFactory

后记

1 -- this.getClass().getResource(String name)
2 -- this.getClass().getClassLoader().getResource(String name)

最近正好使用了上面的来个方法,随便跟踪了一下源码,这里记录一下:
只需要关注(1)的源码即可

public java.net.URL getResource(String name) {
	name = resolveName(name);
	// 从这里可以看出,上面的(1)实际上还是调用了(2)
    ClassLoader cl = getClassLoader0();
    // 这里主要是 向上委派的递归过程(源码就不展示了)
    return cl.getResource(name);
}

这里关注一下resolveName逻辑

private String resolveName(String name) {
    if (!name.startsWith("/")) {
    	// this.getClass().getResource(".......") -- 将定位至this的相对路径下
        Class<?> c = this;
        while (c.isArray()) { c = c.getComponentType(); }
        String baseName = c.getName();
        int index = baseName.lastIndexOf('.');
        if (index != -1) { name = baseName.substring(0, index).replace('.', '/')+"/"+name; }
    } else { 
		// this.getClass().getResource("/.......") -- 将定位至classpath下
	    name = name.substring(1); 
	}
    return name;
}

结论:

  • this.getClass().getResource("") – 相对于this的路径
  • this.getClass().getResource("/") – classpath绝对路径
  • this.getClass().getClassLoader().getResource("") – classpath绝对路径
  • this.getClass().getClassLoader().getResource("/") – 爆炸?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章