SOFAArk-01-蚂蚁金服类隔离工具 SOFAArk 入门及源码讲解
情境导入
你是否遇到过包冲突问题?又是如何解决的?
有些项目都是多年的历史“遗留财产”,老马甚至还遇到过一个应用中有 3 个不同版本的 spring,只能说能跑起来就是奇迹。
不过有时候会进行各种版本升级,然后会发现各种版本冲突,浪费时间在排除各种版本冲突的问题上。
那有没有一种方法,可以帮助我们更好的解决包冲突呢?

今天就让我们一起学习下蚂蚁金服开源的利器——SOFAArk。
SOFAArk
SOFAArk 是一款基于 Java 实现的轻量级类隔离容器,主要提供类隔离和应用(模块)合并部署能力,由蚂蚁金服公司开源贡献;
在大型软件开发过程中,通常会推荐底层功能插件化,业务功能模块化的开发模式,以期达到低耦合、高内聚、功能复用的优点。
特性
基于此,SOFAArk 提供了一套较为规范化的插件化、模块化的开发方案,产品能力主要包括:
定义类加载模型,运行时底层插件、业务应用(模块)之间均相互隔离,单一插件和应用(模块)由不同的 ClassLoader 加载,可以有效避免相互之间的包冲突,提升插件和模块功能复用能力;
定义插件开发规范,提供 maven 打包工具,简单快速将多个二方包打包成插件(Ark Plugin,以下简称 Plugin)
定义模块开发规范,提供 maven 打包工具,简单快速将应用打包成模块 (Ark Biz,以下简称 Biz)
针对 Plugin、Biz 提供标准的编程界面,包括服务、事件、扩展点等机制
支持多 Biz 的合并部署,开发阶段将多个 Biz 打包成可执行 Fat Jar,或者运行时使用 API 或配置中心(Zookeeper)动态地安装卸载 Biz
基于以上能力,SOFAArk 可以帮助解决依赖包冲突、多应用(模块)合并部署等场景问题。
classloader 加载
jvm认为不同classloader加载的类即使包名类名相同,也认为他们是不同的。
sofa-ark将需要隔离的jar包打成plugin,对每个plugin都用独立的classloader去加载。

快速入门
定义 2 个不同版本的 jar
为了模拟不同版本之间的冲突,你可以自己定义 2 个不同版本的 jar 安装到本地,也可以直接使用常用的一些工具包进行模拟。
我这里直接使用了自己的一个工具包:
com.github.houbb
heaven
${heaven.version}
项目结构
一共下面 3 个模块:
sofaark-learn-serviceone
sofaark-learn-servicetwo
sofaark-learn-run
我们让 serviceone 和 servicetwo 依赖不同的 heaven 版本,然后在 run 模块中同时依赖二者,模拟 jar 版本冲突。
serviceone
- pom.xml
指定依赖了 0.0.1 版本的 heaven。
sofa-ark-plugin-maven-plugin
是为了将当前模块打包成为 ark-plugin。
sofaark-learn
org.example
1.0-SNAPSHOT
4.0.0
sofaark-learn-serviceone
com.github.houbb
heaven
0.0.1
com.alipay.sofa
sofa-ark-plugin-maven-plugin
0.6.0
default-cli
ark-plugin
com.github.houbb.sofaark.learn.serviceone.ServiceOne
- ServiceOne.java
服务定义比较简单,输出一下当前类的 classloader。
package com.github.houbb.sofaark.learn.serviceone;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class ServiceOne {
public static void say() {
System.out.println("v0.0.1 classloader:" + ServiceOne.class.getClassLoader());
}
}
servicetwo
这个和 serviceone 基本一样,只是依赖的 heaven 版本不同。
- pom.xml
sofaark-learn
org.example
1.0-SNAPSHOT
4.0.0
sofaark-learn-servicetwo
com.github.houbb
heaven
0.1.120
com.alipay.sofa
sofa-ark-plugin-maven-plugin
0.6.0
default-cli
ark-plugin
com.github.houbb.sofaark.learn.servicetwo.ServiceTwo
- ServiceTwo.java
package com.github.houbb.sofaark.learn.servicetwo;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class ServiceTwo {
public static void say() {
System.out.println("v0.1.120 classloader:" + ServiceTwo.class.getClassLoader());
}
}
run
这个模块会依赖二者的实现。
- pom.xml
sofaark-learn
org.example
1.0-SNAPSHOT
4.0.0
sofaark-learn-run
${project.groupId}
sofaark-learn-serviceone
${project.version}
compile
${project.groupId}
sofaark-learn-servicetwo
${project.version}
compile
${project.groupId}
sofaark-learn-serviceone
${project.version}
ark-plugin
${project.groupId}
sofaark-learn-servicetwo
${project.version}
ark-plugin
com.alipay.sofa
sofa-ark-support-starter
0.6.0
com.alipay.sofa
sofa-ark-maven-plugin
0.6.0
default-cli
repackage
./
executable-ark
注意这里的 ark-plugin
,实际上是引入了上面编译后的 ark-plugin,为了让 idea 识别。
plugins 中的 executable-ark
为了将当前的模块打包成为一个可以执行的 ark 包。
- Main.java
运行的方法如下:
package com.github.houbb.sofaark.learn.run;
import com.alipay.sofa.ark.support.startup.SofaArkBootstrap;
import com.github.houbb.sofaark.learn.serviceone.ServiceOne;
import com.github.houbb.sofaark.learn.servicetwo.ServiceTwo;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class Main {
public static void main(String[] args) {
SofaArkBootstrap.launch(args);
System.out.println("Main classloader: " + Main.class.getClassLoader());
ServiceOne.say();
ServiceTwo.say();
}
}
我们需要指定 SofaArkBootstrap.launch(args);
,让 ark 启动生效。
这样整个入门流程就完成了,对应的日志如下:
Main classloader: com.alipay.sofa.ark.container.service.classloader.BizClassLoader@1cec3a6
v0.0.1 classloader:com.alipay.sofa.ark.container.service.classloader.BizClassLoader@1cec3a6
v0.1.120 classloader:com.alipay.sofa.ark.container.service.classloader.BizClassLoader@1cec3a6
Ark container started in 2894 ms.
可以发现,所有的 classloader 都变成了 ark 对应的容器 BizClassLoader。
接下来,我们可以继续学习一下,这背后的原理。

