1. 應用情境
有時候我們需要在一個 Project 中運行多個不同版本的 jar 包,以應對不同叢集的版本或其它的問題。如果這個時候選擇在同一個項目中實現這樣的功能,那麼通常只能選擇更低版本的 jar 包,因為它們通常是向下相容的,但是這樣也往往會失去新版本的一些特性或功能,所以我們需要以擴充的方式引入這些 jar 包,並通過隔離執行,來實現版本的強制對應。 2. 實現
在 Java 中,所有的類預設通過 ClassLoader 載入,而 Java 預設提供了三層的 ClassLoader,並通過雙親委託模型的原則進行載入,其基本模型與載入位置如下(更多ClassLoader相關原理請自行搜尋):
Java 中預設的 ClassLoader 都規定了其指定的載入目錄,一般也不會通過 JVM 參數來使其載入自訂的目錄,所以我們需要自訂一個 ClassLoader 來載入裝有不同版本的 jar 包的擴充目錄,同時為了使運行擴充的 jar 包時,與啟動項目實現絕對的隔離,我們需要保證他們所載入的類不會有相同的 ClassLoader,根據雙親委託模型的原理可知,我們必須使自訂的 ClassLoader 的 parent 為 null,這樣不管是 JRE 內建的 jar 包或一些基礎的 Class 都不會委託給 App ClassLoader(當然僅僅是將 Parent 設定為 null 是不夠的,後面會說明)。與此同時這些實現了不同版本的 jar 包,是經過二次開發後的可以獨立啟動並執行項目。 2.1 執行個體
現在假定有這樣一個需求,實現針對叢集(比如 Hadoop 叢集)版本為 V1 與 V2 的對應的執行程式,那麼假定有如下項目:
Executor-Parent: 提供基礎的 Maven 引用,可利用 Maven 一鍵打包所有的子模組/項目Executor-Common: 提供基礎的介面,已經有公有的實現等Executor-Proxy: 執行不同版本程式的代理程式Executor-V1: 版本為V1的執行程式Executor-V2: 版本為V2的執行程式
這裡為了更凸顯 ClassLoader 的實現,不做 Executor-Parent 的實現,同時為了簡便,也沒有設定包名。 1) Executor-Common
在 Executor-Common 中提供一個介面,聲明執行的具體方法:
public interface Executor { void execute(final String name);}
這裡的方法使用了基礎類型 String,實際中可能會使用自訂的類型,那麼在 Porxy 的實現中則需要使用自訂的 ClassLoader 來載入參數,並使用反射來擷取方法(後面會有一個簡單的樣本)。回到之前的樣本,這裡同時提供一個抽象的實作類別:
public class AbstractExecutor implements Executor { @Override public void execute(final String name) { this.handle(new Handler() { @Override public void handle() { System.out.println("V:" + name); } }); } protected void handle(Handler handler) { handler.call(); } protected abstract class Handler { public void call() { ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); // 臨時更改 ClassLoader Thread.currentThread().setContextClassLoader(AbstractExecutor.class.getClassLoader()); handle(); // 還原為之前的 ClassLoader Thread.currentThread().setContextClassLoader(oldClassLoader); } public abstract void handle(); }}
這裡需要臨時更改當前線程的 ContextClassLoader, 以應對擴充程式中可能出現的如下代碼:
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();classLoader.loadClass(...);
因為它們會擷取當前線程的 ClassLoader 來載入 class,而當前線程的ClassLoader極可能是App ClassLoader而非自訂的ClassLoader, 也許是為了安全起見,但是這會導致它可能載入到啟動項目中的class(如果有),或者發生其它的異常,所以我們在執行時需要臨時的將當前線程的ClassLoader設定為自訂的ClassLoader,以實現絕對的隔離執行。 2) Executor-V1 & Executor-V2
Executor-V1 和 Executor-V2 依賴了 Executor-Common.jar,並實現了 Executor 介面的方法:
public class ExecutorV1 extends AbstractExecutor { @Override public void execute(final String name) { this.handle(new Handler() { @Override public void handle() { System.out.println("V1:" + name); } }); }}
public class ExecutorV2 extends AbstractExecutor { @Override public void execute(final String name) { this.handle(new Handler() { @Override public void handle() { System.out.println("V2:" + name); } }); }}
這裡僅僅是列印了它們的版本資訊,實際中,它們可能需要引入不同的版本的 Jar 包,然後根據這些 Jar 包完成相應的操作。 3) Executor-Proxy
Executor-Proxy 利用自訂的 ClassLoader 和反射來實現載入與運行 ExecutorV1 和 ExecutorV2 中 Executor 介面的實現,而 ExecutorV1 和 ExecutorV2 將以 jar 包的形式被分別放置在 ${Executor-Proxy_HOME}\ext\v1 和 ${Executor-Proxy_HOME}\ext\v2 目錄下,其中自訂的 ClassLoader 實現如下:
public class StandardExecutorClassLoader extends URLClassLoader { private final static String baseDir = System.getProperty("user.dir") + File.separator + "ext" + File.separator; public StandardExecutorClassLoader(String version) { super(new URL[] {}, null); // 將 Parent 設定為 null loadResource(version); } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { // 測試時可列印看一下 System.out.println("Class loader: " + name); return super.loadClass(name); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { return super.findClass(name); } catch(ClassNotFoundException e) { return StandardExecutorClassLoader.class.getClassLoader().loadClass(name); } } private void loadResource(String version) { String jarPath = baseDir + version; // 載入對應版本目錄下的 Jar 包 tryLoadJarInDir(jarPath); // 載入對應版本目錄下的 lib 目錄下的 Jar 包 tryLoadJarInDir(jarPath + File.separator + "lib"); } private void tryLoadJarInDir(String dirPath) { File dir = new File(dirPath); // 自動載入目錄下的jar包 if (dir.exists() && dir.isDirectory()) { for (File file : dir.listFiles()) { if (file.isFile() && file.getName().endsWith(".jar")) { this.addURL(file); continue; } } } } private void addURL(File file) { try { super.addURL(new URL("file", null, file.getCanonicalPath())); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }}
StandardExecutorClassLoader 在執行個體化時,會自動載入擴充目錄下與其lib目錄下的 jar 包,這裡之所以要載入 lib 目錄下的 jar,是為了載入擴充的依賴包。
有了StandardExecutorClassLoader,我們還需要一個調用各版本程式的代理類ExecutorPorxy,其實現如下:
import java.lang.reflect.Method;public class ExecutorProxy implements Executor { private String version; private StandardExecutorClassLoader classLoader; public ExecutorProxy(String version) { this.version = version; classLoader = new StandardExecutorClassLoader(version); } @Override public void execute(String name) { try { // Load ExecutorProxy class Class<?> executorClazz = classLoader.loadClass("Executor" + version.toUpperCase()); Object executorInstance = executorClazz.newInstance(); Method method = executorClazz.getMethod("execute", String.class); method.invoke(executorInstance, name); } catch (Exception e) { e.printStackTrace(); } }}
這裡是一個比較簡單的實現,因為通過反射調用的方法的參數是基本類型,在實際中,更多的可能是自訂的參數,那麼這時候則需要先通過自訂的 ClassLoader 載入其 Class,然後才能去擷取對應的方法,下面是一個省去內容相關的一個例子(不能直接運行):
public void call() throws IOException { try { // Load HBaseApi class Class<?> hbaseApiClazz = loadHBaseApiClass(); Object hbaseApiInstance = hbaseApiClazz.newInstance(); // Load parameter class Class<?> paramClazz = classLoader.loadClass(VO_PACKAGE_PATH + "." + sourceParame.getClass().getSimpleName()); // Transition parameter to targeParameter from sourceParameter Object targetParam = BeanUtils.transfrom(paramClazz, sourceParame); // Get function Method method = hbaseApiClazz.getMethod(methodName, paramClazz); // Invoke function by targetParam method.invoke(hbaseApiInstance, targetParam); } catch(ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); }}
3. 運行
將ExecutorV1 和 ExecutorV2分別打包,並將其打包後的 jar包與其依賴(lib目錄下)放入 Executor-Proxy 項目的 ext\v1 和 ext\v2 目錄下,在 Executor-Proxy 項目中則可以使用 Junit 進行測試:
public class ExecutorTest { @Test public void testExecuteV1() { Executor executor = new ExecutorProxy("v1"); executor.execute("TOM"); } @Test public void testExecuteV2() { Executor executor = new ExecutorProxy("v2"); executor.execute("TOM"); }}
列印結果最終分別如下:
execute testExecuteV1():V1:TOM
execute testExecuteV2():V2:TOM
4. 總結
總的來說,實現隔離允許指定 jar 包,主要需要做到以下幾點: 自訂 ClassLoader,使其 Parent = null,避免其使用系統內建的 ClassLoader 載入 Class。 在調用相應版本的方法前,更改當前線程的 ContextClassLoader,避免擴充包的依賴包通過Thread.currentThread().getContextClassLoader()擷取到非自訂的 ClassLoader 進行類載入 通過反射擷取 Method 時,如果參數為自訂的類型,一定要使用自訂的 ClassLoader 載入參數擷取 Class,然後在擷取 Method,同時參數也必須轉化為使用自訂的 ClassLoade 載入的類型(不同 ClassLoader 載入的同一個類不相等)
實際運用中,往往容易做到第一點或第三點,而忽略第二點,比如使用 HBase 相關包時。
當然,這隻是一種解決的方式,我們仍然可以使用微服務來達到同樣甚至更棒的效果,
以上。