死磕Tomcat系列(6)——Tomcat如何作到热加载和热部署的
热部署就是在服务器运行时从新部署项目,热加载即在在运行时从新加载class,从而升级应用。html
一般状况下在开发环境中咱们使用的是热加载,由于热加载的实现的方式在Web容器中启动一个后台线程,按期检测相关文件的变化,若是有变化就从新加载类,这个过程不会清空Session。而在生产环境咱们通常应用的是热部署,热部署也是在Web应用后台线程按期检测,发现有变化就会从新加载整个Web应用,这种方式更加完全会清空Session。web
热加载
热加载其实咱们在开发过程当中常常使用,例如咱们使用Idea开发时,咱们在设置页面能够进行设置,当修改文件时,咱们能够选择不重启项目,选择从新加载此文件。而在Tomcat中也能设置,Tomcat默认状况下是不开启热加载的。须要在Tomcat路径下的Context.xml
中配置reloadable
参数来开启这个功能。服务器
<Context reloadable="true"/>
咱们演示一下Tomcat是如何热加载的。在webapp下咱们新建了一个项目,里面的Servlet文件以下架构
public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("MyServlet 在处理 get()请求..."); PrintWriter out = response.getWriter(); response.setContentType("text/html;charset=utf-8"); out.println("<strong>>My Servlet Version1!</strong><br>"); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("MyServlet 在处理 post()请求..."); PrintWriter out = response.getWriter(); response.setContentType("text/html;charset=utf-8"); out.println("<strong>>My Servlet Version1!</strong><br>"); } }
目录结构以下app
-webapp -- mywebapp -- WEB-INF -- web.xml -- classes -- MyServlet.class
为了演示Tomcat运行时能修改class文件可以动态加载。咱们分为如下三步webapp
- 正常启动Tomcat。输入
http://localhost:8080/mywebapp/myservlet
,观察页面输出 - 在Tomcat启动的状况下修改
MyServlet
文件后覆盖原来的class文件 - 再次观察页面状况。观察页面输出是否修改
下面直接用动态图演示效果,更直观一些。ide
咱们能够看到在Tomcat运行的状况下,直接替换class文件是可以直接生效的。那么Tomcat是如何作到的呢?其实咱们能够本身推导一下。post
- 全部的class文件都是交由类加载来管理的
- 若是换了class文件是否是只须要更换相应的类加载器从新加载就行
那么接下来咱们来验证咱们的结论,看一下在Tomcat中是如何实现热加载的。Tomcat要监听class文件是否变化应该是新起了一个线程来观测。那么看到在Context的启动方法中,看到调用了threadStart
的方法。this
protected void threadStart() { backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor() .scheduleWithFixedDelay(new ContainerBackgroundProcessor(),//要执行的Runnable backgroundProcessorDelay, //第一次执行延迟多久 backgroundProcessorDelay, //以后每次隔多久执行一次 TimeUnit.SECONDS); //时间单位 } }
其中在后台开启周期性的任务,使用了Java提供的ScheduledThreadPoolExecutor
。除了能周期性执行任务之外还有线程池的功能。上面代码中调用了scheduleWithFixedDelay
方法,第一个传入的参数就是要执行的任务。咱们接下来看任务类ContainerBackgroundProcessor
是如何实现的。spa
protected class ContainerBackgroundProcessor implements Runnable { @Override public void run() { // 请注意这里传入的参数是 " 宿主类 " 的实例 processChildren(ContainerBase.this); } protected void processChildren(Container container) { try { //1. 调用当前容器的 backgroundProcess 方法。 container.backgroundProcess(); //2. 遍历全部的子容器,递归调用 processChildren, // 这样当前容器的子孙都会被处理 Container[] children = container.findChildren(); for (int i = 0; i < children.length; i++) { // 这里会判断子容器若是已经启动了后台线程,那么这里就不会启动了 if (children[i].getBackgroundProcessorDelay() <= 0) { processChildren(children[i]); } } } catch (Throwable t) { ... }
上面代码中咱们能够知道具体的后台监听代码是在backgroundProcess
方法中实现的。那么咱们看Context
容器的backgroundProcess
方法是如何实现的。
public void backgroundProcess() { //WebappLoader 周期性的检查 WEB-INF/classes 和 WEB-INF/lib 目录下的类文件 Loader loader = getLoader(); if (loader != null) { loader.backgroundProcess(); } ............省略 }
进去loader.backgroundProcess();
中咱们能够看到
public void backgroundProcess() { //此处判断热加载开关是否开启和监控的文件夹中文件是否有修改 if (reloadable && modified()) { try { Thread.currentThread().setContextClassLoader (WebappLoader.class.getClassLoader()); if (context != null) { //Context重启 context.reload(); } } finally { if (context != null && context.getLoader() != null) { Thread.currentThread().setContextClassLoader (context.getLoader().getClassLoader()); } } } }
咱们能够发现Tomcat热加载的步骤
- 若是发现有文件发生变化,热加载开关开启
- 关闭Context容器
- 重启Context容器
在这个过程当中,最重要的部分其实就是类加载器了。由于一个Context容器对应一个类加载器。因此在销毁Context容器的时候也连带着将其类加载器一并销毁了。Context在重启的过程当中也会建立新的类加载器来加载咱们新建的文件。
热部署
若是仍是不懂热部署是什么的,下面演示一遍应该就明白了。Tomcat在启动的时候会将其目录下webapp中war包解压后而后封装为一个Context供外部访问。那么热部署就是在程序运行时,若是咱们修改了War包中的东西。那么Tomcat就会删除以前的War包解压的文件夹,从新解压新的War包。
咱们发现上面动图中在Tomcat运行时,咱们修改了War包的信息,它就会将原来的删除而后从新生成一份。
咱们从上面的动图中其实就看出了热部署和热加载的区别了。热部署是将文件夹删除而后从新解压包。那么热加载是由Context容器负责的。那么热部署又是由哪一个容器负责呢?由于一个文件夹对应一个Context。既然文件夹都删除了,那么确定不是由Context容器负责了。那么应该就是Context的父容器Host来负责。
咱们能够看到Host
容器并无实现本身的backgroundProcess
方法。那么它是如何监听的呢?既然它没有实现方法,确定是调用了父类的backgroundProcess
方法。咱们能够看到在父类的backgroundProcess
中
@Override public void backgroundProcess() { . ...........省略 fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); }
能够看到周期事件的监听器。而Host
的事件监听器是HostConfig
类的lifecycleEvent
方法
@Override public void lifecycleEvent(LifecycleEvent event) { if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {// 周期事件 check(); } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {// 开始以前事件 beforeStart(); } else if (event.getType().equals(Lifecycle.START_EVENT)) { // 开始事件 start(); } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { // 结束事件 stop(); } }
咱们能够看check
方法
protected void check() { if (host.getAutoDeploy()) { // 检查Host下全部已经部署的web应用 DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]); for (int i = 0; i < apps.length; i++) { if (!isServiced(apps[i].name)) checkResources(apps[i], false); } // 检查Web应用是否有变化 if (host.getUndeployOldVersions()) { checkUndeploy(); } // 执行部署 deployApps(); } }
热部署的步骤其实也能够简化为三步骤
- 检查Host管理下的全部web应用
- 若是原来的Web应用被删除,就将相应Context容器删除
- 若是有新War包放进来,就部署相应的War包