一.使用方法及示例
簡介:當引入二方依賴包或三方依賴包時,可能出現外部依賴jar包與自己的工程需要依賴的衝突,或者多個二方三方依賴包互相沖突。這時候就需要一個隔離容器對他們進行隔離,其依賴的原理就是jvm認為不同classloader載入的類即使包名類名相同,也認為他們是不同的。sofa-ark將需要隔離的jar包打成plugin,對每個plugin都用獨立的classloader去載入。
(溫馨提示:若對sofa-ark不太瞭解的,最好先去看看官方文件,簡單瞭解下)
使用的基本步驟:
- 在會發生衝突的jar包的POM檔案加入sofa-ark提供的maven外掛,將其打成特定格式的jar包(plugin)。
- 在外部工程按照約定引入jar包。如果外部工程想打包成可執行的jar(fat-jar),還需要加入特定的maven外掛。
- 直接執行。
名詞解釋:
- Ark Container: Ark 容器,是元件 SOFAArk 的核心,執行 Ark 包時,Ark 容器會最先啟動,負責應用執行時的管理,主要包括構建 Ark Plugin 和 Ark Biz 的類匯入匯出關係表、啟動並初始化 Ark Plugin 和 Ark Biz、管理 Ark Plugin 服務的釋出和引用等等。
- Biz:即業務工程,該工程引用一個或多個外部jar包。
- plugin:會發生衝突的外部依賴jar包經過提供的Maven外掛打成的fat-jar包。執行時,由獨立的類載入器載入,因此有隔離需求的 Jar 包建議打包成 Ark Plugin 供應用依賴。
1.0 sofa-ark原理
在sofa-ark中,使用container容器啟動外部工程(Biz)和衝突jar包(plugin),sofa-ark的plugin maven外掛將衝突的jar包打包成為plugin,外部工程只能引用plugin exported出來的類,並且這個類是由獨立的PluginClassLoader
載入的,從而解決了jar包衝突的問題。
1.1 衝突示例
1.2 安裝sofa-ark
從sofa-ark官網 將整個工程下載下來,在最外層pom.xml所在路徑執行mvn package
和mvn install
將sofa-ark依賴jar包安裝到本地。
1.3 建立基礎依賴Jar包(衝突的包)
自己建立myjar工程,先後正常打包兩個版本安裝到本地maven倉庫。
如:v1版本
v2版本
1.4 建立service包(plugin)
建立如圖所示兩個工程:
在myJarservice-v1
和myJarservice-v2
的pom中分別引用之前寫的兩個基礎依賴jar包。然後在MyJarService1
和MyJarService2
中分別引用對應版本的myjar包裡的方法。
如MyJarService1.java:
MyJarService2.java
注意:這裡兩個Service引用的是不同版本的myjar。
接下來需要對兩個工程打包,和平常打包不一樣的是需要加入sofa-ark-plugin的maven外掛。兩個工程下的pom.xml都要加入:
<build>
<plugins>
<plugin>
<groupId>com.alipay.sofa</groupId>
<artifactId>sofa-ark-plugin-maven-plugin</artifactId>
<version>0.4.0-SNAPSHOT</version>
<executions>
<execution>
<id>default-cli</id>
<goals>
<goal>ark-plugin</goal>
</goals>
<configuration>
<!-- configure exported class -->
<exported>
<!-- configure class-level exported class -->
<classes>
<class>com.netease.sofaservice.MyJar1Service</class>
</classes>
</exported>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
複製程式碼
在exported
標籤裡寫出要對外提供的方法,外部要引用的所有方法都必須寫在這裡,可以以類(<classes>
)為單位和包(packages
)為單位匯出。
到parent工程路徑下mvn package
和mvn install
即可。
1.5 外部工程引用(Biz)
新建一個工程,在pom.xml引入以下依賴:
<dependencies>
<dependency>
<groupId>com.alipay.sofa</groupId>
<artifactId>sofa-ark-support-starter</artifactId>
<version>0.4.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.netease</groupId>
<artifactId>myjarservice-v1</artifactId>
<classifier>ark-plugin</classifier>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.netease</groupId>
<artifactId>myjarservice-v2</artifactId>
<classifier>ark-plugin</classifier>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.netease</groupId>
<artifactId>myjarservice-v1</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.netease</groupId>
<artifactId>myjarservice-v2</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
複製程式碼
注意要新增<classifier>
標籤,因為IDE識別不了ark-plugin的jar包,所以需要再引入一個範圍為provided
的jar包。
然後pom.xml還要新增maven外掛:
<build>
<plugins>
<plugin>
<groupId>com.alipay.sofa</groupId>
<artifactId>sofa-ark-maven-plugin</artifactId>
<version>0.4.0-SNAPSHOT</version>
<executions>
<execution>
<id>default-cli</id>
<!--goal executed to generate executable-ark-jar -->
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<!--specify destination where executable-ark-jar will be saved, default saved to ${project.build.directory}-->
<outputDirectory>./</outputDirectory>
<!--default none-->
<arkClassifier>executable-ark</arkClassifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
複製程式碼
然後再隨便寫個類引入兩個版本service包的類使用即可,在main函式入口處需要加上一句:
SofaArkBootstrap.launch(args);
可以看到列印結果:
二.sofa-ark-plugin-maven-plugin外掛原理分析
2.1 檢視plugin jar包內容
使用sofa-ark-plugin-maven-plugin
Maven外掛即可將jar包打包成可在container中隔離載入的jar包(如myjarservice-v1-1.0-ark-plugin.jarr和myjarservice-v2-1.0-ark-plugin.jar)。進入本地maven倉庫myJarservice-v1工程所在位置,可以看到maven打了兩個包:一個是maven自帶的外掛打的普通的包:myjarservice-v1-1.0.jar
,另一個是sofa-ark提供的maven外掛打的包myjarservice-v1-1.0-ark-plugin.jar
,開啟myjarservice-v1-1.0-ark-plugin.jar
可以看到如下目錄:
相關目錄說明:
com/alipay/sofa/ark/plugin/mark
:標記檔案,標記該 Jar 包是sofa-ark-plugin-maven-plugin
打包生成的Ark Plugin
檔案。META-INF/MANIFEST.MF
:記錄外掛元資訊,其中包含要匯出的和匯入的類。
Manifest-Version: 1.0
groupId: com.netease
artifactId: myjarservice-v1
version: 1.0
priority: 100
pluginName: myjarservice-v1
activator:
import-packages:
import-classes:
import-resources:
export-packages:
export-classes: com.netease.sofaservice.MyJar1Service
export-resources:
複製程式碼
conf/export.index
:外掛匯出類索引檔案;為了避免在執行時計算MANIFEST.MF
中export-packages
下面具體的匯出類,在打包生成Ark Plugin
時,會生成外掛所有匯出類的索引檔案,縮短Ark Container
解析配置時間。lib/
: lib 目錄存放外掛工程依賴的普通 Jar 包,一般包含外掛需要和其他外掛或者業務有隔離需求的 Jar 包;外掛配置的匯出類都包含在這些 Jar 包中。
2.2 分析sofa-ark-plugin-maven-plugin原始碼
進入下載的sofa-ark原始碼中的ark-plugin-maven-plugin
工程,可以看到ArkPluginMojo繼承了
@Mojo(name = "ark-plugin", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.RUNTIME)
public class ArkPluginMojo extends AbstractMojo {
@Parameter(defaultValue = "${project.artifactId}")
public String pluginName;
@Parameter(defaultValue = "100", property = "sofa.ark.plugin.priority")
protected Integer priority;
@Parameter
protected String activator;
@Parameter
protected ExportConfig exported;
@Parameter
protected ImportConfig imported;
...
複製程式碼
@Mojo
就是一個 goal,可以繫結到某個 phase
(這裡是package)執行,@Parameter 是從外面傳進來的引數,可以直接獲取xml中配置的引數。Maven外掛標準要求必須重寫execute()
方法,外掛執行時主要就是執行execute()
。
@Override
public void execute() throws MojoExecutionException
{
Archiver archiver;//zip歸檔
archiver = getArchiver();
outputDirectory.mkdirs();
String fileName = getFileName();
File destination = new File(outputDirectory, fileName);
archiver.setDestFile(destination);
Set<Artifact> artifacts = project.getArtifacts();
artifacts = filterExcludeArtifacts(artifacts);
Set<Artifact> conflictArtifacts = filterConflictArtifacts(artifacts);
addArkPluginArtifact(archiver, artifacts, conflictArtifacts);
addArkPluginConfig(archiver);
archiver.createArchive();
projectHelper.attachArtifact(project, destination, getClassifier());
}
複製程式碼
這裡的將execute()
方法進行了精簡,這個方法主要做了一下幾件事情:
- 建立一個zip格式的歸檔,用來儲存引入的jar包和其他檔案,建立輸出路徑。
- 獲取引入的所有依賴(Artifacts),並且將需要exclude的包排除出去。
- 將所有依賴寫入zip歸檔中的lib目錄下
- 將配置資訊寫入zip歸檔中,包括之前提到的
export.index
,MANIFEST.MF
,mark
經過上述步驟後即把依賴的dependence和配置檔案都寫入zip中了,然後將其轉換為jar字尾即可。
三. Sofa-ark原理分析
3.1 初始化ArkContainer
前面講解了plugin外掛如何工作,這節講述外部工程是如何引用運用plugin外掛打包而成的plugin jar包來解決隔離衝突的。可以從main方法加入的SofaArkBootstrap.launch(args)
進行單步跟蹤,這句程式碼主要是將Container啟動起來,然後讓Container去載入Plugin和Biz。在launch
方法裡通過反射呼叫了SofaArkBootstrap
的remain
方法,在remain
方法裡主要乾了兩件事:
private static void remain(String[] args) throws Exception {// NOPMD
URL[] urls = getURLClassPath();
new ClasspathLauncher(new ClassPathArchive(urls)).launch(args, getClasspath(urls),
entryMethod.getMethod());
}
複製程式碼
- 獲取classpath下的所有jar包,包括jdk自己的jar包和maven引入的jar包。
- 將所有依賴jar包和自己寫的啟動類及其main函式以url的形式傳入
ClasspathLauncher
,ClasspathLauncher
反射呼叫ArkContainer
的main
方法,並且使用ContainerClassLoader
載入ArkContainer
。至此,就開始啟動ArkContainer了。
3.2 啟動ArkContainer
接著就執行到了ArkContainer
中的main方法,傳入的引數args即之前ClasspathLauncher
傳入的url
public static Object main(String[] args) throws ArkException
{
//使用LaunchCommand將傳入的引數按型別分類
LaunchCommand launchCommand = LaunchCommand.parse(args[ARK_COMMAND_ARG_INDEX], Arrays.copyOfRange(args, MINIMUM_ARGS_SIZE, args.length));
//ClassPathArchive將傳入依賴的Jar包分類,並提供獲得plugin和biz的filter方法
ClassPathArchive classPathArchive = new ClassPathArchive(launchCommand.getEntryClassName(), launchCommand.getEntryMethodName(), launchCommand.getEntryMethodDescriptor(), launchCommand.getClasspath());
return new ArkContainer(classPathArchive, launchCommand).start();
}
}
複製程式碼
這個方法主要做了一下幾件事:
- 使用
LaunchCommand
將傳入的引數分類,將classpath的url和自己寫的啟動類的main方法提取出來 - 將
LaunchCommand
傳入ArkContainer
並啟動:
在ArkContainer.start()
中:
public Object start() throws ArkException
{
if (started.compareAndSet(false, true))
{
arkServiceContainer.start();
Pipeline pipeline = arkServiceContainer.getService(Pipeline.class);
pipeline.process(pipelineContext);
}
return this;
}
複製程式碼
arkServiceContainer
中包含了一些Container啟動前需要執行的Service,這些Service被封裝到一個個的PipelineStage
中,這些PipelineStage
又被封裝成List到一個pipeline
中。主要包含這麼幾個PipelineStage
,依次執行:
HandleArchiveStage
篩選所有第三方jar包中含有mark標記的plugin jar,說明這些jar是sofa ark maven外掛打包成的需要隔離的jar。從jar中的export.index中提取需要隔離的類,把他們加入一個PluginList
中,並給每個plugin,分配一個獨立的PluginClassLoader
。同時以同樣的操作給Biz也分配一個BizClassLoader
DeployPluginStage
建立一個map,key是需要隔離的類,value是這個載入這個類使用的PluginClassLoader例項。DeployBizStage
使用BizClassLoader反射呼叫Biz的main方法。
至此,Container就啟動完了。後面再呼叫需要隔離的類時,由於啟動Biz的執行緒已經被換成了BizClassLoader,在loadClass時BizClassLoader會首先看看在DeployPluginStage
建立的Map中是否有PluginClassLoader能載入這個類,如果能就委託PluginClassLoader載入。就實現了不同類使用不同的類載入器載入。