1. 程式人生 > >阿里再開源!模組化開發框架JarsLink

阿里再開源!模組化開發框架JarsLink

JarsLink (原名Titan) 是一個基於JAVA的模組化開發框架,它提供在執行時動態載入模組(一個JAR包)、解除安裝模組和模組間呼叫的API。也是阿里巴巴的開源專案之一 https://github.com/alibaba/jarslink,目前在微貸事業群廣泛使用。

需求背景

  • 應用拆分的多或少都有問題。多則維護成本高,每次釋出一堆應用。少則拆分成本高,無用功能很難下線。
  • 故障不隔離。當一個系統由多人同時參與開發時,修改A功能,可能會影響B功能,引發故障。
  • 多分支開發引發衝突。多分支開發完之後合併會產生衝突。
  • 牽一髮動全身。一處核心程式碼的改動,或一個基礎Jar的升級需要回歸整個系統。
  • 升級和遷移成本高。中介軟體升級每個應用都有升級成本。

模組化開發的好處

  • 可插拔,一個應用由多個模組組成,應用裡的模組可拆和合,模組可快速在多個系統中遷移和部署。
  • 模組化開發,模組之間互相隔離,實現故障隔離。
  • 一個模組一個分支,不會引發程式碼衝突。
  • 在模組中增加或修改功能,只會影響當前模組,不會影響整個應用。
  • 動態部署,在執行時把模組部署到應用中,快速修復故障,提高發布效率。
  • 多版本部署,可以在執行時同時部署某個模組的新舊版本,進行AB TEST。
  • 減少資源消耗,通過部署模組的方式減少應用數量和機器數量。

JarsLink的應用場景

  • 微服務整合測試, 目前一個微服務是一個FAT JAR,如果有幾十個微服務,則需要啟動很多程序,DEBUG埠會很多,使用JarsLink框架合併FAT JAR,再路由請求到其他JAR,就可以只啟動一個程序進行DEBUG測試。
  • 資料管理中心,資料採集的資料來源多,而且每種資料來源都需要對接和開發,通過模組化開發,實現一個數據源使用一個模組進行對接。
  • 指標計算系統,每個TOPIC一個模組,把訊息轉發到模組中進行訊息處理。
  • 後臺管理系統,幾乎每個系統都有後臺開發的需求,新建應用則應用數多,維護成本高,引入模組化開發,一個二級域一個模組來開發後臺功能。

目前螞蟻金服微貸事業部幾個系統和幾十個模組已經使用JarsLink框架。

JarsLink的特性

隔離性

  • 類隔離:框架為每個模組的Class使用單獨的ClassLoader來載入,每個模組可以依賴同一種框架的不同的版本。
  • 例項隔離:框架為每個模組建立了一個獨立的Spring上下文,來載入模組中的BEAN,例項化失敗不會影響其他模組。
  • 資源隔離:後續會支援模組之間的資源隔離,每個模組使用獨立的CPU和記憶體資源。

動態性

  • 動態釋出:模組能在執行時動態載入到系統中,實現不需要重啟和釋出系統新增功能。支援突破雙親委派機制,在執行時載入父載入器已經載入過的類,實現模組升級依賴包不需要系統釋出。
  • 動態解除安裝:模組能在執行時被動態解除安裝乾淨,實現快速下線不需要功能。

易用性

提供了通用靈活的API讓系統和模組進行互動。

實現原理

模組載入

TITAN為每個模組建立一個新的URLClassLoader來載入模組。並且支援突破雙親委派,設定了overridePackages的包將由子類載入進行載入,不優先使用父類載入器已載入的。

模組的解除安裝

解除安裝模組需要滿足三個條件

  • 模組裡的例項物件沒有被引用
  • 模組裡的Class沒有被引用
  • 類載入器沒有被引用

所以需要做到三點解除安裝例項,解除安裝類和解除安裝類載入器,整個模組的解除安裝順序如下:

  • 關閉資源:關閉HTTP連線池或執行緒池。
  • 關閉IOC容器:呼叫applicationContext.close()方法關閉IOC容器。
  • 移除類載入器:去掉模組的引用。
  • 解除安裝JVM租戶(開發中):解除安裝該模組使用的JVM租戶,釋放資源。

模組間隔離

模組化開發需要解決隔離性問題,否則各模組之間會互相影響。模組之間的隔離有三個層次:

  • 類隔離:為每個模組建立一個類載入器來實現類隔離。
  • 例項隔離:為每個模組建立一個新的IOC容器來載入模組裡面的BEAN。
  • 資源隔離:對每個模組只能使用指定的CPU和記憶體。

目前JarsLink實現了類隔離和例項隔離,資源隔離準備引入ALIJVM多租戶來解決。

模組間通訊

