在JVM规范抽象架构中我们提到ClassLoader负责加载Java字节码交给JVM,为了给下面应用更进一步提供基础,我还是快速的介绍一下ClassLoader的工作机制。
ClassLoader常规工作机制如下:
ClassLoader常规工作机制
对上图进一步解释:
□ 每个java.lang.ClassLoader的子类都允许指定一个parent(父加载),如果不指定将直接使用系统级ClassLoader。
□ 每个java.lang.ClassLoader的子类在loadClass的时候都应该先检测自己的父加载器是否已经加载了要求加载的类,如果加载直接返回。
□ 如果没有加载,首先应该用自己父加载器的loadClass去加载,如果父加载器还没有加载成功,就直接调用自己的findClass方法来查找类。
□ 如果自己的findClass还加载不成功,抛出ClassNotFoundException。
一般情况下建议不要破坏这种树形加载机制,因为它维护比较完整的类加载器树,被认为是类加载策略的“最佳实践”,要自定义请直接重写findClass方法即可。我们现实中常碰到的类加载问题(例如加载的类并不是你想要加载的;在同一个应用场景中同一个类多次被加载;在一些Java EE应用服务器中没有充分共享应该共享的类库等)都是对常规加载机制的理解不到位或破坏了常规类加载机制造成的。但当碰到一些特殊需求时(例如在一个JVM进程中一个类有不同的版本需要在父加载器和本身加载器中并存时),还是可以改变常规加载策略的,通常通过直接重写loadClass方法来完成。
好,我们回到ClassLoader的应用!通常对ClassLoader的应用有:
□ 热部署。
□ 软件保护。
□ 动态生成Java字节码。
□ 非常规的字节码加载。
下面我仅仅以Tomcat6的WEB应用ClassLoader为例介绍热部署,其它的留给大家通过过互联网资源去研究。
热部署(Hot Deployment)
热部署其实并没有什么神秘的,最差的热部署可以通过自动重启JVM。但我们一般意义上说的热部署指的是在不影响当前JVM中其它应用的前提下只对需要重新部署的程序进行局部更换。
其实热部署中采用ClassLoader主要是解决热部署中类重新加载的问题,而不是用ClassLoader来完全实现热部署。
我们以Tomcat6的Web应用(WAR)热部署为例看看其实现的基本原理。
在我剪辑代码说思路的时候,大家可以通过在线浏览源代码(http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk )了解详细内容,当然你可以Check out到你的IDE(例如Eclipse,需要安装subclipse)。
注:在大家读代码时看到SecurityManager相关内容的时候可以跳过,仅仅带着它是进行安全管理的理解即可,在后面内容Java安全部分我们会详细介绍该内容。
Tomcat6的Web应用加载实现在包org.apache.catalina.loader中,主要通过类WebappLoader来实现。
我们谈Tomcat的Web应用热部署,就是谈一个WAR的变化驱动Tomcat如何重新加载Web应用,那至少应该有一个监听变化的线程吧?要搞清楚那个线程做到对Web应用变化的扫描,我们还不得不提一下Tomcat的内部架构(参阅http://tomcat.apache.org/tomcat-6.0-doc/architecture/overview.html):
Tomcat的内部架构
从上图我们可以看到一个Web应用肯定有一个Context维护它,一个Context肯定有一个Host维护它,一个Host肯定有一个Engine维护它。那么监听Context变化的线程启动可能在Host和Engine里面,因为在Context级别启动一个线程做热部署这件发生频率不是很高的事情实在有点浪费。那么我们先看看Host的标准实现类org.apache.catalina.core.StandardHost的start方法:
好,我们发现线程启动是在StandardHost(Host的实现)中,追踪ContainerBackgroundProcessor类即可发现WebappLoader中如下的代码:
请注意红色下划线的代码,其中代码WebappLoader.class.getClassLoader默认返回的是org.apache.catalina.loader.WebappClassLoader(注意:这里没有新建一个ClassLoader,而是直接使用同一个ClassLoader,网上很多关于热部署的示例都是新建一个ClassLoader,这样操作简单,但是不科学的,重新发明轮子有什么意义?很浪费!)。在此我们发现热部署就是重新设置了一下ClassLoader然后重新加载(reload)Context。其实事情就这么简单,我前面说过热部署没有什么神秘的,但我也说过如何让变的东西重新加载,不变的东西不重新加载才是“高明”呢!那我们还是来看看WebappClassLoader吧,从中学习一些“高明”的东西。
在研究WebappClassLoader之前我们自己先想想如何在不新建一个ClassLoader的前提下完成类的重新加载:
□ 在重新加载之前清除当前ClassLoader维护的所有已加载类的信息,这是最简单的方法。
□ 在监听变化的时候,把变化的信息进行缓存,在重新加载之前清除变化部分的已加载类信息,这种方法很复杂,需要程序的模块化程度相当高。
Tomcat的WebappClassLoader采用的是第一种策略,在需要热部署之前会调用WebappClassLoader的stop方法,如下图:
在WebappClassLoader的stop方法中两个地方很重要:
□ clearReferences(),清除引用。
□ resourceEntries.cleear(),清除已加载类的缓存。
清除已加载类的信息包括清除对已加载类的相关引用和清除已加载类的缓存两个主要环节。
我们前面说过,重写ClassLoader的loadClass方法是实现自定义类加载器的直接入口,下面我们看看WebappClassLoader的loadClass方法:
loadClass方法很重要的几点,我列出来,其它请大家直接浏览源代码:
□ loadClass方法的操作在一个同步块( )中。这几乎是我们重写类加载器必须的。
□ 首先通过代码 检索是否已经加载,而findLoadedClass0使自己重写的。这和我们前面提到的常规加载机制有所区别。请各位直接浏览findLoadedClass0的代码,其中很重要的操作就是看看resourceEntries中是否已经有已经加载的类,如果你能和前面清除部分提到的resourceEntries.clear()对应上的话,就好理解其加载机制。
□ 如果没有缓冲,再调用 看是否缓存。这步操作才回到常规类加载机制中的已加载类查找,其实按照逻辑,这一步仅仅把非Web应用程序中的类交给更上一层的类加载器来管理。
□ 第三步,如果前两步都没有找到类,首先调用 ,通过系统类加载器直接加载。这个很好玩,为什么?原因很简单!首先我们自己写的Web应用类库通常情况下不会被系统类加载器找见,换句话说系统类加载器一定加载不到我们的类,如果加载到,那就有问题了!但先用系统类加载器恰恰解决了我们“胡乱”往我们的Web应用中扔其它系统包的问题,例如有些人直接把编译期用到的servlet-api.jar扔到自己部署的Web应用中。通过这点,也提醒大家不要乱扔包。
□ 第四步,如果设置代理属性为true,直接用WebappClassLoader的父类加载器。这个也很好理解,例如我们想把Tomcat拿来直接嵌入到我们的应用程序,这时我们希望Tomcat先用调用它的应用程序类加载还是有必要的。
□ 第五步,调用WebappClassLoader重写的findClass,顺着findClass可以找见首先调用方法findClassInternal,这一步才是真正Tomcat自定义如何加载和缓冲Web应用类的地方。请大家自己阅读findClassInternal,主要关注defineClass和definePackage。
□ 第六步,等于一个迫不得已默认类加载,即交给父类加载器或者系统类加载器。
到此为止,就等于基本介绍完了Tomcat的热部署的基本实现。总结一下实现热部署的关键环节:
□ 首先所有的需要热部署的模块架构必须应该有明确的生命周期,例如start和stop。并且在start期间应该有明确的显式类加载器去加载模块类(这个很重要,不要写的代码没有模块的感觉还要热部署,那是给自己找麻烦)。
□ 其次,要动态监听模块的更新,需要一个低层线程来不断地检查。
□ 再次,自定义类加载器,自己维护模块类是否已经加载的判断和缓存。
□ 最后,在执行重新加载的时候记住要先停止(stop)当前的模块应用然后清除引用和缓存(至于如何维护当前模块的运行状态那是另外一件事情,但你还是要考虑),然后重新开始模块(start)。
------------------------------------------------------------------------------
借题发挥:熟练掌握了这块,你可以实战很多有趣的东西,例如你可以写一个插件模式的聊天服务器,并且支持插件的热部署;当然你可以去金蝶碰碰是否可以加入他们的应用服务器团队。
好,先说到这里。