sofa-ark-plugin-maven-plugin 插件原理
这 3 个模块中,都反复出现一个核心插件:sofa-ark-plugin-maven-plugin。
这个插件做了什么?
最好的答案就在源码之中,我们可以到 sofa-ark-plugin 查看对应的源码。
ArkPluginMojo
ark-plugin 核心实现类 ArkPluginMojo 定义如下:
@Mojo(name = "ark-plugin", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.RUNTIME)
public class ArkPluginMojo extends AbstractMojo {}
这里通过注解 @Mojo
定义了 ark-plugin,并将其生效的阶段绑定为 package 打包阶段。
execute 方法
也就是每次执行 mvn package 时,会执行其对应的 execute 方法进行处理。
核心实现精简如下:
@Override
public void execute() throws MojoExecutionException {
Archiver archiver = getArchiver();
String fileName = getFileName();
File destination = new File(outputDirectory, fileName);
File tmpDestination = new File(outputDirectory, getTempFileName());
archiver.setDestFile(tmpDestination);
Set artifacts = project.getArtifacts();
artifacts = filterExcludeArtifacts(artifacts);
Set conflictArtifacts = filterConflictArtifacts(artifacts);
addArkPluginArtifact(archiver, artifacts, conflictArtifacts);
addArkPluginConfig(archiver);
archiver.createArchive();
shadeJarIntoArkPlugin(destination, tmpDestination, artifacts);
if (isAttach()) {
if (StringUtils.isEmpty(classifier)) {
Artifact artifact = project.getArtifact();
artifact.setFile(destination);
project.setArtifact(artifact);
} else {
projectHelper.attachArtifact(project, destination, classifier);
}
}
}
这个方法主要做了下面几步:
建立一个zip格式的归档,用来保存引入的jar包和其他文件,建立输出路径。
获取引入的所有依赖(Artifacts),并且将需要exclude的包排除出去。
将所有依赖写入zip归档中的lib目录下
将配置信息写入zip归档中,包括 export.index,MANIFEST.MF,mark 等
SofaArkBootstrap ark 引导类