模組之間的通訊也有三種方式,RPC,本地呼叫,深克隆/反射。

  • 本地呼叫:目前TITAN的doAction就是使用的這種通訊方式,這種方式要求模組的類載入器是父子關係,且IOC容器也是父子容器。
  • RPC呼叫:用於跨JVM的模組之間呼叫,利用SOFA 4動態API在模組中釋出和引用TR服務來實現。
  • 深克隆/反射:深克隆其他模組的入參,反射其他模組的方法實現呼叫。

類載入機制

OSGI類載入機制的關係採用的是網狀結構,每個模組通過 Export-Package 來宣告我要給別人用哪些類,通過 Import-Package來宣告我要用別人的哪些類。而TITAN框架採用的是扁平化管理,每個模組都有一個共同的父類,這個父類載入器就是載入ModuleLoader類的載入器,好處是便於維護,每個模組的類做到充分隔離,缺點是會載入重複的Class,適用於模組較少的場景。

JarsLink框架類圖

JarsLink框架的類圖如下:

RTJftyUrzrScdfQAZNKi

  • AbstractModuleRefreshScheduler:入口類,負責定期掃描本地和記憶體中的模組是否發生變更,如果變更,則更新模組。
  • ModuleLoader:模組載入引擎,負責模組載入。
  • ModuleManager:模組管理者,負責在執行時註冊,解除安裝,查詢模組和執行Action。
  • Module:模組,一個模組有多個Action。
  • Action:模組裡的執行者。

如何使用

1:引入POM

<dependency>
  <groupId>com.alipay.jarslink</groupId>
  <artifactId>jarslink-api</artifactId>
  <version>1.5.0.20180213</version>
</dependency>    

