咱們知道,在 JVM 中,一個類加載的過程大體分爲加載、連接(驗證、準備、解析)、初始化5個階段。而咱們一般提到類的加載,就是指利用類加載器(ClassLoader)經過類的全限定名來獲取定義此類的二進制字節碼流,進而構造出類的定義。java
Flink 做爲基於 JVM 的框架,在 flink-conf.yaml 中提供了控制類加載策略的參數 classloader.resolve-order,可選項有 child-first(默認)和 parent-first。本文來簡單分析一下這個參數背後的含義。apache
ParentFirstClassLoader 和 ChildFirstClassLoader 類的父類均爲 FlinkUserCodeClassLoader 抽象類,先來看看這個抽象類,代碼很短。安全
public abstract class FlinkUserCodeClassLoader extends URLClassLoader { public static final Consumer<Throwable> NOOP_EXCEPTION_HANDLER = classLoadingException -> {}; private final Consumer<Throwable> classLoadingExceptionHandler; protected FlinkUserCodeClassLoader(URL[] urls, ClassLoader parent) { this(urls, parent, NOOP_EXCEPTION_HANDLER); } protected FlinkUserCodeClassLoader( URL[] urls, ClassLoader parent, Consumer<Throwable> classLoadingExceptionHandler) { super(urls, parent); this.classLoadingExceptionHandler = classLoadingExceptionHandler; } @Override protected final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { try { return loadClassWithoutExceptionHandling(name, resolve); } catch (Throwable classLoadingException) { classLoadingExceptionHandler.accept(classLoadingException); throw classLoadingException; } } protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve) throws ClassNotFoundException { return super.loadClass(name, resolve); } }
FlinkUserCodeClassLoader 繼承自 URLClassLoader。由於 Flink App 的用戶代碼在運行期才能肯定,因此經過 URL 在 JAR 包內尋找全限定名對應的類是比較合適的。而 ParentFirstClassLoader 僅僅是一個繼承 FlinkUserCodeClassLoader 的空類而已。框架
static class ParentFirstClassLoader extends FlinkUserCodeClassLoader { ParentFirstClassLoader(URL[] urls, ClassLoader parent, Consumer<Throwable> classLoadingExceptionHandler) { super(urls, parent, classLoadingExceptionHandler); } }
這樣就至關於 ParentFirstClassLoader 直接調用了父加載器的 loadClass() 方法。以前已經講過,JVM 中類加載器的層次關係和默認 loadClass() 方法的邏輯由雙親委派模型(parents delegation model)來體現,複習一下含義:ide
若是一個類加載器要加載一個類,它首先不會本身嘗試加載這個類,而是把加載的請求委託給父加載器完成,全部的類加載請求最終都應該傳遞給最頂層的啓動類加載器。只有當父加載器沒法加載到這個類時,子加載器纔會嘗試本身加載。
可見,Flink 的 parent-first 類加載策略就是照搬雙親委派模型的。也就是說,用戶代碼的類加載器是 Custom ClassLoader,Flink 框架自己的類加載器是 Application ClassLoader。用戶代碼中的類先由 Flink 框架的類加載器加載,再由用戶代碼的類加載器加載。可是,Flink 默認並不採用 parent-first 策略,而是採用下面的 child-first 策略,繼續看。oop
咱們已經瞭解到,雙親委派模型的好處就是隨着類加載器的層次關係保證了被加載類的層次關係,從而保證了 Java 運行環境的安全性。可是在 Flink App 這種依賴紛繁複雜的環境中,雙親委派模型可能並不適用。例如,程序中引入的 Flink-Cassandra Connector 老是依賴於固定的 Cassandra 版本,用戶代碼中爲了兼容實際使用的 Cassandra 版本,會引入一個更低或更高的依賴。而同一個組件不一樣版本的類定義有可能會不一樣(即便類的全限定名是相同的),若是仍然用雙親委派模型,就會由於 Flink 框架指定版本的類先加載,而出現莫名其妙的兼容性問題,如 NoSuchMethodError、IllegalAccessError 等。this
鑑於此,Flink 實現了 ChildFirstClassLoader 類加載器並做爲默認策略。它打破了雙親委派模型,使得用戶代碼的類先加載,官方文檔中將這個操做稱爲"Inverted Class Loading"。代碼仍然不長,錄以下。url
public final class ChildFirstClassLoader extends FlinkUserCodeClassLoader { private final String[] alwaysParentFirstPatterns; public ChildFirstClassLoader( URL[] urls, ClassLoader parent, String[] alwaysParentFirstPatterns, Consumer<Throwable> classLoadingExceptionHandler) { super(urls, parent, classLoadingExceptionHandler); this.alwaysParentFirstPatterns = alwaysParentFirstPatterns; } @Override protected synchronized Class<?> loadClassWithoutExceptionHandling( String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { // check whether the class should go parent-first for (String alwaysParentFirstPattern : alwaysParentFirstPatterns) { if (name.startsWith(alwaysParentFirstPattern)) { return super.loadClassWithoutExceptionHandling(name, resolve); } } try { // check the URLs c = findClass(name); } catch (ClassNotFoundException e) { // let URLClassLoader do it, which will eventually call the parent c = super.loadClassWithoutExceptionHandling(name, resolve); } } if (resolve) { resolveClass(c); } return c; } @Override public URL getResource(String name) { // first, try and find it via the URLClassloader URL urlClassLoaderResource = findResource(name); if (urlClassLoaderResource != null) { return urlClassLoaderResource; } // delegate to super return super.getResource(name); } @Override public Enumeration<URL> getResources(String name) throws IOException { // first get resources from URLClassloader Enumeration<URL> urlClassLoaderResources = findResources(name); final List<URL> result = new ArrayList<>(); while (urlClassLoaderResources.hasMoreElements()) { result.add(urlClassLoaderResources.nextElement()); } // get parent urls Enumeration<URL> parentResources = getParent().getResources(name); while (parentResources.hasMoreElements()) { result.add(parentResources.nextElement()); } return new Enumeration<URL>() { Iterator<URL> iter = result.iterator(); public boolean hasMoreElements() { return iter.hasNext(); } public URL nextElement() { return iter.next(); } }; } }
核心邏輯位於 loadClassWithoutExceptionHandling() 方法中,簡述以下:spa
可見,child-first 策略避開了「先把加載的請求委託給父加載器完成」這一步驟,只有特定的某些類必定要「遵循舊制」。alwaysParentFirstPatterns 集合中的這些類都是 Java、Flink 等組件的基礎,不能被用戶代碼沖掉。它由如下兩個參數來指定:scala
classloader.parent-first-patterns.default,不建議修改,固定爲如下這些值:
java.; scala.; org.apache.flink.; com.esotericsoftware.kryo; org.apache.hadoop.; javax.annotation.; org.slf4j; org.apache.log4j; org.apache.logging; org.apache.commons.logging; ch.qos.logback; org.xml; javax.xml; org.apache.xerces; org.w3c
以上是關於 flink-conf.yaml 中提供的控制類加載策略的參數 classloader.resolve-order 含義的理解和分享,但願對你們有所啓發和幫助~