Java 自訂 ClassLoader 實現隔離運行不同版本jar包的方式__classloader

來源:互聯網
上載者:User
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 相關包時。

當然,這隻是一種解決的方式,我們仍然可以使用微服務來達到同樣甚至更棒的效果,

以上。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.