看过Pandora文档的一些介绍——轻量级的依赖隔离容器,在我脑子里浮现了这几个名词:Tomcat部署多应用、OSGI、Java9模块化。谷歌“依赖隔离”这个关键词出来的结果:类加载机制、蚂蚁开源的sofa-ark。所以读了很多文章想看看这些名词之间有什么联系。
1、从Tomcat说起
现在SpringBoot推崇内嵌的Servlet容器,直接把Tomcat内嵌到jar包里了。但在早期业务尚不复杂应用还依赖JSP的时候,Tomcat就像一艘航母,承载着若干个轻量级的应用。
其中有个值得思考问题是,整个Tomcat就是一个Java进程,假若Tomcat上有两个应用都依赖spring-framework,但版本不一致,Tomcat如何决定使用哪个版本的依赖呢?
答案是两个版本都存在。这个答案会引起更大的疑问,Java是靠全类名来标识两个类的,如果两个应用用到不同版本的org.springframework.context.annotation.AnnotationConfigApplicationContext
,那这两个类是如何在一个Java进程中共存的呢?
这个问题需要我们对类加载机制有深入的了解才能解答。
2、Java类加载机制
当我们把java代码编译后打包成jar
,我们用到这个jar包的时候只需在classpath
中加上这个依赖就行。
1 | import java.util.List; |
比如我们要用到Guava的Lists
工具类,编译和运行时都需要加上classpath
。
1 | javac -classpath ~/.m2/repository/com/google/guava/guava/20.0/guava-20.0.jar ClassPathTest.java |
如果这里直接改用new ArrayList()
,就不需要指定classpath
。这是因为JDK里的ArrayList
和Guava的Lists
是被不同的类加载器加载的。前者被BootstrapClassLoader加载,后者由SystemClassLoader加载。
2.1、类加载器的定义
ClassLoader中定义了JDK默认的类加载机制:
1 | public abstract class ClassLoader { |
ClassLoader的定义中有一个非常重要的字段就是parent
,从loadClass
的代码可以看出Java类加载遵循所谓的“双亲委托机制”——先看该类是否已经被加载过,如果没有从父类加载器中加载该类,如果父类加载器没找到再调用当前类的加载器中定义的findClass
去加载。
2.2、Java内置的三个类加载器
Java中默认内置3个类加载器:
Bootstrap ClassLoader在虚拟机层,用C++编写。用于加载
rt.jar
等运行时基础类库,也被称作“Root ClassLoader”。ExtClassLoader是用于加载
JAVA_HOME/lib/ext/*.jar
目录的JDK扩展类库。是JDK2.0引入标准扩展机制时添加的类加载器。AppClassLoader是用于加载类路径下的第三方类库,也被称为“SystemClassLoader”,可以通过
ClassLoader.getSystemClassLoader()
获取。
应用也可以自定义类加载器。比如去加载网络服务器上的某个jar包,则可以继承ClassLoader
或URLClassLoader
请求网络进行加载。
1 | public class ClassPathTest { |
为什么要遵循双亲委派机制?
- 保证核心类的安全。防止开发者取了和jdk核心类库中一样的包名和类名,委托给父类加载器能保证JDK类库的类优先加载。
- 保证类的唯一性。先检查是否加载过这个类,避免相同类被多次加载。
3、打破双亲委派机制
说回Tomcat的问题。要让一个Java进程同时加载Spring3.0和Spring4.0两个版本的类,按照JDK自带的双亲委派模型是没法解决的。因为ClassLoader#loaderClass
默认会检查这个类有没有加载过,保证了类在进程中是唯一的。如果我们想加载两个版本的类,需要打破原有的模型:
1 | import java.io.File; |
上面这个例子重写了loadClass
方法,把findClass
方法放在前面调用,让Test
类能够被重复加载多次。
3.1、Tomcat的类加载器
从代码中可以看出Tomcat会先从war包的/WEB-INF/classes
目录尝试加载类,如果失败了再委托给parent加载器。而每个WebApp都会有自己的WebappClassLoader,这样就可以保证每个Webapp的依赖类相互隔离了。
1 | public abstract class WebappClassLoaderBase extends URLClassLoader |
4、菱形依赖问题
前面说到的Tomcat这种场景需要在一个进程中加载两个不同版本依赖。
推而广之,还有软件开发过程中经常碰到的菱形依赖问题(Diamond Dependency)。
Maven作为一个Java领域的依赖管理工具,提供了exclusion
标签来排除LibC
这样的传递依赖,或者直接依赖高版本的库。但这个前提是高版本的依赖需要兼容低版本。向前兼容要保留恶心的祖传代码,这对于有代码洁癖的程序员来说是个极其艰巨的任务,所以除了JDK标准库大多数三方依赖库在升级大版本时会有各种兼容问题,这也是为什么JDK中保留着Vector
和StringBuffer
这样的上古代码的原因。
如果真出现了LibA
和LibB
依赖的版本差别大无法兼容,NoClassDefFoundError
、NoSuchMethodError
等各种错误就会接踵而至,那怎么办呢?
一种方式是直接把别人的代码拷过来,换个包名。这种方式简单粗暴,也许会觉得这个方式很low逼,但其实用的人挺多的,而且不乏业界名流,spring-framework
就是这样把cglib
的代码拷过来的。但这种方式仅局限于cglib这样没有其他依赖的短小精悍的库。
另一种方式就是之前说的通过打破双亲委派模型的类隔离机制。业界比较知名的就是OSGI,Eclipse中的各种插件相互隔离就是靠OSGI实现,而且还支持插件的动态插拔。OSGI联盟野心很大,曾一度想让OSGI成为Java模块化技术的标准,不过Java9在语法层面提供了JPMS标准,直接颠覆了原有的模块化管理方式。
5、从OSGI到Pandora
最初,HSF 1.X为了解决与应用的jar冲突问题,使用OSGi来做隔离。当时淘系大部分的应用都运行在JBoss中,.sar
作为JBoss支持的一种部署格式(与 .war
类似),它在JBoss中的默认启动顺序早于.war
,符合HSF优先于应用启动完成类导出的需求,因此HSF 1.X的部署包被定为taobao-hsf.sar
。
随着集团的业务发展,内部已经有很多诸如HSF、Notify、MetaQ、Diamond、Tair等各种中间件或客户端产品。这些二方包被各个业务系统使用,为了能解决三方包依赖冲突、方便大规模升级并控制二方包升级成本等问题,从HSF 2.X起,“隔离”的功能被独立地交付给Pandora。这时候的“隔离”不再是“HSF与应用的隔离”,而是“中间件与应用的隔离”以及“中间件之间的隔离”。Pandora容器废弃了OSGI框架,只引入了它的隔离机制,重新实现ClassLoader,形成了全新的轻量级隔离容器。
由于线上大量启动脚本已经写死了taobao-hsf.sar
,为了降低风险,所以Pandora独立成隔离容器后,仍然沿用了原有的名字。
和Tomcat类似,每个Pandora Plugin模块都有自己的ModuleClassLoader,这样就能保证每个中间件Plugin相互隔离。
6、PandoraBoot
受Ruby on Rails“约定大于配置”思想的影响,Pivotal基于Spring3.0的注解配置和Spring4.0的@Conditional Bean,开发了支持AutoConfiguration的SpringBoot,大大简化了应用的配置。
而PandoraBoot则将SpringBoot和Pandora进行了整合。让开发可以既享受到SpringBoot简化配置的福利,又能带来Pandora对依赖隔离的功能。
原来的中间件以插件形式加入到taobao-hsf.sar
中,最后sar包越来越大,而PandoraBoot将sar包Maven化,发布到Maven仓库中,可以在Maven依赖中添加taobao-hsf.sar
的依赖,并按需添加相应插件的spring-boot-starter来整合Pandora Plugin,最终sar包和依赖的插件都可以打包到FatJar中。
是否将sar包和插件打包到FatJar是可选的,具体可以参考Pandora-Boot-Maven-Plugin
在日常/线上机器上,都是通过脚本里的
-Dpandora.location
来加载sar包的。 sar包位置是在/home/admin/$appName/target/taobao-hsf.sar
。在pandora-boot 2.1.3版本之后,
taobao-hsf.sar
变成一个空的jar包,它引入了taobao-hsf.sar-container
和其它的插件。具体可以参考PandoraBoot文档《对taobao-hsf.sar
的形式和位置》一节。
6.1、SpringBoot FatJar
SpringBoot会将应用以及相关的依赖打包成一个FatJar,只需要java -jar
命令即可启动应用,这是因为SpringBoot的maven构建插件会将MANIFEST.MF
中的Main-Class
替换成JarLauncher,SpringBoot定义好针对FatJar的类加载器后,再去调SpringBoot的Start-Class
的入口方法。
但SpringBoot不同的是,PandoraBoot加载的不是简单的jar包,有一些二方包是支持依赖隔离的Pandora插件,这些插件包中包含了自己的依赖jar包。
这些插件需要和taobao-hsf.sar
中的插件一样进行依赖隔离。所以PandoraBoot基于SpringBoot的JarLauncher扩展了SarLauncher,再由SarLoaderUtils加载sar
包和外部的插件。
参考链接:
Wikipedia:https://en.wikipedia.org/wiki/Java_Classloader
Java classes and class loading:https://www.ibm.com/developerworks/java/library/j-dyn0429/
Class Loaders in Java:https://www.baeldung.com/java-classloaders
Find a way out of the ClassLoader maze:https://www.javaworld.com/article/2077344/find-a-way-out-of-the-classloader-maze.html
老大难的 Java ClassLoader 再不理解就老了:https://zhuanlan.zhihu.com/p/51374915
Do You Really Get ClassLoaders:https://www.jrebel.com/blog/how-to-use-java-classloaders
深入浅出ClassLoader(译):https://www.atatech.org/articles/33671
Tomcat Class Loader How to:http://tomcat.apache.org/tomcat-8.0-doc/class-loader-howto.html
Pandora VS Hilton:https://www.atatech.org/articles/94481
Pandora Documents:http://gitlab.alibaba-inc.com/middleware-container/pandora/wikis/home
Pandora实现原理:http://gitlab.alibaba-inc.com/middleware-container/pandora/wikis/implementation
Pandora Container 轻量级隔离容器 – 简介、基本原理、使用:https://www.atatech.org/articles/43952
下一代轻量级容器——Pandora(潘多拉)之隔离原理详解:https://www.atatech.org/articles/2640
Pandora Framework的实现原理:http://gitlab.alibaba-inc.com/middleware-container/pandora-framework/wikis/pandora-framework-whatis
Java中隔离容器的实现:http://codemacro.com/2015/09/05/java-lightweight-container/
SOFAArk介绍:https://www.sofastack.tech/projects/sofa-boot/sofa-ark-readme/
Introduction to OSGi:https://www.baeldung.com/osgi
Java 9, OSGi and the Future of Modularity (Part 1):https://www.infoq.com/articles/java9-osgi-future-modularity/
Java 9, OSGi and the Future of Modularity (Part 2):https://www.infoq.com/articles/java9-osgi-future-modularity-part-2/
Java 9, OSGi and the Future of Modularity (Part 1)[中文]:https://www.infoq.cn/article/java9-osgi-future-modularity
Java 9, OSGi and the Future of Modularity (Part 2)[中文]:https://www.infoq.cn/article/java9-osgi-future-modularity-part-2
深入理解OSGI:Java模块化之路:https://www.cnblogs.com/garfieldcgf/p/6378443.html
Classloader-Related Memory Issues:https://www.dynatrace.com/resources/ebooks/javabook/class-loader-issues/
sofa-ark类隔离技术分析调研:https://blog.mythsman.com/post/5d29b12c373f140fc98304a1/
The parent class loader delegation model Detailed:https://codesolu.com/2020/01/07/the-parent-class-loader-delegation-model-detailed/
JVM核心知识体系:https://www.atatech.org/articles/135439