JarsLink依賴的POM也需要引入

    <properties>
        <slf4j.version>1.7.7</slf4j.version>
        <apache.commons.lang.version>2.6</apache.commons.lang.version>
        <apache.commons.collections.version>3.2.1</apache.commons.collections.version>
    </properties>
    

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${org.springframework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${org.springframework.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>${apache.commons.lang.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>${apache.commons.collections.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>17.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

2:引入jarslink BEAN

在系統中引入以下兩個BEAN。

<!-- 模組載入引擎 -->
<bean name="moduleLoader" class="com.alipay.jarslink.api.impl.ModuleLoaderImpl"></bean>
<!-- 模組管理器 -->
<bean name="moduleManager" class="com.alipay.jarslink.api.impl.ModuleManagerImpl"></bean>

3:整合JarsLink API

使用JarsLink API非常簡單,只需要繼承AbstractModuleRefreshScheduler,並提供模組的配置資訊,程式碼如下:

public class ModuleRefreshSchedulerImpl extends AbstractModuleRefreshScheduler {

    @Override
    public List<ModuleConfig> queryModuleConfigs() {
        return ImmutableList.of(ModuleManagerTest.buildModuleConfig());
    }
    
      public static ModuleConfig buildModuleConfig() {
        URL demoModule = Thread.currentThread().getContextClassLoader().getResource("META-INF/spring/demo-1.0.0.jar");
        ModuleConfig moduleConfig = new ModuleConfig();
        moduleConfig.setName("demo");
        moduleConfig.setEnabled(true);
        moduleConfig.setVersion("1.0.0.20170621");
        moduleConfig.setProperties(ImmutableMap.of("svnPath", new Object()));
        moduleConfig.setModuleUrl(ImmutableList.of(demoModule));
        return moduleConfig;
    }

這個排程器在bean初始化的時候會啟動一個排程任務,每分鐘重新整理一次模組,如果模組的版本號發生變更則會更新模組。實現這個方法時,必須把模組(jar包)下載到機器本地,模組的配置資訊說明如下:

  • name:全域性唯一,建議使用英文,忽略大小寫。
  • enabled:當前模組是否可用,預設可用,解除安裝模組時可以設定成false。
  • version:模組的版本,如果版本號和之前載入的不一致,框架則會重新載入模組。
  • Properties:spring屬性配置檔案。
  • moduleUrl:模組的本地存放地址。
  • overridePackages:需要突破雙親委派的包名,一般不推薦使用,範圍越小越好,如com.alipay.XX。

ModuleRefreshSchedulerImpl類註冊成Spring的bean。

    <bean id="moduleRefreshScheduler"
          class="com.alipay.**.ModuleRefreshSchedulerImpl">
        <property name="moduleManager" ref="moduleManager" />
        <property name="moduleLoader" ref="moduleLoader" />
    </bean>

JarsLink API 暫時不提供模組視覺化管理能力,所以需要使用其他系統來管理和釋出模組。目前可以通過com.alipay. jarslink.api.ModuleManager#getModules獲取執行時所有模組的資訊。

你也可以使用API來載入並註冊模組,詳細使用方式可以參考ModuleManagerTest,程式碼如下。

        //1:載入模組
        Module module = moduleLoader.load(buildModuleConfig());

        //2:註冊模組
        ModuleManager moduleManager = new ModuleManagerImpl();
        moduleManager.register(module);

3:開發模組

在模組中只需要實現並開發Action,程式碼如下:


public class HelloWorldAction implements Action<ModuleConfig, ModuleConfig> {

    @Override
    public ModuleConfig execute(ModuleConfig actionRequest) {
        ModuleConfig moduleConfig = new ModuleConfig();
        moduleConfig.setName(actionRequest.getName());
        moduleConfig.setEnabled(actionRequest.getEnabled());
        moduleConfig.setVersion(actionRequest.getVersion());
        moduleConfig.setModuleUrl(actionRequest.getModuleUrl());
        moduleConfig.setProperties(actionRequest.getProperties());
        moduleConfig.setOverridePackages(actionRequest.getOverridePackages());
        return moduleConfig;
    }

    @Override
    public String getActionName() {
        return "helloworld";
    }

}

5:呼叫介面

開發者需要利用JarsLink API把請求轉發給模組,先根據模組名查詢模組,再根據aciton name查詢Action,最後執行Action。

//查詢模組
Module findModule = moduleManager.find(module.getName());
Assert.assertNotNull(findModule);

//查詢和執行Action
String actionName = "helloworld";
ModuleConfig moduleConfig = new ModuleConfig();
moduleConfig.setName("h");
moduleConfig.setEnabled(true);
ModuleConfig result = findModule.doAction(actionName, moduleConfig);

其他特性

Spring配置

通過moduleConfig的Properties屬性可以設定Spring bean變數的配置資訊。

1:定義變數

<bean id="userService" class="com.alipay.XX.UserService">
 <property name="url" value="${url}" />
</bean>

2:配置變數資訊

        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("url", "127.0.0.1");
        moduleConfig.setProperties(properties);

3:排除spring配置檔案

        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("exclusion_confige_name", "text.xml");
        moduleConfig.setProperties(properties);

排除多個檔案用逗號分隔。

最佳實踐

HTTP請求轉發

可以把HTTP請求轉發給模組處理。


private ModuleManager moduleManager;

@RequestMapping(value = "module/{moduleName}/{actionName}/process.json", method = { RequestMethod.GET,RequestMethod.POST })
public Object process(HttpServletRequest request, HttpServletResponse response) {

    Map<String, String> pathVariables = resolvePathVariables(request);

    String moduleName = pathVariables.get("moduleName").toUpperCase()
    String actionName = pathVariables.get("actionName").toUpperCase()
    String actionRequest = XXX;
    return moduleManager.doAction(moduleName,
            actionName, actionRequest);
}


private Map<String, String> resolvePathVariables(HttpServletRequest request) {
        return (Map<String, String>) request
            .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
    }

訊息請求轉發

可以把訊息轉發給模組進行處理。遵循預設大於配置的方式,你可以把TOPIC當做模組名,EventCode當做ActionName來轉發請求。

介面說明

JarsLink框架最重要的兩個介面是ModuleManager和ModuleLoader。

ModuleManager介面

ModuleManager負責註冊,解除安裝,查詢模組和執行Action。


import java.util.List;
import java.util.Map;

/**
 * 模組管理者, 提供註冊,移除和查詢模組能力
 *
 * @author tengfei.fangtf
 * @version $Id: ModuleManager.java, v 0.1 2017年05月30日 2:55 PM tengfei.fangtf Exp $
 */
public interface ModuleManager {

    /**
     * 根據模組名查詢Module
     * @param name
     * @return
     */
    Module find(String name);

    /**
     * 獲取所有已載入的Module
     *
     * @return
     */
    List<Module> getModules();

    /**
     * 註冊一個Module
     *
     * @param module 模組
     * @return 新模組
     */
    Module register(Module module);

    /**
     * 移除一個Module
     *
     * @param name 模組名
     * @return 被移除的模組
     */
    Module remove(String name);

    /**
     * 獲取釋出失敗的模組異常資訊
     *
     * @return
     */
    Map<String, String> getErrorModuleContext();

}

ModuleLoader介面

ModuleLoader只負責載入模組。

public interface ModuleLoader {

    /**
     * 根據配置載入一個模組,建立一個新的ClassLoadr載入jar裡的class
     *
     * @param moduleConfig 模組配置資訊
     *
     * @return 載入成功的模組
     */
    Module load(ModuleConfig moduleConfig);

}


方 騰飛

花名清英,併發網(ifeve.com)創始人,暢銷書《Java併發程式設計的藝術》作者,螞蟻金服技術專家。目前工作於支付寶微貸事業部,關注網際網路金融,併發程式設計和敏捷實踐。微信公眾號aliqinying。