Web服务器需要解决的几个问题
主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere或其他没有列举的服务器,都实现了自己定义的类加载器,而且一般还不止一个。因为一个功能健全的Web服务器,要解决如下几个问题:
- 部署在同一服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录,将会是很大的资源浪费-这主要不是磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存中,如果类库不能共享,虚拟机的方法区就会容易出现过度膨胀的风险。
- 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的Web服务器自身也是使用Java语言来实现的。因此服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。
- 支持JSP应用的Web服务器,大多数都需要支持HotSwap功能。我们知道,JSP文件最终要编译成Java Class才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身的Class文件。而且ASP、PHP和JSP这些网页应用也把修改后无须重启作为一个很大的“优势”来看待。因此主流的Web服务器都会支持JSP生成类的热替换,当然也有非主流的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化。
由于存在上述问题,在部署Web引用时,单独的一个ClassPath就无法满足需求了,所以各种Web服务器都“不约而同”地提供了好几个ClassPath路径使得用户存放第三方类库,这些路径一般都以“lib”或者“classes”命名。被放置在不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相对应的自定义类加载器去加载放置在里面的Java类库。下面我们来看看Tomcat服务器,看看Tomcat具体是如何规划用户类库结构和类加载器的。(ps.这里用的是Tomcat5.x版本,在Tomcat6.x的默认配置下,/common、/server、/shared三个目录已经合并在一起了。)
Tomcat的类库结构
在Tomcat目录结构中,有3组目录/common/*、/server/*、/shared*
,可以存在Java类库,另外还可以加上Web应用程序自身的目录/WEB-INF/*
,一共4组,把Java类库放置在这些目录中的含义如下。
目录 | 作用 |
---|---|
common | 类库对Tomcat和所有的Web应用程序可见 |
server | 类库对Tomcat可见,对Web应用程序不可见 |
shared | 类库对Tomcat不可见,对所有Web应用程序可见 |
WEB-INF | 类库对Tomcat和其他Web应用不可见,只对Web程序本身可见 |
Tomcat的类加载器架构
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这个类加载器按照经典的双亲委派模式来实现,其关系如下图。
顶层3个类加载器是JDK默认提供的类加载器,这3个加载器的作用在这里有解析JVM-类加载机制.而CommonClassLoader、CatalinaClassLoader、SharedClassLoader、WebAppClassLoader则是Tomcat自定义的类加载器,它们分别加载/common/*、/server/*、/shared/*、/WEB-INF/*
中的类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用对应一个WebApp类加载器,每一个Jsp文件对应一个Jsp类加载器。
从上图的委派关系中可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalineClassLoader和SharedClassLoader自己能加载的类则与双方相互隔离。 WebAppClassLoader可以使用ShareClassLoader加载到的类,,但各个WebAppClassLoader实例之间相互隔离。而JasperClassLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class文件,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改的时候,会替换掉目前的JasperClassLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能(那究竟是如何实现的?)。
对于Tomcat的6.x版本,只有指定了tomcat/conf/catalina.properties
配置文件的server.loader和shared.loader项后才会真正建立CatalinaClassLoader和SharedClassLoader的实例,否则会用到这两个类的地方都会用CommonClassLoader的实例代替,而默认的配置文件中没有设置这两个项。所以Tomcat6.x顺理成章地把/common、/shared和/server三个目录合并成了一个/lib目录,这个目录里的类库相当于以前的/common目录中类库的作用。这是Tomcat设计团队为了简化大多数的部署场景所做的一项改进,如果默认的设置不能满足需求,用户可以通过修改配置文件指定server.loader和shared.loader的方式重新启动Tomcat5.x的类加载器架构。
一个问题
思考一个问题:前面曾经提到过一个场景,如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring放在Common或者Shared目录下让这些程序共享。Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户的程序显示是放在/WebApp/WEB-INF目录中的,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?
解答:按照我的理解,类的加载是双亲委派模型,当需要加载Spring时,Tomcat会优先使用WebAppClassLoader去加载Spring,然后会委托父类加载器SharedClassLoader,这时SharedClassLoader加载/shared里面的Spring类库,所以在WebAppClassLoader加载Spring成功。而每个WebAppClassLoader是隔离的,它们加载的Spring互不影响。