- 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的基本模型:
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
相比之下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位,它有两个重载
- public static <S> ServiceLoader<S> load(Class<S> service) 即不提供ClassLoader
- 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接口即可操作后续逻辑
以上过程看似简单,但是仔细看 可以提出一些问题:
- DriverManager 是在哪里,被哪个ClassLoader加载?
- 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的使用需要三步操作
- 创建META-INF/services/service.name文件
- 将service的实现类的全限定名称写入该文件中
- 使用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("/") – 爆炸?