深入理解Java虚拟机(第3版)学习笔记——Tomcat与OSGI中的类加载机制
一个功能健全的Web服务器,需要解决如下的问题:
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离
- 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响
- 支持JSP应用的Web服务器,基本上都需要支持热更新(HotSwap)功能
结论:
在部署Web应用时,单独的一个ClassPath就不能满足需求了,所以各种Web服务器都不约而同地提供了好几个有着不同含义的ClassPath路径供用户存放第三方类库。
通常每一 个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。
1. Tomcat:正统的类加载器结构
在Tomcat目录结构中,可以设置3组目录(/common/*、/server/*和/shared/*,但默认不一定是开放的,可能只有/lib/*目录存在)用于存放Java类库,另外还应该加上Web应用程序自身的“/WEBINF/*”目录,一共4组。把Java类库放置在这4组目录中,每一组都有独立的含义,分别是:
- 放置在**/common**目录中。类库可被Tomcat和所有的Web应用程序共同使用。
- 放置在**/server**目录中。类库可被Tomcat使用,对所有的Web应用程序都不可见。
- 放置在**/shared**目录中。类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
- 放置在**/WebApp/WEB-INF**目录中。类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器, 这些类加载器按照经典的双亲委派模型来实现(以JDK 9之前经典的三层类加载器为例):
由上图知:
Common类加载器能加载的类都可以被Catalina类加载器和Shared类加载器使用
Catalina类加载器和Shared类加载器自己能加载的类则与对方相互隔离
WebApp类加载器可以使用Shared类加载器加载到的类,但各个WebApp类加载器实例之间相互隔离
JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class文件
它存在的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的HotSwap功能。
本例中的类加载结构在Tomcat 6以前是它默认的类加载器结构,在Tomcat 6及之后的版本简化了默认的目录结构:
只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正建立Catalina类加载器和Shared类加载器的实例
默认的配置文件中并没有设置这两个loader项
Tomcat 6之后顺理成章地把/common、/server和/shared这3个目录默认合并到一起变成1个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用
用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用原来完整的加载器架构
类加载器在jdk9后的变化?
(44条消息) 【JVM系列】类加载器详解(JDK9+)_槑!的博客-CSDN博客
被Common类加载器或者Shared加载器加载的Spring如何访问并不在加载范围内的用户程序呢?
==通过线程上下文类加载器去实现==向下加载。
2. OSGI:灵活的类加载器架构
OSGi (Open Service Gateway Initiative)是OSGi联盟(OSGi Alliance)制订的一个基于Java语言的动态模块化规范(在JDK 9引入的JPMS是静态的模块系统)
OSGi中的每个模块(称为Bundle)与普通的Java类库区别并不太大,两者一般都以JAR格式进行封装,区别在于:
一个Bundle可以声明它所依赖的Package(通过Import-Package描述),也可以声明它允许导出发布的Package(通过Export-Package描述);
Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖
Bundle类库的可见性能得到非常精确的控制,一个模块里只有被Export过的Package才可能被外界访问,其他的Package和Class将会被隐藏起来。
OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。
引入OSGI的主要优点:基于OSGi架构的程序很可能会实现模块级的热插拔功能,
当程序升级更新或调试除错时,可以只停用、重新安装然后启用程序的其中一部分。这对大型软件、企业级程序开发来说是一个非常有诱惑力的特性,譬如Eclipse中安装、卸载、更新插件而不需要重启动,就使用到了这种特性。
实例:
假设:
Bundle A:声明发布了 packageA,依赖了 java.*的包;
Bundle B:声明依赖了 packageA 和 packageC,同时也依赖了 java.*的包;
Bundle C:声明发布了 packageC,依赖了 packageA。
继承关系:
委托规则:
- 以java.*开头的类,委派给父类加载器加载。
- 否则,委派列表名单内的类,委派给父类加载器加载。
- 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的Classpath,使用自己的类加载器加载。
- 否则,查找是否在自己的Fragment Bundle中,如果是则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。
在OSGi中,加载器之间的关系不再是双亲委派模型的树形结构,而是已经进一步发展成一种更为复杂的、运行时才能确定的网状结构