一、本文纲要
二、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 的源码实现,敬请期待了