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 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 > <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 > <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 { 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 加载的。
所以在构建插件时,需要尽可能的去规避可能出现引起类型检查的地方: