阿里再開源!模組化開發框架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框架的類圖如下:
- 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);
}