ARK 插件基本规则及注意事项

SOFAARK 是一个轻量级的类隔离框架,其有两个基本的能力:解决依赖包冲突和多应用(模块)合并部署。本篇将从解决依赖角度来说明下 SOFARK 插件的基本使用规则。

下图是官方文档中提供的用于描述依赖包冲突的一个场景:

这里通过一个工程来模拟这种场景,然后通过将其中一个打包成插件的方式来解决。

案例工程

1
2
3
4
5
├── ark-main-project
├── dependency-one
├── dependency-two
├── dependency-two-plugin

  • ark-main-project 为一个 简单的springboot 工程
  • dependency-one 依赖1,可以对应到图中的 dependency A
  • dependency-two 依赖2,可以对应到图中的 dependency B
  • dependency-two-plugin ,dependency-two 的插件包

另外还有一个 dependency-incompatible 工程,用于描述冲突的依赖。

dependency-incompatible

dependency-incompatible 有两个版本 1.0 和 2.0 ,1.0 和 2.0 是不兼容的。

1.0 版本中提供了两个方法:

1
2
3
4
5
6
7
8
9
public class IncompatibleUtil {

public static String test1(){
return "test1";
}
public static String test2(){
return "test2";
}
}

2.0 版本中提供了两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static String test1(){
return "test1";
}

public static String test3(){
return Incompatible.test();
}

public static class Incompatible {
public static String test() {
return "test";
}
}
}

dependency-one

1
2
3
4
5
public class TestOneUtil {
public String testOne(){
return IncompatibleUtil.test1()+IncompatibleUtil.test2();
}
}

dependency-two

1
2
3
4
5
6
7
8
9
10
// import org.springframework.util.StringUtils;
public class TestTwoUtil {
public String testTwo(String param){
if (StringUtils.isEmpty(param)){
return IncompatibleUtil.test1() + IncompatibleUtil.test3();
} else {
return IncompatibleUtil.test1() + IncompatibleUtil.test3();
}
}
}

这里引入 spring 的依赖查看是否会引入异常

ark-main-project

ark-main-project 引入了 dependency-one 和 dependency-two 两个依赖,然后在启动类中分别调用 dependency-one 和 dependency-two 中提供的 api 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
test("test");
}

public static void test(String param) {
if (!StringUtils.isEmpty(param)){
TestOneUtil testOneUtil = new TestOneUtil();
System.out.println(testOneUtil.testOne());
TestTwoUtil testTwoUtil = new TestTwoUtil();
System.out.println(testTwoUtil.testTwo(param));
}
else {
System.out.println("no params");
}
}
}

由于 dependency-one 和 dependency-two 底层都都依赖了 dependency-incompatible ,且 dependency-incompatible 的两个版本不兼容,所以在启动时会报错。

dependency-two 插件改造

根据文档前面那张图的描述,这里需要将其中一个改造成插件的方式,使用独立的 classloader 来加载,从而达到版本兼容。这里改造 dependency-two 。

新建一个 dependency-two-plugin 模块,然后引入 dependency-two 依赖,并且将 冲突的 api 包导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<dependencies>
<dependency>
<artifactId>dependency-two</artifactId>
<groupId>com.glmapper.bridge.boot</groupId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.alipay.sofa</groupId>
<artifactId>sofa-ark-plugin-maven-plugin</artifactId>
<version>0.6.0</version>
<executions>
<execution>
<id>default-cli</id>
<goals>
<goal>ark-plugin</goal>
</goals>

<configuration>
<exported>
<packages>
<!--导出冲突的 api -->
<package>com.glmapper.bridge.boot.two</package>
</packages>
</exported>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

关于插件的导出,对于 dependency-two 中,ark-main-projet 中使用到的是 TestTwoUtil 这里类,因此仅需要将这个类导出即可。
mvn clean install 安装到本地仓库,然后在 ark-main-project 中引用。

将 ark-main-project 中的 dependency-two 依赖修改为 dependency-two-plugin 。

1
2
3
4
5
<dependency>
<groupId>com.glmapper.bridge.boot</groupId>
<artifactId>dependency-two-plugin</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

因为插件是运行在容器上的,所以也需要将 ark-main-project 改造成 ark 工程,具体可以参考官方文档。改造完成之后,打包 ark-main-project 工程,然后通过 java -jar 启动,运行结果如下,实现了类隔离。

NoClassDefFoundError 异常的发生

关于上面 SpringUtils 工具类在插件中和 BIZ 中均加载并且不会报错的解释是,SpringUtils 虽然在插件中和 BIZ 中都被加载了,但是没有报错,是因为没有触发 java 的 type check 机制。

那么还有一种情况会导致出现 java.lang.NoClassDefFoundError 异常,这种情况是在插件中将 spring 相关的包指定不打入插件了,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<configuration>
<exported>
<packages>
<package>com.glmapper.bridge.boot.two.*</package>
</packages>
</exported>
<!--不将 spring 的包打进去-->
<excludeGroupIds>
<excludeGroupId>org.springframework</excludeGroupId>
<excludeGroupId>org.springframework.boot</excludeGroupId>
<excludeGroupId>org.apache.tomcat.embed</excludeGroupId>
</excludeGroupIds>
</configuration>

那么这样打出的包实际上包的大小会非常小,但是问题在于运行时,插件从当前 /iib 目录下找不到 spring 相关的依赖,就会报 java.lang.NoClassDefFoundError 。

LinkageError 异常的发生

ark-main-project 中

dependency-two 中

重新打包,然后执行

没有报错。此时插件中的类和 biz 中的类完全都是独立的。但是会存在一种情况,比如插件中有一个日志工具类,然后在 Biz 使用了这个工具类,则会报错。

在 dependency-two 中增加一个 LoggerUtil 的类,

1
2
3
4
5
6
7
8
9
public class LoggerUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggerUtil.class);
public void info(String message){
LOGGER.info(message);
}
public static Logger getLogger(){
return LOGGER;
}
}

然后在 ark-main-project 中这样使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MainApplication {
// 使用 LoggerUtil 获取日志对象实例
private static final Logger LOGGER = LoggerUtil.getLogger();

public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
test("test");
}
public static void test(String param) {
// 记录日志
LOGGER.info("test in biz.");
if (!StringUtils.isEmpty(param)){
TestOneUtil testOneUtil = new TestOneUtil();
System.out.println(testOneUtil.testOne());
TestTwoUtil testTwoUtil = new TestTwoUtil();
System.out.println(testTwoUtil.testTwo(param));
}
else {
System.out.println("no params");
}
}
}

这种情况下就会导致报错: Caused by: java.lang.LinkageError: loader constraint violation: loader (instance of com/alipay/sofa/ark/container/service/classloader/BizClassLoader) previously initiated loading for a different type with name “org/slf4j/Logger

1
private static final Logger LOGGER = LoggerUtil.getLogger();

单从这段代码来看,报错的原因在于,Logger LOGGER 的对象加载是被 BizClassLoader 加载的,但是 LoggerUtil.getLogger() 返回的对象是由 PluginClassLoader 加载的。

所以在构建插件时,需要尽可能的去规避可能出现引起类型检查的地方:

  • 方法参数检验
  • 变量赋值
  • 方法返回值
作者

卫恒

发布于

2019-08-28

更新于

2022-04-23

许可协议

评论