【JAVA】自定義類載入器實現類隔離

kamier發表於2023-03-04

一、背景

某服務需要連線操作多種元件每種元件可能有多個版本),如kafka、mongodb、es、mysql等等,並且後續需要適配更多的元件。
image.png

主要難點:連線操作多元件多版本,且同種元件的不同版本所依賴的jar包可能不一樣,操作原始碼也可能發生改變,專案無法直接依賴jar包,會產生類衝突

二、解決思路

由於每種元件的不同版本所依賴的jar包不同,我們可以借鑑tomcat的實現方式,透過自定義類載入器打破雙親委派機制來實現類隔離,從而達到操作多元件多版本的目的。
image.png

2.1 建立依賴所在目錄

針對每一種元件我們建立一個目錄,比如/data/kafka、/data/mongodb、/data/es等,且每種元件的不同版本建立對應的子目錄,比如/data/kafka/0.10、/data/kafka/0.11,目錄結構如下

| ----/data
| --------/kafka
| ------------/0.10
| ------------/0.11
| --------/mysql
| ------------/5.7
| ------------/8.0
| ...

把每種元件不同版本對應的依賴包放在各個子目錄下面。

2.2 定義操作介面

在common公共模組中定義一個介面AbstractOperator,該介面定義一些通用方法,如下:

public interface Operator {
    /**
     * 測試連線
     * @param connectionInfo
     * @return
     */
    boolean testConnection(String connectionInfo);

    /**
     * 獲取元件版本
     * @return
     */
    String getVersion(String connectionInfo);
}

再定義各種元件的介面,如KafkaOperator、MysqlOperator等,使其繼承該通用介面。元件介面內部包含一些元件自身的操作,如KafkaOperator中定義了getTopics、createTopic、deleteTopic等方法。程式碼如下:

public interface KafkaOperator extends Operator{
    /**
     * 獲取topic列表
     * @param connectionInfo
     * @return
     */
    List<String> getTopics(String connectionInfo);

    /**
     * 建立topic
     * @param connectionInfo
     * @param topic
     * @return
     */
    boolean createTopic(String connectionInfo, String topic);

    /**
     * 刪除topic
     * @param connectionInfo
     * @param topic
     * @return
     */
    boolean deleteTopic(String connectionInfo, String topic);
}

2.3 編寫並構建業務包

大致步驟如下:

  1. 針對每種元件的不同版本,可以在專案下新建一個模組,該模組依賴common公共模組
  2. 建立入口類com.kamier.Entry(所有元件的不同版本的入口類的全限定名統一為com.kamier.Entry),並實現對應的元件介面,比如Kafka的0.10版本,那麼就實現KafkaOperator介面。
    image.png
  3. 編寫業務邏輯程式碼
public class Entry implements KafkaOperator {
    @Override
    public List<String> getTopics(String connectionInfo) {
        return null;
    }

    @Override
    public boolean createTopic(String connectionInfo, String topic) {
        return false;
    }

    @Override
    public boolean deleteTopic(String connectionInfo, String topic) {
        return false;
    }

    @Override
    public boolean testConnection(String connectionInfo) {
        return false;
    }

    @Override
    public String getVersion(String connectionInfo) {
        return null;
    }
}
  1. 打成jar包
  2. 將jar包放在對應的目錄下,與依賴包同級,如/data/kafka/0.10

2.4 自定義類載入器

經過前面的準備工作,元件的每個版本的目錄下都有了相應的依賴包和業務包。
開始編寫一個自定義類載入器繼承URLClassLoader,重寫loadClass方法,優先載入當前類載入器路徑下的class來打破雙親委派模式,程式碼如下

public static class MyClassLoader extends URLClassLoader {

        public MyClassLoader(URL[] urls) {
            super(urls);
        }

        public Class<?> loadClass(String name) throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                // 先檢查當前類載入器是否已經裝載該類
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    try {
                        // 在當前類載入器的路徑下查詢
                        c = findClass(name);
                    } catch (ClassNotFoundException e) {
                        // 說明在當前類載入器的路徑下沒有找到
                    }

                    if (c == null) {
                        // 走雙親委派機制
                        if (getParent() != null) {
                            c = getParent().loadClass(name);
                        }
                    }
                }
                return c;
            }
        }
    }

針對每種元件的不同版本,我們建立與其對應的自定義類載入器,並將該版本對應目錄下的所有jar包(包括依賴包和業務包)的URL傳入。

2.5 主流程步驟

步驟如下:

  1. 當我們從頁面上接收到一個獲取Kafka(版本為0.10)topic列表的請求時,先判斷是否已經初始化過Kafka(0.10版本)的類載入器,如果還未初始化,則進行類載入器的初始化
URL[] urls = null;
File dir = new File("/data/kafka/0.10");
if (dir.isDirectory()) {
    File[] files = dir.listFiles();
    urls = new URL[files.length];
    for (int i = 0; i < files.length; i++) {
        urls[i] = files[i].toURL();
    }
}

MyClassLoader contextClassLoader = new MyClassLoader(urls);
  1. 透過類載入器載入入口類com.kamier.Entry並例項化,透過反射呼叫對應的方法(元件與其對應的方法列表可以統一維護在資料庫中)。
Class loadClass = contextClassLoader.loadClass("com.kamier.Entry");
Object entry = loadClass.newInstance();
Method method = loadClass.getDeclaredMethod("getTopics");
List<String> a = (List) method.invoke(entry, 引數);
  1. 獲取到結果並返回

三、總結

至此整個實現步驟就結束了,我們透過自定義類載入器的方式來實現類隔離,從而達到操作多元件多版本的目的。

相關文章