初始化 Ark Container
我们使用的方式,和普通的 main() 方法相比,就是多了一句 SofaArkBootstrap.launch(args);
对应的源码如下:
public static void launch(String[] args) {
try {
// ark 是否已经启动
// 直接 debug 可以发现,会进入到判断之中
if (!isSofaArkStarted()) {
//
entryMethod = new EntryMethod(Thread.currentThread());
IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(
entryMethod.getDeclaringClassName());
//MAIN_ENTRY_NAME 对应的方法名称为 remain,实际上这里就是一个反射调用 remain()
LaunchRunner launchRunner = new LaunchRunner(SofaArkBootstrap.class.getName(),
MAIN_ENTRY_NAME, args);
Thread launchThread = new Thread(threadGroup, launchRunner,
entryMethod.getMethodName());
launchThread.start();
LaunchRunner.join(threadGroup);
threadGroup.rethrowUncaughtException();
System.exit(0);
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
核心目的:
(1)将 ark container 启动起来
(2)让 ark container 加载 ark-plugin 和 ark-biz
- isSofaArkStarted ark 是否已经启动
实现如下:
private static boolean isSofaArkStarted() {
Class bizClassLoader = SofaArkBootstrap.class.getClassLoader().getClass();
return BIZ_CLASSLOADER.equals(bizClassLoader.getCanonicalName());
}
- remain()
实现如下:
private static void remain(String[] args) throws Exception {// NOPMD
AssertUtils.assertNotNull(entryMethod, "No Entry Method Found.");
URL[] urls = getURLClassPath();
new ClasspathLauncher(new ClassPathArchive(entryMethod.getDeclaringClassName(),
entryMethod.getMethodName(), urls)).launch(args, getClasspath(urls),
entryMethod.getMethod());
}
作用:
获取classpath下的所有jar包,包括jdk自己的jar包和maven引入的jar包。
将所有依赖jar包和自己写的启动类及其main函数以url的形式传入ClasspathLauncher,ClasspathLauncher反射调用ArkContainer的main方法,并且使用ContainerClassLoader加载ArkContainer。
至此,就开始启动ArkContainer了。
启动 ArkContainer
接着就运行到了ArkContainer中的main方法,传入的参数args即之前 ClasspathLauncher 传入的url。
ClasspathLauncher 继承自 ArkLauncher,实现如下:
public class ArkLauncher extends BaseExecutableArchiveLauncher {
public final String SOFA_ARK_MAIN = "com.alipay.sofa.ark.container.ArkContainer";
public static void main(String[] args) throws Exception {
new ArkLauncher().launch(args);
}
public ArkLauncher() {
}
public ArkLauncher(ExecutableArchive executableArchive) {
super(executableArchive);
}
@Override
protected String getMainClass() {
return SOFA_ARK_MAIN;
}
}
所以后续反射调用 main 实际上会调用到 ArkContainer#main()
方法。
完整实现如下:
public static Object main(String[] args) throws ArkRuntimeException {
// 参数校验
if (args.length binding : injector
.findBindingsByType(new TypeLiteral() {
})) {
arkServiceList.add(binding.getProvider().get());
}
Collections.sort(arkServiceList, new OrderComparator());
// 循环 ark 列表,执行 init
for (ArkService arkService : arkServiceList) {
LOGGER.info(String.format("Init Service: %s", arkService.getClass().getName()));
arkService.init();
}
ArkServiceContainerHolder.setContainer(this);
ArkClient.setBizFactoryService(getService(BizFactoryService.class));
ArkClient.setBizManagerService(getService(BizManagerService.class));
ArkClient.setInjectionService(getService(InjectionService.class));
ArkClient.setEventAdminService(getService(EventAdminService.class));
ArkClient.setArguments(arguments);
LOGGER.info("Finish to start ArkServiceContainer");
} finally {
ClassLoaderUtils.popContextClassLoader(oldClassLoader);
}
}
}
pipeline 流水线
arkServiceContainer中包含了一些Container启动前需要运行的Service,这些Service被封装到一个个的PipelineStage中,这些PipelineStage又被封装成List到一个pipeline中。
主要包含这么几个PipelineStage,依次执行:
(1)HandleArchiveStage
筛选所有第三方jar包中含有mark标记的plugin jar,说明这些jar是sofa ark maven插件打包成的需要隔离的jar。
从jar中的export.index中提取需要隔离的类,把他们加入一个PluginList中,并给每个plugin,分配一个独立的PluginClassLoader。同时以同样的操作给Biz也分配一个BizClassLoader
(2)DeployPluginStage
创建一个map,key是需要隔离的类,value是这个加载这个类使用的PluginClassLoader实例。
(3)DeployBizStage
使用BizClassLoader反射调用Biz的main方法。
至此,Container就启动完了。
后面再调用需要隔离的类时,由于启动Biz的线程已经被换成了BizClassLoader,在loadClass时BizClassLoader会首先看看在DeployPluginStage创建的Map中是否有PluginClassLoader能加载这个类,如果能就委托PluginClassLoader加载。
就实现了不同类使用不同的类加载器加载。
小结
对于类冲突,ark 确实是一种非常优雅轻量的解决方案。
背后核心原理就是对于 jvm classloader 和 maven plugin 的理解和应用。
学习好原理,并且和具体的应用场景结合起来,就产生了新的技术工具。
希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。
我是老马,期待与你的下次相遇。