一个功能健全的类加载器,都要解决以下几个问题:
(1)部署在同一服务器上的两个web应用程序所使用的java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当可以保证两个应用程序的类库可以相互独立使用。
(2)部署在同一个服务器上的两个web应用程序所使用的java类库可以相互共享,这个需求也很常见,例如用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10分Spring分别放在各个应用程序的隔离目录中,将会是很大的资源浪费-----主要到不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区很容易就会出现过度膨胀的危险。
(3)服务器需要尽可能的保证自身的安全不受部署的web应用程序影响,目前,很多主流的java web服务器自身也是使用java语言来实现的,因此服务器本身也有类库依赖问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。
(4)支持JSP应用的web服务器,十有八九都需要支持HotSwap功能,我们知道JSP文件最终要被编译成java Class文件才能被虚拟机执行,但JSp文件由于其纯文本存储的特性,被运行时修改的概率远远大于第三方类库或程序自己的Class文件,而且ASP、PHP、JSP这些网页应用也把修改后无须重启作为一个很大的“优势”来看待,因此“主流”web服务器都会支持JSP生成类的热替换,当然也有“非主流”,如运行在生产模式下的webLogic服务器默认就不会处理JSP文件的变化。
由于上述存在的问题,在部署web应用时,单独的一个ClassPath就无法满足需求了,所以各种web服务器都不约而同提供了好几个ClassPath路径用户存放第三方类库,这些路径一般都以“lib”或“classess”命名,被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的jav类库,那么Tomcat是如何规划用户的类库结构和类加载器的?
在Tomcat目录结构中,有三组目录(“/common/*”、“/server/*”、和“/shared/*”)可以存放在java类库中,另外还可以加上web应用程序自身的目录“/WEB-INF/*”,一共四组,把java类库放置在这些目录中的含义分别是:
(1)放置在/common目录中;类库可被Tomcat和所有的web应用程序共同使用。
(2)放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
(3)放置在/shared目录中:类库可被所有web应用程序共同使用,但对Tomcat自己不可见。
(4)放置在/WebApp/WEB-INF目录中:类库仅仅可以被此web应用程序使用,对Tomcat和其他web应用程序都不可见。
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现。下图是tomcat服务器的类加载结构:
灰色背景的三个类加载器时JDK默认提供的类加载器。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分载/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中java类库的逻辑,其中webApp类加载器和JSP类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个JSP类加载器。
从上图的委派关系可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对象相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离,而JasperLoader的加载范围仅仅是这个Jsp文件所编译出来的那一个Class,它出现的目的就是为了被丢弃:当服务器监测到Jsp文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现Jsp文件的HotSwap功能。
对于Tomcat的6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正建立CatalinaClassLoader和SharedClassLoader的实例,否则会用到这两个类加载器的地方都会用CommonClassLoader的实例来替换,而默认的配置文件中没有设置这两个loader项,所以Tomcat6.x顺理成章的把/common、/server、/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/commom目录张类库的作用。这是tomcat设计团队为了简化大多数的部署场景所做的一项改进,如果默认设置不能满足需要,用户可以通过修改配置文件制定server.loader和share.loader的方式重新启用Tomcat5.x的加载器架构。
2、OSGI灵活的类加载器架构
OSGI中的每个模块(称为Bundle)与普通的java类库区别并不太大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明它允许导出发布的Java Package(通过Export-Package描述)。在OSGI中,Bundle之间的依赖关系从传统的上层模块依赖底层模块变成平级模块之间的依赖(至少外观上是如此),而且类库的可见性能得到了非常精确的控制,一个模块里只有被Export过的Package才可能被外界访问,其他的Package和Class将会被隐藏起来,除了更精确的模块划分和可见性控制外,引入OSGI另外一个重要理由是,基于OSGI的程序很可能可以实现模块级的热插拔功能,当程序升级更新或调试除错时,可以只停用,重新安装然后启用程序的其中一部分,这对企业级程序开发来说是一个非常有诱惑力的特性。
OSGI之所以能有上述诱人的特点,要归功于它灵活的类加载器结构,OSGI的Bundle类加载器之间只有规则,没有固定的委派关系,例如,某个Bundle声明了一个它依赖的Package,如果有其它Bundle声明发布了这个Package后,那么对这个Package的所有类加载动作都会委派给发布它的Bundle类加载器去完成,不涉及某个具体的Package时,各个Bundle加载器都是平级的关系,只有具体使用到某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派和依赖。
另外,一个Bundle类加载器为其他Bundle提供服务时,会根据Export-Package列表严格控制访问范围,如果一个类存在于Bundle的类库中但是没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其他Bundle使用,而且OSGI平台也不会把其他Bundle的类加载请求分配给这个Bundle来处理。
例如,假设存在BundelA、BundleB和BundleC三个模块,并且这三个Bundle定义的依赖关系为:
BundleA:声明发布了packageA,依赖了java.*的包。
BundleB:声明依赖了packageA和packageC,同 时也依赖了java.*的包。
BundleC:声明发布了packageC,依赖了packageA。
那么这三个Bundle之间的类加载器以及父类加载器之家关系如图:
上图只是体现了加载器件关系的概念模型,并且只是体现了OSGI中最简单的加载器委派关系,一般来说,在OSGI中,加载一个类可能发生的查找行为和委派关系会比上图复杂的多,类加载时可能进行的查找规则如下:
以java.*开头的类,委派给父类加载器加载。
否则,委派列表名单内的类,委派给父类加载器加载。
否则,Import列表中的类,委派个Export这个类的Bundle的类加载器加载。
否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
否则,查找是否在自己的FragMent Bundle中,如果是则委派给Fragment Bundle的类加载器加载。
否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
否则,类查找失败。
从上图看出,在OSGI里面,加载器之间的关系不再是双亲委派模型的树形结构,而是进一步发展成了一种运行时才能确定的网状结构。这种网站结构的类加载器在带来更优秀的灵活性的同时,也可能产生许多新的隐患。