一、本文纲要
二、ClassLoader 类加载器
1、Java 中的类加载器以及双亲委派机制
Java 中的类加载器,是 Java 运行时环境的一部分,负责动静加载 Java 类到 Java 虚拟机的内存中。
有了类加载器,Java 运行零碎不须要晓得文件与文件系统。
那么类加载器,什么类都加载吗?加载的规定是什么?
Java 中的类加载器有四种,别离是:
- BootstrapClassLoader,顶级类加载器,加载 JVM 本身须要的类;
- ExtClassLoader,他负责加载扩大类,如 jre/lib/ext 或 java.ext.dirs 目录下的类;
- AppClassLoader,他负责加载利用类,所有 classpath 目录下的类都能够被这个类加载器加载;
- 自定义类加载器,如果你要实现本人的类加载器,他的父类加载器都是 AppClassLoader。
类加载器采纳了双亲委派模式,其工作原理是,如果一个类加载器收到了类加载申请,它并不会本人先去加载,而是把这个申请委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,顺次递归,申请最终将达到顶层的启动类加载器,如果父类加载器能够实现类加载工作,就胜利返回,假使父类加载器无奈实现此加载工作,子加载器才会尝试本人去加载,这就是双亲委派模式。
双亲委派模式的益处是什么?
第一,Java 类随着它的类加载器一起具备了一种带有优先级的档次关系,通过这种档次关系能够防止类的反复加载,当父类加载器曾经加载过一次时,没有必要子类再去加载一次。
第二,思考到平安因素,Java 外围 Api 类不会被随便替换,外围类永远是被下层的类加载器加载。如果咱们本人定义了一个 java.lang.String 类,它会优先委派给 BootStrapClassLoader 去加载,加载完了就间接返回了。
如果咱们定义了一个 java.lang.ExtString,能被加载吗?答案也是不能的,因为 java.lang 包是有权限管制的,自定义了这个包,会报一个错如下:
java.lang.SecurityException: Prohibited package name: java.lang
2、双亲委派机制源码浅析
Java 程序的入口就是 sun.misc.Launcher 类,咱们能够从这个类开始看起。
上面是这个类的一些重要的属性,写在正文里了。
public class Launcher {private static URLStreamHandlerFactory factory = new Launcher.Factory();
// static launchcher 实例
private static Launcher launcher = new Launcher();
// bootclassPath,就是 BootStrapClassLoader 加载的系统资源
private static String bootClassPath = System.getProperty("sun.boot.class.path");
// 在 Launcher 构造方法中,会初始化 AppClassLoader,把它作为全局实例保存起来
private ClassLoader loader;
private static URLStreamHandler fileHandler;
......
}
这个类加载的时候,就会初始化 Launcher 实例,咱们看一下无参构造方法。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 取得 ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);
}
try {
// 取得 AppClassLoader,并赋值到全局属性中
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);
}
// 把 AppClassLoader 的实例赋值到以后上下文的 ClassLoader 中,和以后线程绑定
Thread.currentThread().setContextClassLoader(this.loader);
// ...... 省略无关代码
}
能够看到,先取得一个 ExtClassLoader,再把 ExtClassLoader 作为父类加载器,传给 AppClassLoader。最终会调用这个办法,把 ExtClassLoader 传给 parent 参数,作为父类加载器。
而在初始化 ExtClassLoader 的时候,没有传参:
Launcher.ExtClassLoader var1;
try {var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);
}
而最终,给 ExtClassLoader 的 parent 传的参数是 null。能够先记住这个属性,上面在讲 ClassLoader 源码时会用到这个 parent 属性。
而后 Launcher 源码外面还有四个零碎属性,值得咱们运行一下看看,如下图
从下面的运行后果中,咱们也能够轻易看到不同的类加载器,是从不同的门路下加载不同的资源。而即使咱们只是写一个 Hello World,类加载器也会在前面默默给咱们加载这么多类。
看完了 Launcher 类的代码,咱们再来看 java.lang.ClassLoader 的代码,真正的双亲委派机制的源码是在这个类的 loaderClass 办法中。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {
// 首先,查看这个类是否曾经被加载了,最终实现是一个 native 本地实现
Class<?> c = findLoadedClass(name);
// 如果还没有被加载,则开始架子啊
if (c == null) {long t0 = System.nanoTime();
try {
// 首先如果父加载器不为空,则应用父类加载器加载。Launcher 类里提到的 parent 就在这里应用的。if (parent != null) {c = parent.loadClass(name, false);
} else {
// 如果父加载器为空(比方 ExtClassLoader),就应用 BootStrapClassloader 来加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { }
// 如果还没有找到,则应用 findClass 类来加载。也就是说如果咱们自定义类加载器,就重写这个办法
if (c == null) {long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {resolveClass(c);
}
return c;
}
}
这段代码还是比拟清晰的,加载类的时候,首先判断类是不是曾经被加载过了,如果没有被加载过,则看本人的父类加载器是不是为空。如果不为空,则应用父类加载器加载;如果父类加载器为空,则应用 BootStrapClassLoader 加载。
最初,如果还是没有加载到,则应用 findClass 来加载类。
类加载器的基本原理就剖析到这里,上面咱们再来剖析一个 Java 中乏味的概念,SPI。
三、SPI 技术
1、什么是 SPI,为什么要有 SPI
SPI 全称(Service Provide Interface),在 JAVA 中是一个比拟重要的概念,在框架设计中被宽泛应用。
在框架设计中,要遵循的准则是 对扩大凋谢,对批改敞开 ,保障框架实现对于使用者来说是黑盒。因为框架不可能做好所有的事件,只能把共性的局部抽离进去进行流程化,而后留下一些扩大点让使用者去实现,这样不同的扩大就不必批改源代码或者对框架进行定制。也就是咱们常常说的 面向接口编程。
我了解的 SPI 用更艰深的话来讲,就是一种可插拔技术。最容易了解的就是 USB,定义好 USB 的接口标准,不同的外设厂家依据 USB 的规范去制作本人的外设,如鼠标,键盘等。另外一个例子就是 JDBC,Java 定义好了 JDBC 的标准,不同的数据库厂商去实现这个标准。Java 并不会管某一个数据库是如何实现 JDBC 的接口的。
2、如何实现 SPI
这里我在 Github 上有一个工程,Flink-Practice,是公众号文章附带的代码,有须要能够下载:
Flink 实战代码
实现 SPI 的话,要遵循上面的一些标准:
- 服务提供者提供了接口的具体实现后,须要在资源文件夹中创立 META-INF/services 文件夹,并且新建一个以全类名为名字的文本文件,文件内容为实现类的全名(如图中上面的红框);
- 接口实现类必须在工程的 classpath 下,也就是 maven 中须要退出依赖或者 jar 包援用到工程里(如图中的 serviceimpl 包,我就放在了以后工程下了,执行的时候,会把类编译成 class 文件放到以后工程的 classpath 下的);
- SPI 的实现类中,必须有一个不带参数的空构造方法
执行测试类之后输入如下:
能够看到,实现了提供方接口的类,都被执行了。
3、SPI 源码浅析
入口在 ServiceLoader.load 办法这里
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取以后线程的上下文类加载器。ContextClassLoader 是每个线程绑定的
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
首先须要晓得,Thread.currentThread().getContextClassLoader(); 应用这个获取的类加载器是 AppClassLoader,因为咱们的代码是在 main 函数执行的,而自定义的代码都是 AppClassLoader 加载的。
能够看到最终这个 classloader 是被传到这个中央
那么不传这个 loader 进来,就加载不到吗?答案是的确加载不到。
因为 ServiceLoader 是在 rt.jar 包中,而 rt.jar 包是 BootstrapClassLoader 加载的。而实现了接口提供者的接口的类,个别是第三方类,是在 classpath 下的,BootStrapClassLoader 能加载到 classpath 下的类吗?不能,AppClassLoader 才会去加载 classpath 的类。
所以,这里的上下文类加载器(ContextClassLoader),它其实是毁坏了双亲委派机制的,然而也为程序带来了微小的灵活性和可扩展性。
其实 ServiceLoader 外围的逻辑就在这两个办法里
private boolean hasNextService() {if (nextName != null) {return true;}
if (configs == null) {
try {
// 寻找 META-INF/services/ 类
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}
// 解析这个类文件的所有内容
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 加载这个类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider" + cn + "not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider" + cn + "not a subtype");
}
try {
// 初始化这个类
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider" + cn + "could not be instantiated",
x);
}
throw new Error(); // This cannot happen}
寻找 META-INF/services/ 类,解析类的内容,结构 Class,初始化,返回,就这么简略了。
4、SPI 的毛病以及 Dubbo 是如何重构 SPI 的
通过后面的剖析,能够发现,JDK SPI 在查找扩大实现类的过程中,须要遍历 SPI 配置文件中定义的所有实现类,该过程中会将这些实现类全副实例化。如果 SPI 配置文件中定义了多个实现类,而咱们只须要应用其中一个实现类时,就会生成不必要的对象。例如,在 Dubbo 中,org.apache.dubbo.rpc.Protocol 接口有 InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol、ThriftProtocol 等多个实现,如果应用 JDK SPI,就会加载全副实现类,导致资源的节约。
Dubbo SPI 不仅解决了上述资源节约的问题,还对 SPI 配置文件扩大和批改。
首先,Dubbo 依照 SPI 配置文件的用处,将其分成了三类目录。
META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI。
META-INF/dubbo/ 目录:该目录用于寄存用户自定义 SPI 配置文件。
META-INF/dubbo/internal/ 目录:该目录用于寄存 Dubbo 外部应用的 SPI 配置文件。
而后,Dubbo 将 SPI 配置文件改成了 KV 格局,例如:
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
其中 key 被称为扩展名(也就是 ExtensionName),当咱们在为一个接口查找具体实现类时,能够指定扩展名来抉择相应的扩大实现。例如,这里指定扩大名为 dubbo,Dubbo SPI 就晓得咱们要应用:org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 这个扩大实现类,只实例化这一个扩大实现即可,毋庸实例化 SPI 配置文件中的其余扩大实现类。
四、Flink 源码中应用到 SPI 和 Classloader 的中央
在 Flink 源码中,有很多这样的 SPI 扩大点
在 flink-clients 模块中
执行器工厂的接口,有本地执行器的实现和近程执行器工厂类的实现,这些都是通过 SPI 来实现的。
另外,在 Flink Clients 入口类 CliFronted 中,也应用了经典的 ContextClassLoader 用法,应用反射的形式来执行用户程序中编写的 main 办法
下一篇文章,咱们来剖析 Flink-Clients 的源码实现,敬请期待了