序
为什么我要写这篇博客,之前在分析tomcat整个请求过程的时候,越分析到后面,发现疑点越多,到最后,简直分析不下去了。 为什么呢?因为Tomcat源码是一块整体,WEB应用请求处理和Tomcat的容器加载息息相关, JSP页面解析成Servlet时,使用到的各个参数都与容器加载相关,因此这篇博客对于后面博客分析整个请求处理起到重要作用,对之前分析JSP源码的博客也会做一定补充。 而这篇博客从Host开始分析,再分析Context,最后分析Wrapper,因此博客的篇幅极长,所以读者可能要做好尽量准备 。
先从Web应用加载开始分析起, Web应用加载属于Servlet启动的核心处理过程 。
Catalina对Web应用的加载主要岂StandardHost,HostConfig, StandardContext, ContextConfig , StandardWrapper 这5个类组成 。
如果以一张时序图来展示Catalina对Web应用的加载过程,那么将如图 3-3 所示 。
如图3-3 展示了Web应用核心加载过程,而非全部,接下来让我们详细分析一下每一个类的具体实现。
3.4.1 StandardHost
StandardHost加载Web应用(即StandardContext)的入口有两个,而且前面的时序图也很好的说明了这一点,其中的入口是在Catalina构造Server实例时,如果Host元素存在Context子元素(server.xml中),那么Context元素将会作为Host容器的子容器添加到Host实例当中 ,并且在Host启动时,由生命周期管理接口的start()方法启动(默认调用子容器的start()方法)。
<Host name=“localhost” appBase=“webapps”
unpackWARs=“true” autoDeploy=“true”>
<Context docBase=“myApp” path=“/myApp1” reloadable=“true”></Context>
</Host>
其中,docBase为Web应用的根目录的文件路径,path为Web应用的根请求地址,如上,假如我们Tomcat 地址的http://127.0.0.1:8080,那么,Web应用的根请求地址为127.0.0.1:8080/myApp1。
那我们来看一个例子,
2. 测试结果
通过此方式加载,尽管Tomcat 处理简单(当解析server.xml时并一并完成Context 创建),但对于使用者来说却并不是一种好的方式,毕竟没有人愿意每次部署新的Web应用或者删除旧的应用时,都必须修改一下server.xml文件 。
当然,如果部署的Web应用相对固定,且每个应用需要分别在特定的目录下进行管理,那么可以选择这种部署方式,此时,如果仅配置了Host,那么所有的Web应用需要旋转到同一个基础目录下。
另一个入口则是由HostConfig自动扫描部署目录,创建Context 实例并启动,这是大多数Web应用的加载方式,此部分将在3.4.2节详细的讲解 。
StandardHost的启动加载过程如下
- 为Host添加一个Value实现ErrorReportValue(我们可以通过修改Host的errorReportValueClass属性指定自己的错误处理Value),该类主要用于在服务器处理异常时输出错误页面,如果我们没有在web.xml中添加错误处理页面,Tomcat 返回的异常栈顶页面便是ErrorReportValue生成的 。
【注意】如果希望定制Web应用的错误页面,除了按照Servlet规范在web.xml中添加<error-page>外,还可以通过设置Host的errorReportValueClass属性实现,前者的作用范围是当前Web应用,后者是整个虚拟机,除非错误页面与具体的Web应用无关,否则不推荐使用此配置方式,当然,修改该配置还有一个重要的用途,出于安全考虑对外隐藏服务器縙,毕竟ErrorReportValue输出内容是包含了服务器信息的。
protected synchronized void startInternal() throws LifecycleException { // errorValve = org.apache.catalina.valves.ErrorReportValve String errorValve = getErrorReportValveClass(); if ((errorValve != null) && (!errorValve.equals(""))) { try { boolean found = false; // 从管道中获取所有阀门 Valve[] valves = getPipeline().getValves(); for (Valve valve : valves) { // 查找server.xml中是否配置了org.apache.catalina.valves.ErrorReportValve阀门 if (errorValve.equals(valve.getClass().getName())) { found = true; break; } } // 如果没有配置org.apache.catalina.valves.ErrorReportValve阀门 if(!found) { // 手动创建一个ErrorReportValve阀门加入到管道中 Valve valve = (Valve) Class.forName(errorValve).getDeclaredConstructor().newInstance(); getPipeline().addValve(valve); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString( "standardHost.invalidErrorReportValveClass", errorValve), t); } } // 调用StandardHost父类的ContainerBase的startInternal()方法启动虚拟机 。 super.startInternal(); }
管道-Pipeline
管道(Pipeline)其实属于一种设计模式,在Tomcat 中,它将不同的容器级别串联起来的通道,当请求进来时就可以通过管道进行流转处理,Tomcat中会有4个级别的容器,每个容器都会有一个属于自己的管道 。
不同级别的容器的管道完成的工作都不一样,每个管道要搭配阀门(Value)才能工作,Host容器的Pipeline默认以StandardHostValue作为基础阀门,这个阀门的主要处理逻辑是先将当前线程上下文加载器设置成Context容器的类加载器,让后面的Context 容器处理时使用该类加载器,然后调用子容器的Context的管道 。
如果有其他逻辑需要在Host容器级别处理,可以往该管道添加包含了逻辑的阀门,当Host管道被调用时会执行该阀门的逻辑 。
在理解管道的基础上,我们来看一个关于ErrorReportValve的例子 。 从源码中分析得出,我们可以在server.xml中配置ErrorReportValve阀门,如果不配置,则Tomcat会帮我们创建一个ErrorReportValve阀门,那我们自己配置一个试试。
- 配置ErrorReportValve阀门很简单。
<Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Valve className="org.apache.catalina.valves.ErrorReportValve"/> </Host> </Engine>
- 创建Servlet ,故意抛出异常
public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doGet(req,resp); System.out.println("doGet方法执行"); int i = 0 ; int j = 0; int c = i / j ; resp.getWriter().append("xxxxxx"); } }
- 将打好的包放到servelet-test-1.0.war放到webapps目录下。
- 测试
源码分析
在StandardWrapperValue的invoke()方法中异常处理打断点 。
这里有一个异常方法,我们进入看看 。
private void exception(Request request, Response response, Throwable exception) { request.setAttribute("javax.servlet.error.exception", exception); response.setStatus(500); response.setError(); }
在exception()方法中,做了两个非常重要的事情,将servlet抛出的异常堆栈放到request的javax.servlet.error.exception属性中,同时设置response的status为500。 做这个操作有什么用呢?
ErrorReportValve既然是阀门,我也在管本次请求曾经有多少故事,直接在ErrorReportValve的invoke()方法中打断点即可,流程处理肯定会流转到ErrorReportValve方法中。
public void invoke(Request request, Response response) throws IOException, ServletException { // 管道中的阀门就是通过invoke方法串联起来的 getNext().invoke(request, response); if (response.isCommitted()) { if (response.setErrorReported()) { // 之前没有报告错误,但我们无法写入错误页,因为响应已经提交。尝试刷新仍要写入客户端的任何数据 try { response.flushBuffer(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } // 立即关闭,向客户发出出错的信号 response.getCoyoteResponse().action(ActionCode.CLOSE_NOW, request.getAttribute(RequestDispatcher.ERROR_EXCEPTION)); } return; } // 我们在之前的exception()方法中,将异常堆栈信息保存到了request中 Throwable throwable = (Throwable) request.getAttribute(javax.servlet.error.exception); // 如果异步请求正在进行,并且在容器线程完成后不会结束,请不要在此处处理任何错误页面。 if (request.isAsync() && !request.isAsyncCompleting()) { return; } if (throwable != null && !response.isError()) { // 如果存在throwable,并且response并没有被设置错误状态码,并且response还没有提交,则重置response内容 response.reset(); response.sendError(500);// 设置错误状态码为500 } // 如果response被设置了暂停响应,此时因为要回写错误页面,因此要终止暂停响应 response.setSuspended(false); try { // 生成错误报告页面 report(request, response, throwable); } catch (Throwable tt) { ExceptionUtils.handleThrowable(tt); } }
我相信大家对异步处理这一块比较迷惑,如果正在处理异步请求,并且在容器线程完成后不会结束,此时不会处理任何错误页面 ,这个是什么意思呢?
深入isAsync()方法 ,发现最终用到了AsyncContext这个类,我在网上找找关于AsyncContext异步处理http请求的文章,有如下内容 。
AsyncContext
AsyncContext是Servlet 3.0提供的异步处理类,主要作用为释放Servlet 线程,让当前Servlet 线程去处理别的请求。
我们先对比下使用AsyncContext后的流程变化
Servlet 3.0 之前,一个普通 Servlet 的主要工作流程大致如下:
- 第一步,Servlet 接收到请求之后,可能需要对请求携带的数据进行一些预处理;
- 第二步,调用业务接口的某些方法,以完成业务处理;
- 第三步,根据处理的结果提交响应,Servlet 线程结束。
其中第二步的业务处理通常是最耗时的,这主要体现在数据库操作,以及其它的跨网络调用等,在此过程中,Servlet 线程一直处于阻塞状态,直到业务方法执行完毕。在处理业务的过程中,Servlet 资源一直被占用而得不到释放,对于并发较大的应用,这有可能造成性能的瓶颈。
Servlet 3.0 针对这个问题做了开创性的工作,现在通过使用 Servlet 3.0 的异步处理支持,之前的 Servlet 处理流程可以调整为如下的过程:
- 第一步,Servlet 接收到请求之后,可能首先需要对请求携带的数据进行一些预处理;
- 第二步,Servlet 线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器,
- 第三步,Servlet 还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有 ServletRequest 和 ServletResponse 对象的引用),或者将请求继续转发给其它 Servlet。
Servlet 线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回。
那我们如何模拟出异步情况下,不返回异常页面呢?
//@WebServlet( asyncSupported = true) public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doGet(req, resp); System.out.println("doGet方法执行"); // 释放http连接,转为异步 String name = req.getClass().getName(); System.out.println("servlet name is :" + name); try { Field field = req.getClass().getDeclaredField("request"); field.setAccessible(true); Object ob = field.get(req); System.out.println("request name is :" + ob.getClass().getName()); Field async = ob.getClass().getDeclaredField("asyncSupported"); async.setAccessible(true); async.set(ob,true); Object asyncSupported = async.get(ob); System.out.println("========asyncSupported====="+asyncSupported); } catch (Exception e) { e.printStackTrace(); } AsyncContext context = req.startAsync(); // 4秒才超时了,超时也会中断当前请求直接返回 context.setTimeout(4000L); int i = 0; int j = 0; int c = i / j; resp.getWriter().append("xxxxxx"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("doGet方法执行"); doGet(req, resp); } private class RequestFacade { } }
本来我们可以通过@WebServlet( asyncSupported = true)设置request的asyncSupported为true的,但是在经过层层封装后,Request不再是原来的Request对象,因此上面通过反射设置asyncSupported的值为true ,先来看一下如果不用设置asyncSupported值的情况。
@WebServlet( asyncSupported = true) public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doGet(req, resp); System.out.println("doGet方法执行"); AsyncContext context = req.startAsync(); context.setTimeout(4000L); int i = 0; int j = 0; int c = i / j; resp.getWriter().append("xxxxxx"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("doGet方法执行"); doGet(req, resp); } }
抛出异常。 为什么会抛出异常呢?
通过在代码中寻寻觅觅,发现是在调用 req.startAsync();时,request的asyncSupported为false,我们不是在Servlet中配置了@WebServlet( asyncSupported = true)注解了不?
在请求过程中,request的asyncSupported属性不是被设置为true了不?为什么在调用startAsync()方法时asyncSupported会变成false,带着疑问在代码中寻寻觅觅,终于在ApplicationFilterChain的internalDoFilter()方法中发现,如果Wrapper的asyncSupported为false,还是会将request的asyncSupported字段设置为false。
这就好办了,在servlet申明中添加async-supported标签 。如下。
<servlet> <servlet-name>my</servlet-name> <servlet-class>com.example.servelettest.HelloServlet</servlet-class> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>my</servlet-name> <url-pattern>/MyServlet</url-pattern> </servlet-mapping>
当然还有一种办法就之前所写的,直接通过反射将request的asyncSupported设置为true,也会得到相同的效果 。
接下来,开始测试 。
从测试结果来看,代码中已经抛出了java.lang.ArithmeticException: / by zero异常,而页面中却显示该网页无法正常动作,而如果想让错误页面无法正常显式,只需要(request.isAsync() && !request.isAsyncCompleting()) 返回true即可,最终也就是AsyncState中状态isAsync为true , isCompleting为true的状态,我们来看看AsyncState枚举类。
private enum AsyncState { DISPATCHED (false, false, false, false), STARTING (true, true, false, false), STARTED (true, true, false, false), MUST_COMPLETE (true, true, true, false), COMPLETE_PENDING(true, true, false, false), COMPLETING (true, false, true, false), TIMING_OUT (true, true, false, false), MUST_DISPATCH (true, true, false, true), DISPATCH_PENDING(true, true, false, false), DISPATCHING (true, false, false, true), MUST_ERROR (true, true, false, false), ERROR (true, true, false, false); private final boolean isAsync; private final boolean isStarted; private final boolean isCompleting; private final boolean isDispatching; private AsyncState(boolean isAsync, boolean isStarted, boolean isCompleting, boolean isDispatching) { this.isAsync = isAsync; this.isStarted = isStarted; this.isCompleting = isCompleting; this.isDispatching = isDispatching; } }
上面加粗状态即为不可返回状态,而我们调用startAsync()方法,实际上将AsyncState状态转化为STARTED,而此时isAsync 为true ,而isCompleting为false,因此才会进入不返回错误页面的逻辑。而AsyncState状态之间转换图如下。感兴趣的小伙伴可以自行研究,这里就不深入了。
上述代码在https://github.com/quyixiao/servelet-test
的version_startAsync分支 。
接下来,我们还是将HelloServlet恢复正常 。
public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doGet(req,resp); System.out.println("doGet方法执行"); int i = 0 ; int j = 0; int c = i / j ; resp.getWriter().append("xxxxxx"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("doGet方法执行"); doGet(req,resp); } }
通过测试,得到如下结果,idea中抛出异常,并且页面中展示错误堆栈信息。
那页面中的错误堆栈信息如何来的呢?我们进入report()方法 。
protected void report(Request request, Response response, Throwable throwable) { // 在之前的exception()中,已经将状态码设置为500 int statusCode = response.getStatus(); // 在如下3种情况下,不执行任何操作 // 1. 在1xx、2xx和3xx状态 // 2. 如果已写入任何内容 // 3. 如果响应未明确标记为错误且未报告该错误 if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) { return; } // filter方法将字符'<' 转化为 '<' , '>' 转化为 '>' , // '&' 转化为 '&' , " 转化为 '"' String message = RequestUtil.filter(response.getMessage()); if (message == null) { if (throwable != null) { // 如果response中没有message,并且throwable不为空,那从throwable中取 String exceptionMessage = throwable.getMessage(); if (exceptionMessage != null && exceptionMessage.length() > 0) { // 转化exceptionMessage中的 < ,> & , " message = RequestUtil.filter((new Scanner(exceptionMessage)).nextLine()); } } if (message == null) { message = ""; } } String reason = null; String description = null; // 根据当前语言环境获取LocalStrings_xxx.properties文件 // 如果是中国,则取LocalStrings_zh_CN.properties 文件,构建StringManager对象 StringManager smClient = StringManager.getManager( "org.apache.catalina.valves" request.getLocales()); response.setLocale(smClient.getLocale()); try { // 如果当前在中国,并且状态码为500 ,则取org.apache.catalina.valves包下的LocalStrings_zh_CN.properties文件 // 配置的http.500.reason和http.500.desc作为reason和description的值 reason = smClient.getString("http." + statusCode + ".reason"); description = smClient.getString("http." + statusCode + ".desc"); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } if (reason == null || description == null) { if (message.isEmpty()) { // 如果reason , description , message 都为空,则不做任何处理 return; } else { // 如果message不为空,而reason和description为空,则从org.apache.catalina.valves包下的LocalStrings_xxx.properties文件中取出 // errorReportValve.unknownReason和errorReportValve.noDescription 作为原因和description reason = smClient.getString("errorReportValve.unknownReason"); description = smClient.getString("errorReportValve.noDescription"); } } StringBuilder sb = new StringBuilder(); sb.append("<!doctype html><html lang=\""); sb.append(smClient.getLocale().getLanguage()).append("\">"); sb.append("<head>"); sb.append("<title>"); // 从org.apache.catalina.valves包下的LocalStrings_xxx.properties文件中取出 errorReportValve.statusHeader的值,其他相关取值,这里就不再赘述 sb.append(smClient.getString("errorReportValve.statusHeader", String.valueOf(statusCode), reason)); sb.append("</title>"); sb.append("<style type=\"text/css\">"); sb.append(TomcatCSS.TOMCAT_CSS); sb.append("</style>"); sb.append("</head><body>"); sb.append("<h1>"); sb.append(smClient.getString("errorReportValve.statusHeader", String.valueOf(statusCode), reason)).append("</h1>"); // 是否显示报告 , 我们可以通过 showReport 来控制 if (isShowReport()) { sb.append("<hr class=\"line\" />"); sb.append("<p><b>"); sb.append(smClient.getString("errorReportValve.type")); sb.append("</b> "); if (throwable != null) { sb.append(smClient.getString("errorReportValve.exceptionReport")); } else { sb.append(smClient.getString("errorReportValve.statusReport")); } sb.append("</p>"); if (!message.isEmpty()) { sb.append("<p><b>"); sb.append(smClient.getString("errorReportValve.message")); sb.append("</b> "); sb.append(message).append("</p>"); } sb.append("<p><b>"); sb.append(smClient.getString("errorReportValve.description")); sb.append("</b> "); sb.append(description); sb.append("</p>"); // 如果异常堆栈不为空,将异常堆栈封装到StringBuilder对象中 if (throwable != null) { String stackTrace = getPartialServletStackTrace(throwable); sb.append("<p><b>"); sb.append(smClient.getString("errorReportValve.exception")); sb.append("</b> <pre>"); sb.append(RequestUtil.filter(stackTrace)); sb.append("</pre></p>"); int loops = 0; Throwable rootCause = throwable.getCause(); while (rootCause != null && (loops < 10)) { stackTrace = getPartialServletStackTrace(rootCause); sb.append("<p><b>"); sb.append(smClient.getString("errorReportValve.rootCause")); sb.append("</b> <pre>"); sb.append(RequestUtil.filter(stackTrace)); sb.append("</pre></p>"); // In case root cause is somehow heavily nested rootCause = rootCause.getCause(); loops++; } sb.append("<p><b>"); sb.append(smClient.getString("errorReportValve.note")); sb.append("</b> "); sb.append(smClient.getString("errorReportValve.rootCauseInLogs")); sb.append("</p>"); } sb.append("<hr class=\"line\" />"); } //是否显示服务器信息 if (isShowServerInfo()) { sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>"); } sb.append("</body></html>"); try { try { response.setContentType("text/html"); response.setCharacterEncoding("utf-8"); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); if (container.getLogger().isDebugEnabled()) { container.getLogger().debug("status.setContentType", t); } } Writer writer = response.getReporter(); if (writer != null) { // If writer is null, it's an indication that the response has // been hard committed already, which should never happen writer.write(sb.toString()); response.finishResponse(); } } catch (IOException e) { // Ignore } catch (IllegalStateException e) { // Ignore } }
因为报告中可能带有服务器信息,有时我们不想将服务器及异常堆栈信息返回给前端,怎么办呢?我们可以在server.xml的Value中配置。 如果不想显示报告 ,则做如下配置
<Valve className=“org.apache.catalina.valves.ErrorReportValve” showReport=“false” />
如果想显示堆栈信息,但不想显示服务器相关信息,则在Value中配置showReport为false即可。
实际上Tomcat对整个请求的处理过程都在不同的级别的管道中流转,而对错误页面的处理其实就是在StandardHostValue阀门中,它调用对应的Context容器对请求处理后,根据请求对象的响应码,判断是否需要返回对应的错误页面,同时它还根据处理过程中发生的异常寻找对应的错误页面,这样就实现了Servlet规范中的错误页面的功能 。
接下来,我们继续跟进StandardHost的startInternal()方法 。
protected synchronized void startInternal() throws LifecycleException { // Start our subordinate components, if any // 启动下级组件,如果有的话 // 容器的类加载器 Loader loader = getLoaderInternal(); if ((loader != null) && (loader instanceof Lifecycle)) ((Lifecycle) loader).start(); // 容器的日志 logger = null; getLogger(); Manager manager = getManagerInternal(); if ((manager != null) && (manager instanceof Lifecycle)) ((Lifecycle) manager).start(); Cluster cluster = getClusterInternal(); if ((cluster != null) && (cluster instanceof Lifecycle)) ((Lifecycle) cluster).start(); Realm realm = getRealmInternal(); if ((realm != null) && (realm instanceof Lifecycle)) ((Lifecycle) realm).start(); DirContext resources = getResourcesInternal(); if ((resources != null) && (resources instanceof Lifecycle)) ((Lifecycle) resources).start(); // Start our child containers, if any // 如果在server.xml中配置了<Context/>节点,那么对于Host节点就存在children,这个时候就会启动context, 并且是通过异步启动的 Container children[] = findChildren(); List<Future<Void>> results = new ArrayList<Future<Void>>(); for (int i = 0; i < children.length; i++) { results.add(startStopExecutor.submit(new StartChild(children[i]))); } MultiThrowable multiThrowable = null; for (Future<Void> result : results) { try { result.get(); } catch (Throwable e) { log.error(sm.getString("containerBase.threadedStartFailed"), e); if (multiThrowable == null) { multiThrowable = new MultiThrowable(); } multiThrowable.add(e); } } if (multiThrowable != null) { throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"), multiThrowable.getThrowable()); } // Start the Valves in our pipeline (including the basic), if any if (pipeline instanceof Lifecycle) { ((Lifecycle) pipeline).start(); } // 这个时候会触发START_EVENT事件,会进行deployApps setState(LifecycleState.STARTING); // Start our thread // Engine容器启动一个background线程 threadStart(); }
调用父类ContainerBase的startInternal()方法启动虚拟主机,其处理主要分为如下几步 。
- 如果配置了集群组件Cluster,则启动。
- 如果配置了安全组件Realm,则启动。
- 启动子节点(即通过server.xml中的<Context> 创建的StandardContext实例)
- 启动Host持有的Pipeline组件
- 设置Host状态为STARTING,此时会触发START_EVENT生命周期事件,HostConfig监听该事件,扫描Web部署目录,对于部署目录文件,WAR 包 ,目录会自动创建StandardContext实例, 添加Host并启动 。
- 启动Host层级的后台任务处理,Cluster后台处理任务(包括部署变更检测,心跳)Realm 后台任务处理, Pipeline中的Value的后台任务处理(某些Value通过后台任务实现定期处理能力,如StuckThreadDetectionValve 用于定时检测耗时请求并输出 )
关于集群Cluster 和 Realm 相关的内容,我们在后面的博客再来分析,我们先分析server.xml中的<Context> 的创建。而pipeline默认为StandardPipeline, 我们进入其start()方法。
public abstract class LifecycleBase implements Lifecycle { private volatile LifecycleState state = LifecycleState.NEW; } public final synchronized void start() throws LifecycleException { if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) || LifecycleState.STARTED.equals(state)) { if (log.isDebugEnabled()) { Exception e = new LifecycleException(); log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e); } else if (log.isInfoEnabled()) { log.info(sm.getString("lifecycleBase.alreadyStarted", toString())); } return; } if (state.equals(LifecycleState.NEW)) { init(); } else if (state.equals(LifecycleState.FAILED)) { stop(); } else if (!state.equals(LifecycleState.INITIALIZED) && !state.equals(LifecycleState.STOPPED)) { invalidTransition(Lifecycle.BEFORE_START_EVENT); } try { setStateInternal(LifecycleState.STARTING_PREP, null, false); startInternal(); if (state.equals(LifecycleState.FAILED)) { // This is a 'controlled' failure. The component put itself into the // FAILED state so call stop() to complete the clean-up. stop(); } else if (!state.equals(LifecycleState.STARTING)) { // Shouldn't be necessary but acts as a check that sub-classes are // doing what they are supposed to. invalidTransition(Lifecycle.AFTER_START_EVENT); } else { setStateInternal(LifecycleState.STARTED, null, false); } } catch (Throwable t) { // This is an 'uncontrolled' failure so put the component into the // FAILED state and throw an exception. ExceptionUtils.handleThrowable(t); setStateInternal(LifecycleState.FAILED, null, false); throw new LifecycleException(sm.getString("lifecycleBase.startFail", toString()), t); } }
state的默认生命周期状态是LifecycleState.NEW,因此先进入其init()方法。
public final synchronized void init() throws LifecycleException { if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { setStateInternal(LifecycleState.INITIALIZING, null, false); initInternal(); setStateInternal(LifecycleState.INITIALIZED, null, false); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); setStateInternal(LifecycleState.FAILED, null, false); throw new LifecycleException( sm.getString("lifecycleBase.initFail",toString()), t); } }
在初始化方法中,设置当前状态为INITIALIZING,调用initInternal()方法,当调用完initInternal()方法后,如果没有抛出异常,则设置当前状态为INITIALIZED,如果抛出异常,则设置当前状态为FAILED,之后将调用stop()方法,关于组件的生命周期LifecycleState,先来看一下类结构。
public enum LifecycleState { NEW(false, null), INITIALIZING(false, Lifecycle.BEFORE_INIT_EVENT), INITIALIZED(false, Lifecycle.AFTER_INIT_EVENT), STARTING_PREP(false, Lifecycle.BEFORE_START_EVENT), STARTING(true, Lifecycle.START_EVENT), STARTED(true, Lifecycle.AFTER_START_EVENT), STOPPING_PREP(true, Lifecycle.BEFORE_STOP_EVENT), STOPPING(false, Lifecycle.STOP_EVENT), STOPPED(false, Lifecycle.AFTER_STOP_EVENT), DESTROYING(false, Lifecycle.BEFORE_DESTROY_EVENT), DESTROYED(false, Lifecycle.AFTER_DESTROY_EVENT), FAILED(false, null), /** * @deprecated Unused. Will be removed in Tomcat 9.0.x. The state transition * checking in {@link org.apache.catalina.util.LifecycleBase} * makes it impossible to use this state. The intended behaviour * can be obtained by setting the state to * {@link LifecycleState#FAILED} in * <code>LifecycleBase.startInternal()</code> */ @Deprecated MUST_STOP(true, null), /** * @deprecated Unused. Will be removed in Tomcat 9.0.x. The state transition * checking in {@link org.apache.catalina.util.LifecycleBase} * makes it impossible to use this state. The intended behaviour * can be obtained by implementing {@link Lifecycle.SingleUse}. */ @Deprecated MUST_DESTROY(false, null); private final boolean available; private final String lifecycleEvent; private LifecycleState(boolean available, String lifecycleEvent) { this.available = available; this.lifecycleEvent = lifecycleEvent; } /** * May the public methods other than property getters/setters and lifecycle * methods be called for a component in this state? It returns * <code>true</code> for any component in any of the following states: * <ul> * <li>{@link #STARTING}</li> * <li>{@link #STARTED}</li> * <li>{@link #STOPPING_PREP}</li> * <li>{@link #MUST_STOP}</li> * </ul> */ public boolean isAvailable() { return available; } public String getLifecycleEvent() { return lifecycleEvent; } }
从上面分析过程中,发现容器中的组件都有NEW,INITIALIZING,INITIALIZED,STARTING_PREP,STARTING,STARTED,STOPPING_PREP,STOPPING,STOPPED,DESTROYING,DESTROYED,FAILED 等状态,相应的伴随着BEFORE_INIT_EVENT,AFTER_INIT_EVENT,BEFORE_START_EVENT…等事件,状态是怎样流转,流转过程中又发送了哪些事件呢?请看下图。
状态之间的转变如下图所示
接下来,我们来看看事件的发送相关代码 。
private synchronized void setStateInternal(LifecycleState state, Object data, boolean check) throws LifecycleException { if (log.isDebugEnabled()) { log.debug(sm.getString("lifecycleBase.setState", this, state)); } if (check) { // Must have been triggered by one of the abstract methods (assume // code in this class is correct) // null is never a valid state if (state == null) { invalidTransition("null"); // Unreachable code - here to stop eclipse complaining about // a possible NPE further down the method return; } // Any method can transition to failed // startInternal() permits STARTING_PREP to STARTING // stopInternal() permits STOPPING_PREP to STOPPING and FAILED to // STOPPING // 例如 this.state 为 STARTING_PREP ,但state 为STOPPING_PREP 则抛出异常 // 也就是说STARTING_PREP 状态之后,一定是STARTING ,不可能是其他状态,如果是其他状态,则抛出异常 if (!(state == LifecycleState.FAILED || (this.state == LifecycleState.STARTING_PREP && state == LifecycleState.STARTING) || (this.state == LifecycleState.STOPPING_PREP && state == LifecycleState.STOPPING) || (this.state == LifecycleState.FAILED && state == LifecycleState.STOPPING))) { // No other transition permitted invalidTransition(state.name()); } } this.state = state; String lifecycleEvent = state.getLifecycleEvent(); if (lifecycleEvent != null) { fireLifecycleEvent(lifecycleEvent, data); } } private void invalidTransition(String type) throws LifecycleException { String msg = sm.getString("lifecycleBase.invalidTransition", type, toString(), state); // 抛出生命周期不正常异常 throw new LifecycleException(msg); }
这个方法的原理很简单,就是设置当前组件的状态,但是在设置之前做一定的较验,设置好后,调用相应的事件方法,而事件则来源于枚举LifecycleState,如INITIALIZING(false, Lifecycle.BEFORE_INIT_EVENT), 如果设置组件状态为INITIALIZING,则触发BEFORE_INIT_EVENT事件,接下来进入事件调用方法。
private LifecycleSupport lifecycle = new LifecycleSupport(this); protected void fireLifecycleEvent(String type, Object data) { lifecycle.fireLifecycleEvent(type, data); }
在这里,需要注意一点的是LifecycleSupport的构造函数传递了一个参数this,这一点需要注意。而private LifecycleSupport lifecycle = new LifecycleSupport(this);和 fireLifecycleEvent() 属于抽象类LifecycleBase,也就是继承了LifecycleBase的类都具有调用生命周期方法fireLifecycleEvent(),看看哪些类继承了LifecycleBase类,请看下图。
图片相关链接
https://www.processon.com/view/link/633d4d015653bb6a7b4b898f
这幅图也只是将LifecycleBase相关类他们之间的关系做了一个简单的描述,因为有70多个类,关系太过复杂,一些不常用的,就不画出来了。 详细类结构如下。
进入fireLifecycleEvent()方法 。
public void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(lifecycle, type, data); // 拿到组件的所有监听器 LifecycleListener interested[] = listeners; // ContextCOnfig for (int i = 0; i < interested.length; i++) interested[i].lifecycleEvent(event); }
再调用其lifecycleEvent()方法。以MemoryLeakTrackingListener为例,来分析一下内存泄漏监听器。
private Map<ClassLoader, String> childClassLoaders = new WeakHashMap<ClassLoader, String>(); private class MemoryLeakTrackingListener implements LifecycleListener { @Override public void lifecycleEvent(LifecycleEvent event) { if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) { if (event.getSource() instanceof Context) { Context context = ((Context) event.getSource()); childClassLoaders.put(context.getLoader().getClassLoader(), context.getServletContext().getContextPath()); } } } }
MemoryLeakTrackingListener 监听器主要辅助完成关于内存泄漏跟踪的工作,一般情况下,如果我们通过重启Tomcat 重启Web 应用,则不存在内存泄漏问题,但是如果不重启Tomcat而对Web 应用进行重新加载 ,则可能会导致内存泄漏,因为重载后可能会导致原来的某些
看看是什么原因导致 Tomcat 内存泄漏,这个要从热部署开始说起,因为Tomcat 提供了不必要的重启容器而只需要重启Web 应用以达到热部署的功能,其实是通过定义一个WebClassLoader 类加载器,当热部署时,就将原来的类加载器废弃并重新实例化一个WebappClassLoader类加载器,但这种方式可能存在内存泄漏问题。因为类加载器是一个结构复杂的对象,导致它不能被GC回收的可能性比较多,除了对类加载器对象引用可能导致其无法回收之外,对其加载的元数据(方法,类,字段等) ,有引用也可能会导致无法被GC 回收
Tomcat 的类加载器之间有父子关系,这里看启动类加载器BootstrapClassLoader和Web 应用类加载器WebappClassLoader ,在JVM 中BootstrapClassLoader 负责加载rt.jar 包的java.sql.DriverManager ,而WebappClassLoader 负责加载Web 应用中的Mysql 驱动包,其中 有一个很重要的步骤就是Mysql 的驱动类需要注册到DriverManager 中,即DriverManager.registerDriver (new Driver ) 它由Mysql 驱动包自动完成 。这样一来,Web应用进行热部署来操作时,如果没有将Mysql 的DriverManager 中反注册掉,则会导致 WebappclassLoader 无法回收,造成内存泄漏 。
接着讨论Tomcat 如何对内存泄漏进行监控,要判断WebappClassLoader 会不会导致内存泄漏,只须要判断WebappClassLoader 有没有被GC 回收
即可,在Java 中有一种引用叫弱引用,它能很好的判断WebappClassLoader 有没有被GC 回收掉,被弱引用关联的对象只能生存到下一次垃圾回收。发生之前,即如果某WebappClassLoader 对象只能被某个弱引用关联外还被其他的对象引用,那么WebappClassLoader 对象是不会被回收的。根据这些条件就可以判断是否有WebappClassLoader 发生内存泄漏 。
而findReloadedContextMemoryLeaks()方法则是具体实现。
public String[] findReloadedContextMemoryLeaks() { // 进行一次gc(),如果没有被回收掉,则证明存在内存泄漏 System.gc(); List<String> result = new ArrayList<String>(); for (Map.Entry<ClassLoader, String> entry : childClassLoaders.entrySet()) { ClassLoader cl = entry.getKey(); if (cl instanceof WebappClassLoaderBase) { if (!((WebappClassLoaderBase) cl).isStarted()) { result.add(entry.getValue()); } } } // 将泄漏的ClassLoader返回 return result.toArray(new String[result.size()]); }
Tomcat 是实现通过WeakHashMap来实现弱引用的,只须将WebappClassLoader对象放到WeakHashMap 中,例如 weakMap.put(“Loader1”,WebappClassLoader), 当WebappClassLoader 及其包含的元素没有被其他任何类加载器中的元素引用时,JVM 发生垃圾回收时则会把WebappClassLoader 对象回收。
Tomcat 中的每个Host 容器都会对应若干个应用,为了跟踪这些应用是否有内存泄漏,需要将对应的Context 容器注册到Host 容器中的WeakHashMap中,而这里讨论的监听MemoryLeakTrackingListenner 就负责Context 对应的WebappClassLoader 的注册工作 。
当然,实现了LifecycleListener接口的当然不止MemoryLeakTrackingListener,还有其他的类也实现了这个接口。 如下图所示。
tomcat帮我们定义的LifecycleListener,在使用上是很方便的。如JreMemoryLeakPreventionListener,则直接在server.xml配置文件中配置Listener即可,如下图所示。
当然,除了tomcat本身帮我们实现了LifecycleListener类外,我们也可以根据需要自定义监听器,如下。
public class MyTestLifecycleListener implements LifecycleListener { private static final Log log = LogFactory.getLog(VersionLoggerListener.class); @Override public void lifecycleEvent(LifecycleEvent event) { log.info("MyTestLifecycleListener type = " + event.getType() + ", data = " + event.getData()); } }
其实MyTestLifecycleListener没有做什么实际的业务,只是打印了一下事件,及调用本次事件的相应参数。
接下来,进入StandardPipeline的startInternal()方法。
protected synchronized void startInternal() throws LifecycleException { // Start the Valves in our pipeline (including the basic), if any Valve current = first; if (current == null) { current = basic; } while (current != null) { if (current instanceof Lifecycle) ((Lifecycle) current).start(); current = current.getNext(); } setState(LifecycleState.STARTING); }
看到这里,不知道大家有没有想起之前的一幅图,如下所示。
管道中有不同的阀门,如果阀门也实现了Lifecycle接口,则阀门也是需要启动的,因此调用所有实现了Lifecycle接口阀门的start()方法,在这里,有没有小伙伴发现,阀门之间是通过next指针来实现,也就是阀门的结构是链表结构。先来看setBasic()方法。
在这里,我们发现一个特点,每一个容器都有一个基本的阀门,StandardEngine的基础阀门是StandardEngineValve,StandardHost的基础阀门是StandardHostValve,StandardContext的基础阀门是StandardContextValve,StandardWrapper的基础阀门是StandardWrapperValve。
public void setBasic(Valve valve) { // Change components if necessary Valve oldBasic = this.basic; if (oldBasic == valve) return; // Stop the old component if necessary if (oldBasic != null) { if (getState().isAvailable() && (oldBasic instanceof Lifecycle)) { try { // 关闭阈门 ((Lifecycle) oldBasic).stop(); } catch (LifecycleException e) { log.error("StandardPipeline.setBasic: stop", e); } } if (oldBasic instanceof Contained) { try { ((Contained) oldBasic).setContainer(null); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } } } // Start the new component if necessary if (valve == null) return; if (valve instanceof Contained) { ((Contained) valve).setContainer(this.container); } if (getState().isAvailable() && valve instanceof Lifecycle) { try { // 启动阈门 ((Lifecycle) valve).start(); } catch (LifecycleException e) { log.error("StandardPipeline.setBasic: start", e); return; } } // Update the pipeline Valve current = first; while (current != null) { // 找到basic阈门,并替换掉 if (current.getNext() == oldBasic) { current.setNext(valve); break; } current = current.getNext(); } this.basic = valve; }
上面setBasic()方法做了三件事情
- 将旧的阈门停下
- 将新的阈门启动
- 替换掉旧的oldBasic阈门
不过大家注意,first指向队首阈门,basic指向队尾阈门。还有一点值得注意,getState().isAvailable(),那这个条件判断什么时候为true,什么时候为false呢?
从截图可以看出,当StandardPipeline的状态为STARTING,STARTED,STOPPING_PREP 时,对旧的阈门需要关闭,为什么呢?如果StandardPipeline的状态为STARTING,STARTED则说明StandardPipeline已经调用过start()方法,那么旧的oldBasic阈门肯定也已经调用了start()方法,为了安全起见,调用oldBasic的stop()方法,当StandardPipeline的状态为STOPPING_PREP时,说明还没有调用oldBasic的stop()方法,因此提前调用其stop()方法。
如果StandardPipeline的状态为STARTING,STARTED,STOPPING_PREP时,为什么新的baisc需要调用其start()方法呢? 如果状态为STARTING,STARTED,则说明StandardPipeline已经调用过start()方法了,所以新加入的baisc需要先启动, 但状态为STOPPING_PREP时,为什么也需要启动basic呢? 当StandardPipeline处于STOPPING_PREP时,此时StandardPipeline可能还需要调用其fireLifecycleEvent()方法,到真正的停止还有一定时间,StandardPipeline还能提供一定时间的服务,因此新baisc还是需要启动的。 这是我对这个方法的理解 。
接下来看其另外一个重要的方法 。
public void addValve(Valve valve) { // Validate that we can add this Valve if (valve instanceof Contained) ((Contained) valve).setContainer(this.container); // Start the new component if necessary if (getState().isAvailable()) { if (valve instanceof Lifecycle) { try { // 如果StandardPipeline的状态为STARTING,STARTED,STOPPING_PREP // 新加入的阈门需要先启动 ((Lifecycle) valve).start(); } catch (LifecycleException e) { log.error("StandardPipeline.addValve: start: ", e); } } } // Add this Valve to the set associated with this Pipeline // 如果first == null ,则表示StandardPipeline 除了basic外,没有其他阈门 if (first == null) { first = valve; valve.setNext(basic); } else { Valve current = first; // 将新加入的阈门添加到baisc前 while (current != null) { if (current.getNext() == basic) { current.setNext(valve); valve.setNext(basic); break; } current = current.getNext(); } } container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve); }
不知道上面的算法大家看明白没有,其主要目的是将新加入的阈门添加到baisc前。
我相信此时,你对addValue()肯定有了自己的理解 。 接下来,以下图为例,看阈门是如何被添加到管道的。
如果要理解ErrorReportValve和AccessLogValve被添加到StandardHost的StandardPipeline中,需要去看之前的博客 Tomcat 源码解析一初识 ,这篇博客中详细的分析了Digester的实现,也就是Tomcat如何解析XML,这也是我见过的最好的解析XML的方式,当然,对于<Host>解析,当然要看HostRuleSet配置。
分析一下
<Valve className=“org.apache.catalina.valves.AccessLogValve” directory=“logs”
prefix=“localhost_access_log.” suffix=“.txt”
pattern=“%h %l %u %t “%r” %s %b” />的实现过程 。
反射创建org.apache.catalina.valves.AccessLogValve对象,再将配置中的directory,prefix,pattern设置到AccessLogValve对象中,再调用StandardHost的addValue()方法,而StandardHost本身并没有实现addValve()方法,而是调用其父类ContainerBase继承而来的addValve()方法 。
public synchronized void addValveValve valve) { pipeline.addValve(valve); }
而addValve()方法的用意再明显不过了,就是调用其StandardPipeline的addValve()方法,而我们所知道的StandardEngine, StandardHost, StandardContext,StandardWrapper都继承了ContainerBase ,因此,四大组件都可以设置阈门,并通过addValue()方法添加到其相应的管道中。
我们大费周章的来分析阈门,到底有什么用呢?这个问题需要在下一篇博客再来分析了,就是整个Http请求(到底做了哪些事情,如何根据uri 找到对应的servlet的,也需要大量篇幅来分析)。这里就不做过多的深入。
我们进入threadStart()方法。
protected void threadStart() { if (thread != null) return; System.out.println(this.getInfo() + "的backgroundProcessorDelay等于=" + backgroundProcessorDelay); if (backgroundProcessorDelay <= 0) return; threadDone = false; String threadName = "ContainerBackgroundProcessor[" + toString() + "]"; // ContainerBackgroundProcessor线程每隔一段时间会调用容器内的backgroundProcess方法,并且会调用子容器的backgroundProcess方法 // 方法 threadStart 传递一个 ContainerBackgroundProcessor 对象创建一个新线 程。ContainerBackgroundProcessor 实现了 java.lang.Runnable 接口 thread = new Thread(new ContainerBackgroundProcessor(), threadName); thread.setDaemon(true); thread.start(); }
默认情况下只有Engine的backgroundProcessorDelay大于0,为10。这一点从StandardEngine的构造函数中体现。
public StandardEngine() { super(); pipeline.setBasic(new StandardEngineValve()); /* Set the jmvRoute using the system property jvmRoute */ try { setJvmRoute(System.getProperty("jvmRoute")); } catch(Exception ex) { log.warn(sm.getString("standardEngine.jvmRouteFail")); } // By default, the engine will hold the reloading thread backgroundProcessorDelay = 10; }
也就是说,虽然每个容器在启动的时候都会走到当前方法,但是只有Engine能继续往下面去执行但是其他容器是可以配置backgroundProcessorDelay属性的,只要配置了大于0,那么这个容器也会单独开启一个backgroundProcessor线程一个上下文容器需要其它组件如加载器和管理器的支持。这些组件通常需要一个单独的线程来处理后台过程(background processing)。
例如,加载器通过一 个线程检查类文件和 JAR 文件的时间戳来支持自动重载。管理器需要一个线程来检查它管理的 Session 对象过期时间。在 Tomcat4 中,这些组件都有自己的线程。为了节省资源,Tomcat 使用了一种不同的方式来处理。所有的后台过程都分享 同一个线程。如果一个组件或者是容器需要定期的来执行操作,它需要做的是将 这些代码写入到 backgroundProcess 方法即可。共享线程有 ContainerBase 对象创建,ContainerBase在他的start方法中调用 threadStart 方法。
接着,我们进入ContainerBackgroundProcessor类。
protected class ContainerBackgroundProcessor implements Runnable { @Override public void run() { Throwable t = null; String unexpectedDeathMessage = sm.getString( "containerBase.backgroundProcess.unexpectedThreadDeath", Thread.currentThread().getName()); try { while (!threadDone) { try { // 对于Engine而言,每次默认睡眠10秒,backgroundProcessorDelay的默认值为10 Thread.sleep(backgroundProcessorDelay * 1000L); } catch (InterruptedException e) { // Ignore } if (!threadDone) { // 获取当前的容器 Container parent = (Container) getMappingObject(); ClassLoader cl = Thread.currentThread().getContextClassLoader(); System.out.println("ContainerBackgroundProcessor在运行"+ parent.getName()); if (parent.getLoader() != null) { System.out.println(parent.getName() + "有loader"); cl = parent.getLoader().getClassLoader(); } // 执行子容器的background processChildren(parent, cl); } } } catch (RuntimeException e) { t = e; throw e; } catch (Error e) { t = e; throw e; } finally { if (!threadDone) { log.error(unexpectedDeathMessage, t); } } }
ContainerBackgroundProcessor 是 ContainerBase 的内部类,在他的 run 方法 里,有一个 while 循环定期的调用它的 processChildren 方法。processChildren 调用 backgroundProcess 来处理它的每个孩子的 processChildren 方法。要实现 backgroundProcess 方法,以 ContainerBase 的子类可以有一个线程来周期性的 执行任务,例如检查时间戳或者 Session 对象的终结时间。
protected void processChildren(Container container, ClassLoader cl) { try { if (container.getLoader() != null) { Thread.currentThread().setContextClassLoader (container.getLoader().getClassLoader()); } container.backgroundProcess(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error("Exception invoking periodic operation: ", t); } finally { Thread.currentThread().setContextClassLoader(cl); } Container[] children = container.findChildren(); for (int i = 0; i < children.length; i++) { if (children[i].getBackgroundProcessorDelay() <= 0) { // 调用子容器的backgroundProcess方法, delay小于0才调用,如果大于0,则该容器会有自己单独的background线程 processChildren(children[i], cl); } } }
StandardEngine,StandardHost,StandardContext,StandardWrapper都实现了ContainerBase类,如下图所示 。
除了StandardContext和StandardWrapper实现了backgroundProcess()方法,其他的容器并没有实现backgroundProcess()方法。
因此,我们来看backgroundProcess()的一般实现。
public void backgroundProcess() { // 如果当前状态不是STARTING , STARTED,STOPPING_PREP ,则不做任何处理 if (!getState().isAvailable()) return; // 如果配置了集群Cluster标签,则调用其backgroundProcess()方法 Cluster cluster = getClusterInternal(); if (cluster != null) { try { cluster.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e); } } // 热加载 Loader loader = getLoaderInternal(); // Context.webapploader if (loader != null) { try { loader.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e); } } // 处理session Manager manager = getManagerInternal(); if (manager != null) { try { manager.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e); } } Realm realm = getRealmInternal(); if (realm != null) { try { realm.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e); } } Valve current = pipeline.getFirst(); while (current != null) { try { // 遍历容器的所有管道,调用其backgroundProcess()方法 current.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e); } current = current.getNext(); } // PERIODIC_EVENT事件 fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); }
如前所述,Catalina的容器支持定期执行自身及其子容器的后台处理过程(该机制位置所有容器的父类ContainerBase中,默认情况下由Engine维护后台任务处理线程),具体处理过程在容器的backgroundProcess() 方法中定义 , 该机制常用于定时扫描Web应用的变更,并进行重新加载后台任务处理完之后,将触发PERIODIC_EVENT事件当HostConfig 接收到PERIODIC_EVENT 事件后,会检测守护资源的变更情况,如果发生了变更,将重新加载或者部署应用以及更新资源的最后修改时间。【注意】重新加载和重新部署的区别在于,前者是针对同一个Context对象的重启,而后者是重新创建了一个Context对象,Catalina中,同时守护两类资源 。以区别是重新加载应用还是重新部署应用,如Context描述文件变更,则需要重新部署应用,而web.xml文件变更时,则需要重新加载Context 即可 。
那先来看热加载的backgroundProcess()方法实现。
public void backgroundProcess() { // StandardContext 定义了 reloadable 属性来标识是否支持应用程序的重加载。 当允许重加载的时候, // 当 web.xml 或者 WEB-INF/classes 目录下的文件被改变的 时候会重加载。 if (reloadable && modified()) { System.out.println(container.getInfo()+"触发了热加载"); try { Thread.currentThread().setContextClassLoader (WebappLoader.class.getClassLoader()); if (container instanceof StandardContext) { ((StandardContext) container).reload(); } } finally { if (container.getLoader() != null) { Thread.currentThread().setContextClassLoader (container.getLoader().getClassLoader()); } } } else { closeJARs(false); } }
从上面方法可以看出,热加载只对于StandardContext有用 。 而reloadable默认为false,因此热加载的条件先要在Context标签中配置reloadable为true ,另外需要判断classLoader加载的路径发生了修改,那我们来看modified()方法的实现。
public boolean modified() { if (log.isDebugEnabled()) log.debug("modified()"); // Checking for modified loaded resources // 当前已经被加载了的class的路径 int length = paths.length; // A rare race condition can occur in the updates of the two arrays // It's totally ok if the latest class added is not checked (it will // be checked the next time // 当前已经被加载了的类对应的文件或jar的最近修改时间 int length2 = lastModifiedDates.length; if (length > length2) length = length2; // 遍历已经被加载了的 for (int i = 0; i < length; i++) { try { // 当前这个文件的最近修改时间 long lastModified = ((ResourceAttributes) resources.getAttributes(paths[i])) .getLastModified(); // 如果和之前的不相等 if (lastModified != lastModifiedDates[i]) { if( log.isDebugEnabled() ) log.debug(" Resource '" + paths[i] + "' was modified; Date is now: " + new java.util.Date(lastModified) + " Was: " + new java.util.Date(lastModifiedDates[i])); return (true); } } catch (NamingException e) { // 如果没有找到这个文件,则文件被删掉了 log.error(" Resource '" + paths[i] + "' is missing"); return (true); } } // 当前应用的jar的个数 length = jarNames.length; // Check if JARs have been added or removed // 检查是否有jar包添加或删除 if (getJarPath() != null) { try { // 当前存在的jar包 NamingEnumeration<Binding> enumeration = resources.listBindings(getJarPath()); int i = 0; while (enumeration.hasMoreElements() && (i < length)) { NameClassPair ncPair = enumeration.nextElement(); String name = ncPair.getName(); // Ignore non JARs present in the lib folder if (!name.endsWith(".jar")) continue; if (!name.equals(jarNames[i])) { // Missing JAR log.info(" Additional JARs have been added : '" + name + "'"); return (true); } i++; } if (enumeration.hasMoreElements()) { while (enumeration.hasMoreElements()) { NameClassPair ncPair = enumeration.nextElement(); String name = ncPair.getName(); // Additional non-JAR files are allowed // 新增了jar包 if (name.endsWith(".jar")) { // There was more JARs log.info(" Additional JARs have been added"); return (true); } } } else if (i < jarNames.length) { // There was less JARs log.info(" Additional JARs have been added"); return (true); } } catch (NamingException e) { if (log.isDebugEnabled()) log.debug(" Failed tracking modifications of '" + getJarPath() + "'"); } catch (ClassCastException e) { log.error(" Failed tracking modifications of '" + getJarPath() + "' : " + e.getMessage()); } } // No classes have been modified return (false); }
我相信通过注释来理解上面代码就很简单了, 先遍历所有的class文件判断他的最后修改时间与当前文件的修改时间是否一致,如果不一致,则表示class文件被修改过,需要重新加载,当然,如果class文件被删除了,导致异常时,也说明class文件被修改了,需要重新加载,另外是对jar包的判断,以jar包为判断条件判断是否需要重新加载分三种情况,第一种情况判断jar包的名字是否和之前加载时的名字是否一致,如果名字不一致,则证明修改过,第二种情况,如果包的名字都没有发生修改,
但jar包的个数增加,则也需要重新加载,当然还有一种情况,比如将原来的xxx.jar包名改成了xxx.jar1,此时文件个数并没有减少,但jar包个数减少了,因此也需要重新加载。
我们以servelet-test-1.0项目为例,来分析jarNames和lastModifiedDates的来源。
WebappClassLoaderBase
synchronized void addJar(String jar, JarFile jarFile, File file) throws IOException { if (jar == null) return; if (jarFile == null) return; if (file == null) return; if (log.isDebugEnabled()) log.debug("addJar(" + jar + ")"); int i; if ((jarPath != null) && (jar.startsWith(jarPath))) { // jarPath为/WEB-INF/lib // jar为/WEB-INF/lib/commons-codec-1.11.jar // 则jarName为commons-codec-1.11.jar String jarName = jar.substring(jarPath.length()); while (jarName.startsWith("/")) jarName = jarName.substring(1); // 把当前这个jar的名字添加到jarNames数组中 String[] result = new String[jarNames.length + 1]; for (i = 0; i < jarNames.length; i++) { result[i] = jarNames[i]; } result[jarNames.length] = jarName; // 将jar包名加入到jarNames中 jarNames = result; } try { // Register the JAR for tracking // 获取jar包的最后修改时间 long lastModified = ((ResourceAttributes) resources.getAttributes(jar)) .getLastModified(); // 把当前这个jar的路径添加到paths数组中 String[] result = new String[paths.length + 1]; for (i = 0; i < paths.length; i++) { result[i] = paths[i]; } result[paths.length] = jar; paths = result; // 把当前这个jar的lastModified添加到lastModifiedDates数组中 long[] result3 = new long[lastModifiedDates.length + 1]; for (i = 0; i < lastModifiedDates.length; i++) { result3[i] = lastModifiedDates[i]; } // 将jar包的最后修改时间添加到lastModifiedDates中 result3[lastModifiedDates.length] = lastModified; lastModifiedDates = result3; } catch (NamingException e) { // Ignore } // If the JAR currently contains invalid classes, don't actually use it // for classloading if (!validateJarFile(file)) return; JarFile[] result2 = new JarFile[jarFiles.length + 1]; for (i = 0; i < jarFiles.length; i++) { result2[i] = jarFiles[i]; } result2[jarFiles.length] = jarFile; jarFiles = result2; // Add the file to the list File[] result4 = new File[jarRealFiles.length + 1]; for (i = 0; i < jarRealFiles.length; i++) { result4[i] = jarRealFiles[i]; } result4[jarRealFiles.length] = file; jarRealFiles = result4; }
这段代码的原理是很简单的,但什么时候调用addJar()方法呢?请看下图。
从上图中得知。在StandardContext.startInternal()->WebappLoader.start()->WebappLoader.setRepositories()->WebappLoader.addJar()方法,真正原因是StandardContext的start()方法调用时会调用其类加载器加载jar包,在加载过程中保存了jar包的最后修改时间,大家一定要清楚,目前我们还在分析StandardHost的start()方法,就已经这样复杂了。 因此这篇博客可能会很杂,很长,但如果没有这些基础知识,你分析再多的http请求,nio,这些都没有意义,最多也只能管中窥豹只见一斑。
再来看class文件的最后修改时间保存到lastModifiedDates中。
从图中可以看出,在调用findResourceInternal()方法时,会将/com/example/servelettest/HelloServlet.class文件的最后修改时间保存到lastModifiedDates中去,而findResourceInternal()方法在哪里调用的呢?
大家不知道看到我截图的用意没有,Container[] children = context.findChildren();,我们知道四大组件中,StandardContext的Child是不是StandardWrapper,而StandardWrapper可以看作是Servlet,而servelet-test-1.0项目下有HelloServlet。
现在终于知道lastModifiedDates存储的是哪些文件的修改时间了吧。 所以你不要天真的以为修改HttpUtil的class文件会导致tomcat热部署,tomcat只对jar包,servlet文件,像jsp文件最终也被编译成servlet文件,这些class做修改时,才会导致热布署。像工具类,实体,这些文件修改,是不会导致热布署的。
接下来看StandardContext的reload()方法
public synchronized void reload() { // Validate our current component state if (!getState().isAvailable()) throw new IllegalStateException (sm.getString("standardContext.notStarted", getName())); if(log.isInfoEnabled()) log.info(sm.getString("standardContext.reloadingStarted", getName())); // Stop accepting requests temporarily. // 设置为暂停 setPaused(true); try { // 停止StandardContext stop(); } catch (LifecycleException e) { log.error( sm.getString("standardContext.stoppingContext", getName()), e); } try { // 启动StandardContext start(); } catch (LifecycleException e) { log.error( sm.getString("standardContext.startingContext", getName()), e); } // 暂停结束 setPaused(false); if(log.isInfoEnabled()) log.info(sm.getString("standardContext.reloadingCompleted", getName())); }
设置为暂停,会导致正在处理的请求进入睡眠。
当然stop()方法,start()方法,当真正分析StandardContext的启动和停止时再进行分析了。
public void backgroundProcess() { // count从0开始,每6次处理一下过期session,processExpiresFrequency默认值为6 count = (count + 1) % processExpiresFrequency; // count为0时才会处理过期的Session if (count == 0) processExpires(); }
在分析session过期处理之前,先来分析session是何时添加的。 在代码中寻寻觅觅,找到了add(Session session) 方法,在方法中打一个断点,据我所知,凡是jsp页面的访问肯定有session,因此在浏览器中访问。
默认会访问index.jsp
此时add(Session session)的断点生效了。
而什么时候会调用getPageContext(Servlet servlet, ServletRequest request, ServletResponse response, String errorPageURL, boolean needsSession, int bufferSize, boolean autoflush) 方法呢?其实是在生成的jsp中调用了此方法。
综合这些原因,终于知道在servlet调用过程中会生成Session并加入到sessions中。
接下来看processExpires()方法的处理逻辑
public void processExpires() { long timeNow = System.currentTimeMillis(); Session sessions[] = findSessions(); int expireHere = 0 ; if(log.isDebugEnabled()) log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length); for (int i = 0; i < sessions.length; i++) { // isValid放在中会进行过期的销毁处理,消费成功则session不合法 if (sessions[i]!=null && !sessions[i].isValid()) { // 过期session个数计数 expireHere++; } } long timeEnd = System.currentTimeMillis(); if(log.isDebugEnabled()) // 打印本次session处理时间及处理session个数 log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere); processingTime += ( timeEnd - timeNow ); } public Session[] findSessions() { return sessions.values().toArray(new Session[0]); }
processExpires()方法的原理很简单,就是遍历所有的session并调用其isValid()方法,如果session已经失效,则记录到expireHere中,并打印出来。所以真正session过期方法在isValid()中,进入isValid()方法。
有3种类型的session,我们以StandardSession为例进行分析。
public boolean isValid() { // 在调用expire(true)时已经被设置为失效,则直接返回当前session已经失效 if (!this.isValid) { return false; } // 正在进行过期处理 if (this.expiring) { return true; } // 如果开启了session 活跃数计数,并且当前session 有正在处理的请求, if (ACTIVITY_CHECK && accessCount.get() > 0) { return true; } // 设置的过期时间 if (maxInactiveInterval > 0) { long timeNow = System.currentTimeMillis(); int timeIdle; if (LAST_ACCESS_AT_START) { timeIdle = (int) ((timeNow - lastAccessedTime) / 1000L); } else { timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L); } if (timeIdle >= maxInactiveInterval) { expire(true); } } return this.isValid; }
查看上面的代码,主要就是通过对比当前时间和上次访问的时间差是否大于了最大的非活动时间间隔,如果大于就会调用expire(true)方法对session进行超期处理。这里需要注意一点,默认情况下LAST_ACCESS_AT_START为false,读者也可以通过设置系统属性的方式进行修改,而如果采用LAST_ACCESS_AT_START的时候,那么请求本身的处理时间将不算在内。比如一个请求处理开始的时候是10:00,请求处理花了1分钟,那么如果LAST_ACCESS_AT_START为true,则算是否超期的时候,是从10:00算起,而不是10:01。
StandardSession
public void access() { this.thisAccessedTime = System.currentTimeMillis(); if (ACTIVITY_CHECK) { accessCount.incrementAndGet(); } } /** * End the access. */ public void endAccess() { isNew = false; /** * The servlet spec mandates to ignore request handling time * in lastAccessedTime. */ if (LAST_ACCESS_AT_START) { this.lastAccessedTime = this.thisAccessedTime; this.thisAccessedTime = System.currentTimeMillis(); } else { this.thisAccessedTime = System.currentTimeMillis(); this.lastAccessedTime = this.thisAccessedTime; } if (ACTIVITY_CHECK) { accessCount.decrementAndGet(); } }
先不来管代码什么时候调用这两个方法,要知道的是请求开始处理时会调用access()方法,请求处理结束时会调用endAccess()方法,当请求刚开始处理时thisAccessedTime= System.currentTimeMillis();,如果LAST_ACCESS_AT_START为true , lastAccessedTime = thisAccessedTime = 请求刚开始处理时间 , 而timeIdle = (int) ((timeNow - lastAccessedTime) / 1000L); 不就是当前时间 - 请求刚开始的处理时间不? 如果10点发送一个请求,处理花费1分钟,那么lastAccessedTime = 10点,而不是10:01 分。 如果LAST_ACCESS_AT_START为false,情况相反,这里就不分析了。
如果ACTIVITY_CHECK为true时,通过accessCount记录当前正在处理的请求数,原理就很简单了,access()调用时accessCount ++ , endAccess()方法调用时accessCount – ,因此只要ACTIVITY_CHECK 为true 并且 accessCount.get() > 0 , 则session不能失效 。
接下来看access()和endAccess()方法何里开始调用呢?在session获取时,会访问access()方法。
当然啦,也是通过jsp生成的servlet代码pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true); 方法调用时访问的access()方法 。
在servlet调用结束后,request.recycle(); 在回收资源的时候调用了endAccess(),关于request 请求的整体流程,我们留到下一篇博客分析。这里先简单介绍。
接下来,我们来看isValid()的另外一个参数,maxInactiveInterval session的有效时间 。
从上图中可以看出maxInactiveInterval = StandardContext的sessionTimeout * 60 ,而sessionTimeout 默认值为30 ,因此maxInactiveInterval的默认值为1800,转化为时间为30分钟,这也是session有效期为30分钟的由来 。
从上图中可以得知,当session已经存在,则会从session存储文件中读取 /Users/quyixiao/gitlab/tomcat/work/Catalina/localhost/servelet-test-1.0/SESSIONS.ser,因此maxInactiveInterval的另外一种来源于SESSIONS.ser文件的读取 。 那SESSIONS.ser文件又是什么时候存储的呢?
如何来测试呢?
- 先启动tomcat
- 访问带有jsp页面
- 查看文件生成情况 ,如下图所示
当停止程序时调用链的调用关系如下图所示 。
【logstack】 run:Catalina$CatalinaShutdownHook:953 => stop:Catalina:807 => stop:LifecycleBase:220 => stopInternal:StandardServer:789 => stop:LifecycleBase:220 => stopInternal:StandardService:520 => stop:LifecycleBase:220 => stopInternal:ContainerBase:1317 => :ContainerBase$StopChild:1775 ========StopChild======= 【logstack】 run:Thread:748 => run:ThreadPoolExecutor$Worker:624 => runWorker:ThreadPoolExecutor:1149 => run:FutureTask:-1 => run$$$capture:FutureTask:266 => call:ContainerBase$StopChild:1770 => call:ContainerBase$StopChild:1782 => stop:LifecycleBase:220 => stopInternal:ContainerBase:1317 => :ContainerBase$StopChild:1775 ========StopChild======= 【logstack】 run:FutureTask:-1 => run$$$capture:FutureTask:266 => call:ContainerBase$StopChild:1770 => call:ContainerBase$StopChild:1782 => stop:LifecycleBase:220 => stopInternal:StandardContext:6071 => stop:LifecycleBase:220 => stopInternal:StandardManager:530 => unload:StandardManager:355 => doUnload:StandardManager:391 =doUnload====/Users/quyixiao/gitlab/tomcat/work/Catalina/localhost/servelet-test-1.0/SESSIONS.ser
从方法调用链关系分析,最终因为CatalinaShutdownHook的钩子函数的调用,才导致SESSIONS.ser生成 。 CatalinaShutdownHook又是何时生成的呢?作用又是怎样子的呢?
protected class CatalinaShutdownHook extends Thread { @Override public void run() { try { if (getServer() != null) { Catalina.this.stop(); } } catch (Throwable ex) { ExceptionUtils.handleThrowable(ex); log.error(sm.getString("catalina.shutdownHookFail"), ex); } finally { // If JULI is used, shut JULI down *after* the server shuts down // so log messages aren't lost LogManager logManager = LogManager.getLogManager(); if (logManager instanceof ClassLoaderLogManager) { ((ClassLoaderLogManager) logManager).shutdown(); } } } }
当程序被关闭时,会自顶向下调用所有组件的stop()方法,最终调用StandardContext的stop()方法时,会生成SESSIONS.ser文件 。
Runtime.addShutdownHook解释
如果你想在jvm关闭的时候进行内存清理、对象销毁等操作,或者仅仅想起个线程然后这个线程不会退出,你可以使用Runtime.addShutdownHook。
这个方法的作用就是在JVM中增加一个关闭的钩子。
当程序正常退出、系统调用 System.exit方法或者虚拟机被关闭时才会执行系统中已经设置的所有钩子,当系统执行完这些钩子后,JVM才会关闭。
所谓钩子,就是一个已初始化但并不启动的线程。JVM退出通常通过两种事件。
- 程序正常退出,例如最后一个非守护进程退出、使用System.exit()退出等
- 程序异常退出,例如使用Ctrl+C触发的中断、用户退出或系统关闭等系统事件等 该方法的说明如下
public class ShutdownHook extends Thread{ @Override public void run() { System.out.println("=========================="); } } public class ShutdownHookTest { public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new ShutdownHook()); for(int i = 0 ;i < 1000;i ++){ try { Thread.sleep(1000); System.out.println("for 循环执行 " + i); } catch (InterruptedException e) { e.printStackTrace(); } } } }
当直接点击idea的终止程序按钮,会执行ShutdownHook钩子函数 。
kill -3 33617 关闭程序
kill -3 pid 无法关闭程序,而钩子函数并没有执行。
kill -15 pid 时,程序关闭,钩子函数执行。
kill -9 pid 时,程序关闭,钩子函数不执行。
通过上面的测试,我相信大家明白一个现象,假如用jsp开发的一个项目,用户登陆信息保存在session中,当重启项目时,如果用kill -9 杀死项目,再启动项目,所有已经登录的用户需要重新登录,假如通过tomcat自带的脚本先catalina.sh -stop,再catalina.sh -start时,登陆的用户是不需要重新登陆的,因为catalina.sh -stop 会将session持久化文件中,tomcat再次启动时会加载持久化的session信息,我相信通过这个例子,你应该理解其中的原理了。
接下来看session的过程处理方法
public void expire(boolean notify) { // Check to see if session has already been invalidated. // Do not check expiring at this point as expire should not return until // isValid is false if (!isValid) return; // 并发控制 synchronized (this) { // Check again, now we are inside the sync so this code only runs once // Double check locking - isValid needs to be volatile // The check of expiring is to ensure that an infinite loop is not // entered as per bug 56339 // 如果已经失效了,则返回 if (expiring || !isValid) return; if (manager == null) return; // Mark this session as "being expired" expiring = true; // Notify interested application event listeners // FIXME - Assumes we call listeners in reverse order Context context = (Context) manager.getContainer(); // The call to expire() may not have been triggered by the webapp. // Make sure the webapp's class loader is set when calling the // listeners ClassLoader oldTccl = null; if (context.getLoader() != null && context.getLoader().getClassLoader() != null) { oldTccl = Thread.currentThread().getContextClassLoader(); if (Globals.IS_SECURITY_ENABLED) { PrivilegedAction<Void> pa = new PrivilegedSetTccl( context.getLoader().getClassLoader()); AccessController.doPrivileged(pa); } else { Thread.currentThread().setContextClassLoader( context.getLoader().getClassLoader()); } } try { // 触发Session过期事件 Object listeners[] = context.getApplicationLifecycleListeners(); if (notify && (listeners != null)) { HttpSessionEvent event = new HttpSessionEvent(getSession()); for (int i = 0; i < listeners.length; i++) { int j = (listeners.length - 1) - i; // 如果listeners非HttpSessionListener ,则跳过 if (!(listeners[j] instanceof HttpSessionListener)) continue; HttpSessionListener listener = (HttpSessionListener) listeners[j]; try { context.fireContainerEvent("beforeSessionDestroyed", listener); // session销毁事件 listener.sessionDestroyed(event); context.fireContainerEvent("afterSessionDestroyed", listener); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); try { context.fireContainerEvent( "afterSessionDestroyed", listener); } catch (Exception e) { // Ignore } manager.getContainer().getLogger().error (sm.getString("standardSession.sessionEvent"), t); } } } } finally { if (oldTccl != null) { if (Globals.IS_SECURITY_ENABLED) { PrivilegedAction<Void> pa = new PrivilegedSetTccl(oldTccl); AccessController.doPrivileged(pa); } else { Thread.currentThread().setContextClassLoader(oldTccl); } } } // 这里存在一种情况考虑,如果ACTIVITY_CHECK && accessCount.get() == 0,成立时accessCount == 0 的 // 而当调用expire()方法时,此时可能会有新的servelt正在处理,调用了 // access(),导致accessCount.incrementAndGet() 增加,因此这里需要将accessCount置为零 if (ACTIVITY_CHECK) { accessCount.set(0); } // Remove this session from our manager's active sessions // 利用Manager删除当前session manager.remove(this, true); // Notify interested session event listeners if (notify) { fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null); } // Call the logout method if (principal instanceof GenericPrincipal) { GenericPrincipal gp = (GenericPrincipal) principal; try { gp.logout(); } catch (Exception e) { manager.getContainer().getLogger().error( sm.getString("standardSession.logoutfail"), e); } } // We have completed expire of this session // 过期了就不合法了 setValid(false); // 过期处理完成 expiring = false; // Unbind any objects associated with this session // 拿到该session所有的key String keys[] = keys(); if (oldTccl != null) { if (Globals.IS_SECURITY_ENABLED) { PrivilegedAction<Void> pa = new PrivilegedSetTccl( context.getLoader().getClassLoader()); AccessController.doPrivileged(pa); } else { Thread.currentThread().setContextClassLoader( context.getLoader().getClassLoader()); } } try { // 遍历key,按key从该Session内部的ConcurrentHashMap中进行移除 for (int i = 0; i < keys.length; i++) { removeAttributeInternal(keys[i], notify); } } finally { if (oldTccl != null) { if (Globals.IS_SECURITY_ENABLED) { PrivilegedAction<Void> pa = new PrivilegedSetTccl(oldTccl); AccessController.doPrivileged(pa); } else { Thread.currentThread().setContextClassLoader(oldTccl); } } } } }
代码看上去那么多,其实原理还是简单的,在移除session之前发送session销毁事件,接着就是将session从sessions 中移除掉。 请看如下方法
public void remove(Session session, boolean update) { // If the session has expired - as opposed to just being removed from // the manager because it is being persisted - update the expired stats if (update) { long timeNow = System.currentTimeMillis(); // session存活时间,当前是remove方法,不要考虑自动过期的,可能是手到remove int timeAlive = (int) (timeNow - session.getCreationTimeInternal())/1000; // 记录一下所有Session中最大的存活时间 updateSessionMaxAliveTime(timeAlive); // 过期Session数量+1 expiredSessions.incrementAndGet(); SessionTiming timing = new SessionTiming(timeNow, timeAlive); synchronized (sessionExpirationTiming) { // 添加到尾部,并移除头结点? sessionExpirationTiming.add(timing); sessionExpirationTiming.poll(); } } // 根据sessionId进行移除,这里的移除只是将某一个Session对象从ConcurrentHashMap中删除掉,该Session对象仍然存在 if (session.getIdInternal() != null) { sessions.remove(session.getIdInternal()); } }
接下来分析StandardHost的fireLifecycleEvent()的Lifecycle.PERIODIC_EVENT事件。
从截图中得知,StandardHost有3个事件HostConfig , 2个MapperListener,那么这三个监听器是何时添加进去的呢?
- 先来分析HostConfig作为监听器何时被加入到StandardHost的listeners中的,在LifecycleSupport的addLifecycleListener(LifecycleListener listener) 方法中打一个断点 。
从截图来看,显然HostConfig是通过解析xml得来的,但是server.xml中并没有配置HostConfig啊,在代码中寻寻觅觅。
不知道大家明白上面配置的意思没有,他的意思就是只要在server.xml中配置了Host,在xml解析时,只要解析到<Host />标签,则会为StandardHost添加org.apache.catalina.startup.HostConfig监听器。
接下来,看MapperListener的来源 。
首先看connector启动。为什么会有两个connector呢? 因为在server.xml中有两个Connector配置。
而在Connctor的startInternal方法中调用了如下代码mapperListener.start();
MapperListener中调用了addListeners(engine);方法
将MapperListener添加到容器的listeners中。
private void addListeners(Container container) { container.addContainerListener(this); // 容器事件监听器 container.addLifecycleListener(this); // 生命周期事件监听器 for (Container child : container.findChildren()) { addListeners(child); } }
所以上面这段代码使用递归的方式非常巧妙的将MapperListener添加到所有容器的listeners。
从打印结果来看,是不是很有规率, 从StandardEngine,StandardHost,StandardContext,StandardWrapper 依次添加MapperListener。
生命周期监听器HostConfig
Host作为虚拟主机容器用于放置Context级别容器,而Context其实对应的就是Web应用,实际上每个虚拟主机可能会对应部署多个应用,每个应用都有自己的属性,当 Tomcat启动时,必须把对应的Web应用的属性设置到对应的Context中,根据Web 项目生成的Context中,根据Web项目生成的Context并将Context添加到Host容器中,另外,当我们把这些Web应用程序复制到指定的目录后,还有一个非常重要的步骤就是加载,把Web项目加载到对应的Host容器中。
在Tomcat启动时,有两个阶段可以将Context添加到Host中, 第一种方式是用Digester框架解析server.xml文件时将生成的Context添加到Host中,这种方式需要先将Context节点配置到server.xml的Host节点下 , 这样做的缺点是不但把应用配置与Web服务器耦合在一块,而且对server.xml配置的修改不会立即生效,除非重启Tomcat,另外一种方式就是在server.xml加载解析完后再在特定的时刻寻找的Context配置文件,这时已经将应用配置解耦出Web服务器,配置文件可能为Web应用的/META-INF/context.xml文件,也可能是%CATALINA_HOME%/conf/[EngineName]/%[HostName]/[WebName].xml。
第一种方式server.xml解析时会自动组织好Host 与Context的关系,我们重点讨论第二种方式,由于Tomcat有完整的一套生命周期管理,因此第二种方式交给监听器去做很合适,相应的监听器只有在Tomcat 中才可以访问,当Tomcat启动时,它必须把所有的Web项目都加载到对应的Host容器内, 完全这些任务就是HostConfig 监听器,HostConfig 实例邮Lifecycle接口,当START_EVENT事件发生时则会执行Web应用部署加载动作,Web应用有3种部署类型:Descriptor描述符, WAR 包以及目录 , 所以部署时也要根据不同的类型做不同的处理。
下面看看HostConfig 分别如何部署不同的类型的Web应用 。
Descriptor描述符的部署就是通过对指定的部署文件解析后进行部署,部署文件会按照一定的规则放置,一般为%CATALINA_HOME%/conf/[EngineName]/[HostName]/MyTomcat.xml,其中MyTomcat.xml中的MyTomcat为web项目名,此文件的内容大致为<Context docBase=“D:\MyTomcat” reloadable=“true” /> ,其中,docBase 指定了Web应用的绝对路径,reloadable为true,表示/WEB-INF/classes/和/WEB-INF/lib 改变时会自动重加载,另外,如果一个Host包含多个Context则可以配置多个xml描述文件,如MyTomcat.xml,MyTomcat1.xml ,MyTomcat2.xml。
部署和加载的工作相对比较耗时,而且存在多个应用一起部署加载的情况,如果由Tomcat主线程一个一个的部署,可能会导致整体的启动时间过长,为了优化应用部署耗时问题, HostConfig监听器引入了线程池进行多应用同时部署,使用Future进行线程协调,如图8.2所示,最上面的主线程,到过1处时表示开始对多个应用进行部署,为每个应用分别创建一个任务并交给线程池执行,只有当所有的任务都执行完毕时(达到2处),主线程才会继续执行。
部署任务主要 做的事情如下。
- 通过Digester框架解析指定的Context配置文件,例如这里是MyTomcat.xml根据配置文件配置的属性, 生成Context对象 。
- 通过反射生成ContextConfig , 并作为监听器添加到第1步生成的Context中。
- 设置Context对象的其他属性,如Context配置文件路径,Name属性,Path属性和版本属性。
- Context对象的docBase属性用于表示整个Web工程的路径,将Context 配置文件路径和docBase放到重新部署的监听器列表中,即Tomcat 会有专门的后台线程检测这些文件是否有改动,如果有改动,则要重新执行部署动作,部署指定的重新组织Host与Context的关系并且加载Context 。
- 调用Host的addChild方法将上面生成的Context对象添加到Host容器中,此时会触发Context启动,启动动作相当的复杂,在后面再来分析 。
- 将Context对象中的WatchedResource添加到重新加载监听器列表中,Tomcat专门的后台线程检测这些文件是否改动,如果有改动,则会重新执行加载,加载指定的是不会重新组织Host与Context的关系的,而只是根据更新后的Web项目更改Context的内容 。
WAR包类型
WAR包类型的部署是直接读取%CATALINA_HOME%/webapps目录下所有以war包形式打包的Web项目,然后根据war包的内容生成Tomcat内部需要的各种对象,同样由于部署和加载的工作比较耗时,为了优化多个应用项目部署时间,使用线程池和Future机制 。
部署WAR包类型时,主要的任务如下。
- 尝试读取war包里面的/META-INF/context.xml
- 通过Digester框架解析context.xml文件,根据配置属性生成Context对象
- 通过反射生成ContextConfig , 并作为监听器添加到Context对象中。
- 设置Context对象的其他属性,如ContextName属性,Path属性,DocBase属性和版本属性。
- 调用Host的addChild方法将Context 对象添加到Host 中,此时会触发Context启动。
- 将Context对象中的WatchedResource添加到重新加载监听列表中,Tomcat会有专门的后台线程检测这些文件是否改动,则会重新执行加载,加载的是不会重新组织Host与Context的关系,而是只根据更新后的Web项目更改Context内容 。
目录类型
目录类型的部署是直接读取%CATALINA_HOME%/webapps 目录下所有目录形式的Web项目,与前面两种类型一样,使用线程池和Future优化部署时。
部署目录类型时主要的任务如下 。
- 读取目录里的META-INF/context.xml
- 通过Digester框架解析context.xml文件,根据配置属性生成Context对象 。
- 通过反射生成ContextConfig ,并作为监听器添加到Context对象中。
- 设置Context对象的其他属性,如 ContextName属性,Path属性, DocBase属性和版本属性
- 调用Host的addChild方法将Context对象添加到Host中,此时会触发Context启动,启动动作相当繁杂 。
- 将Context对象中的WatchedResource添加到重加载监听列表中,Tomcat会有专心的后台线程检测这些文件是否改动,如果有改动,则会重新执行加载,加载拇的是不会重新组织Host与Context 的关系,而是根据更新后的Web项目更改Context的内容 。
接下来,进入HostConfig的lifecycleEvent()方法
public void lifecycleEvent(LifecycleEvent event) { // Identify the host we are associated with try { host = (Host) event.getLifecycle(); if (host instanceof StandardHost) { // 复制StandardHost中的copyXML属性到HostConfig中 setCopyXML(((StandardHost) host).isCopyXML()); // 复制StandardHost的deployXML属性到HostConfig中 setDeployXML(((StandardHost) host).isDeployXML()); // 复制StandardHost的unpackWARs属性到HostConfig中 setUnpackWARs(((StandardHost) host).isUnpackWARs()); // 设置Digester的Context标签创建的对象类型 setContextClass(((StandardHost) host).getContextClass()); } } catch (ClassCastException e) { log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e); return; } // Process the event that has occurred 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(); } }
setContextClass()上面有setContextClass()方法值得注意 。
public void setContextClass(String contextClass) { String oldContextClass = this.contextClass; this.contextClass = contextClass; if (!oldContextClass.equals(contextClass)) { synchronized (digesterLock) { digester = createDigester(getContextClass()); } } }
当StandardHost中配置了contextClass时,并且与旧的contextClass不相等,默认旧的contextClass为org.apache.catalina.core.StandardContext,此时需要重新创建解析XML的digester。
protected static Digester createDigester(String contextClassName) { Digester digester = new Digester(); digester.setValidating(false); // Add object creation rule digester.addObjectCreate("Context", contextClassName, "className"); // Set the properties on that object (it doesn't matter if extra // properties are set) digester.addSetProperties("Context"); return (digester); }
我相信看到这里,大家应该明白上面Digester的创建用意吧 ,如果不明白,去看 Tomcat 源码解析一初识 这篇博客,当解析到<Context />标签时,会通过反射创建contextClassName对象,并将<Context />中的所有属性设置到contextClassName对象属性中。
而之前我们得知lifecycleEvent()方法中发送的是PERIODIC_EVENT事件,因此肯定会进入check()方法。
protected void check() { if (host.getAutoDeploy()) { // Check for resources modification to trigger redeployment DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]); for (int i = 0; i < apps.length; i++) { // 注意 HostConfig的serviced属性维护了一个Web应用列表,该列表会由Tomcat的管理程序通过MBean进行配置,当Tomcat修改 // 某个Web 应用 (如重新部署)时,会先通过同步的addServiced()将其添加到serviced()列表中,并且在操作完毕后,通过同步 // removeServiced()方法将其移除,通过此种方式,避免后台定时任务与Tomcat 管理工具的冲突,因此,在部署HostConfig 中的 // 描述文件,Web 应用目录 , WAR包时,均需要确认serviced列表中不存在同名的应用 。 if (!isServiced(apps[i].name)) checkResources(apps[i], false); } // Check for old versions of applications that can now be undeployed if (host.getUndeployOldVersions()) { checkUndeploy(); } // Hotdeploy applications deployApps(); } }
对于每一个已经部署的Web 应用,不包含在serviced列表中,Serviced列表的具体作用参见下面的注意 ,检查用于重新部署的守护资源,对于每一个守护资源文件或者目录,如果发生变更,那么就有以下几种情况 。
- 如果资源对应目录,则仅更新守护资源列表中的上次修改时间如果Web 应用存在Context描述文件并且当前变更的WAR包文件,则得到原Context 的docBase,如果docBase不以".war"结尾(即Context指向的是WAR解压目录 )删除解压目录并重新加载,否则直接重新加载,更新守护资源 。其他情况下,直接卸载应用,并且由接下来处理步骤重新部署。
- 对于每个已经部署的web 应用,检查用于重新加载的守护资源,如果资源发生变更,则重新加载Context 对象 。
- 如果Host配置为卸载旧版本应用(undeployOldVersions属性为true),则检查并卸载
- 部署Web 应用(新增以及处于卸载状态的描述文件,Web 应用目录,WAR 包),部署过程 同上面叙述 。
关于check()方法,虽然代码量少,要理解还是很复杂的。 首先看DeployedApplication[] apps =
deployed.values().toArray(new DeployedApplication[0]);这一行代码需要先理解deployed的值从哪里来的。
先来看deployWAR()方法
再来看deployDirectory()方法。
再看看deployDescriptor(ContextName cn, File contextXml)方法。
布署项目的3种方式,1. 描述符部署,2. War包部署,3. 文件夹部署,具体细节后面来分析。当然还有一种情况,manageApp(Context context) 方法。也对deployed做了修改。
光从代码层面来看,没有地方调用manageApp()方法,既然idea没有直接的引用关系,那肯定是通过反射调用的,从全局角度是查看manageApp()方法的调用地方。
HostConfig的serviced属性维护了一个Web应用列表,该列表会由Tomcat的管理程序通过MBean进行配置,当Tomcat修改某个Web 应用 (如重新部署)时,会先通过同步的addServiced()将其添加到serviced()列表中,并且在操作完毕后,通过同步 removeServiced()方法将其移除,Tomcat 管理工具通过此种方式对StandardContext进行重新加载或重新布署。
protected synchronized void checkResources(DeployedApplication app, boolean skipFileModificationResolutionCheck) { String[] resources = app.redeployResources.keySet().toArray(new String[0]); // Offset the current time by the resolution of File.lastModified() // FILE_MODIFICATION_RESOLUTION_MS默认为1秒 long currentTimeWithResolutionOffset = System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS; for (int i = 0; i < resources.length; i++) { File resource = new File(resources[i]); if (log.isDebugEnabled()) log.debug("Checking context[" + app.name + "] redeploy resource " + resource); long lastModified = app.redeployResources.get(resources[i]).longValue(); if (resource.exists() || lastModified == 0) { // File.lastModified() has a resolution of 1s (1000ms). The last // modified time has to be more than 1000ms ago to ensure that // modifications that take place in the same second are not // missed. See Bug 57765. // 文件中当前上一次修改时间不等于map中记录的上一次修改时间 并且 (host没有开启热部署 或者 文件中当前的上一次修改时间小于1秒前 或者 跳过文件修改检查为true) // 其中需要注意的是,每次进行热部署时只会对一秒之前修改的资源文件进行检查,如果每次都是检查当前是否修改了,那么很有可能还没有修改完就被部署了 // 当然,如果skipFileModificationResolutionCheck为true,从而只要资源文件修改了就会进行检查 if (resource.lastModified() != lastModified && (!host.getAutoDeploy() || resource.lastModified() < currentTimeWithResolutionOffset || skipFileModificationResolutionCheck)) { System.out.println("热部署过程中"+resource.getAbsolutePath()+"发生了修改或添加"); if (resource.isDirectory()) { // No action required for modified directory // 如果是文件目录发生了修改,那么只需要更新map中value为最新的上一次修改时间,不用做其他事情 app.redeployResources.put(resources[i], Long.valueOf(resource.lastModified())); } else if (app.hasDescriptor && resource.getName().toLowerCase( Locale.ENGLISH).endsWith(".war")) { // 如果是通过描述符部署的war,也就是在context.xml中的docbase指定一个war的方式,如果是这种情况下的war包发生了修改 // Modified WAR triggers a reload if there is an XML // file present // The only resource that should be deleted is the // expanded WAR (if any) Context context = (Context) host.findChild(app.name); String docBase = context.getDocBase(); // 这种情况到底会怎么产生? if (!docBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) { // This is an expanded directory File docBaseFile = new File(docBase); if (!docBaseFile.isAbsolute()) { docBaseFile = new File(appBase(), docBase); } reload(app, docBaseFile, resource.getAbsolutePath()); } else { // 如果就是war包发生了修改,那么进行热加载 reload(app, null, null); } // Update times app.redeployResources.put(resources[i], Long.valueOf(resource.lastModified())); app.timestamp = System.currentTimeMillis(); boolean unpackWAR = unpackWARs; if (unpackWAR && context instanceof StandardContext) { unpackWAR = ((StandardContext) context).getUnpackWAR(); } if (unpackWAR) { addWatchedResources(app, context.getDocBase(), context); } else { addWatchedResources(app, null, context); } return; } else { // Everything else triggers a redeploy // (just need to undeploy here, deploy will follow) undeploy(app); deleteRedeployResources(app, resources, i, false); return; } } } else { // 如果文件 // There is a chance the the resource was only missing // temporarily eg renamed during a text editor save try { Thread.sleep(500); } catch (InterruptedException e1) { // Ignore } // Recheck the resource to see if it was really deleted if (resource.exists()) { continue; } // Undeploy application System.out.println("热部署过程中"+resource.getAbsolutePath()+"发生了删除"); undeploy(app); deleteRedeployResources(app, resources, i, true); return; } } // 监测web.xml是否发生改动 resources = app.reloadResources.keySet().toArray(new String[0]); boolean update = false; for (int i = 0; i < resources.length; i++) { File resource = new File(resources[i]); if (log.isDebugEnabled()) { log.debug("Checking context[" + app.name + "] reload resource " + resource); } long lastModified = app.reloadResources.get(resources[i]).longValue(); // File.lastModified() has a resolution of 1s (1000ms). The last // modified time has to be more than 1000ms ago to ensure that // modifications that take place in the same second are not // missed. See Bug 57765. if ((resource.lastModified() != lastModified && (!host.getAutoDeploy() || resource.lastModified() < currentTimeWithResolutionOffset || skipFileModificationResolutionCheck)) || update) { if (!update) { System.out.println("热部署流程中发现web.xml文件发生了改变"); // Reload application reload(app, null, null); update = true; } // Update times. More than one file may have been updated. We // don't want to trigger a series of reloads. app.reloadResources.put(resources[i], Long.valueOf(resource.lastModified())); } app.timestamp = System.currentTimeMillis(); } }
关于这一块的代码逻辑,因为涉及到redeployResources , 和reloadResources 这些文件,到底何时被加入监测的呢? 只在tomcat启动StandardContext时,会加入这些文件,因为StandardContext的启动还没有分析,关于热布署这一块,我们将整个流程启动分析完成后,再来分析了。
关于ContainerBackgroundProcessor的源码分析就暂时告一段落,接下来,继续分析ContainerBase的startInternal()方法 。
protected synchronized void startInternal() throws LifecycleException { ... // Start our child containers, if any // 如果在server.xml中配置了<Context/>节点,那么对于Host节点就存在children,这个时候就会启动context, 并且是通过异步启动的 Container children[] = findChildren(); List<Future<Void>> results = new ArrayList<Future<Void>>(); for (int i = 0; i < children.length; i++) { results.add(startStopExecutor.submit(new StartChild(children[i]))); } MultiThrowable multiThrowable = null; for (Future<Void> result : results) { try { result.get(); } catch (Throwable e) { log.error(sm.getString("containerBase.threadedStartFailed"), e); if (multiThrowable == null) { multiThrowable = new MultiThrowable(); } multiThrowable.add(e); } } ... }
这段代码,我们要弄清楚两点,以<Host name=“localhost” appBase=“webapps”
unpackWARs=“true” autoDeploy=“true” copyXML=“true” >为例,他的child从何而来。 第二点,当调用child的start()方法后,也就是StandardContext的start()后做了哪些事情 。
又发挥在代码中寻寻觅觅的技能了。 在HostConfig的lifecycleEvent()方法中beforeStart()方法中打一个断点,我们知道StandardEngine下肯定是StandardHost,为什么呢?
tomcat/conf/server.xml中Server,Service,Engine,Host他们xml结构如下。
<Server port="8005" shutdown="SHUTDOWN"> <Service name="Catalina"> <Engine name="Catalina" defaultHost="localhost" > <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" copyXML="true" >
在解析到<Host />标签时会走HostRuleSet规则,进入HostRuleSet类中。
现在你终于理解StandardHost为什么是StandardEngine的子节点,原来解析tomcat/conf/server.xml而来的,再回归之前的断点。
从这个图中,你会发现StandardEngine, StandardHost, StandardContext,StandardWrapper都实现了Container接口,并且都继承了ContainerBase类, 因此ContainerBase类中的startInternal()方法都会被调用到。
调用beforeStart()方法。
public void beforeStart() { if (host.getCreateDirs()) { File[] dirs = new File[] {appBase(),configBase()}; // 如果不是目录,则打印错误日志 for (int i=0; i<dirs.length; i++) { if (!dirs[i].mkdirs() && !dirs[i].isDirectory()) { log.error(sm.getString("hostConfig.createDirs",dirs[i])); } } } }
上面获取文件目录,有两个重要方法 ,一个是appBase(),另外一个是configBase(),StandardHost需要从这两个目录中寻找child 。先来看appBase()方法 。
protected File appBase() { if (appBase != null) { return appBase; } // host的appBase默认是webapps,代码写死的,当然你也可以通过配置修改 appBase = returnCanonicalPath(host.getAppBase()); return appBase; }
接下来看returnCanonicalPath()方法的实现。
protected File returnCanonicalPath(String path) { File file = new File(path); File base = new File(System.getProperty(catalina.base)); if (!file.isAbsolute()) file = new File(base,path); try { return file.getCanonicalFile(); } catch (IOException e) { return file; } }
返回返回catalina.base + path ,如我的tomcat.base为/Users/quyixiao/gitlab/tomcat,则appBase()方法将返回/Users/quyixiao/gitlab/tomcat/webapps 。接下来看configBase()方法 。
protected File configBase() { if (configBase != null) { return configBase; } // 如果我们在server.xml中<Host/>标签配置了xmlBase,则使用catalina.base + 手动配置的路径 if (host.getXmlBase()!=null) { configBase = returnCanonicalPath(host.getXmlBase()); } else { // 如果没有手动配置 StringBuilder xmlDir = new StringBuilder("conf"); Container parent = host.getParent(); if (parent instanceof Engine) { xmlDir.append('/'); // 追加EngineName xmlDir.append(parent.getName()); } xmlDir.append('/'); // 追加HostName ,用/分隔 xmlDir.append(host.getName()); // 默认就是catalina.base +"conf" + "/" + EngineName + "/" + HostName configBase = returnCanonicalPath(xmlDir.toString()); } return (configBase); }
通过上面分析,得到appBase和configBase路径。
beforeStart()方法的调用,虽然业务逻辑很不复杂,但有着重要的作用,同时这种方式对于理解整个容器的启动有着借鉴意义 。
Container children[] = findChildren(); List<Future<Void>> results = new ArrayList<Future<Void>>(); for (int i = 0; i < children.length; i++) { results.add(startStopExecutor.submit(new StartChild(children[i]))); }
难道这段代码没有意义不?
我们来看一个例子,在<Host/> 标签下加<Context docBase=“servelet-test-1.0.war” path=“/my-test”></Context> 标签 。
打断点结果如下 。
原因也是server.xml的解析。
截图中有一个参数值create,默认为true,所以对于StandardHost如何添加StandardContext的child已经很清楚了,这只是StandardContext的一部分来源,还有其他地方也可以添加StandardContext,我们接着来看其他地方添加StandardContext的实现。当然StandardContext的start()方法做了哪些事情,这个过程异常复杂,后面再来分析 。
接下来在HostConfig的lifecycleEvent()方法中的 start();打断点 。
因为原理和beforeStart()类似,这里就不再赘述 。
public void start() { if (log.isDebugEnabled()) log.debug(sm.getString("hostConfig.start")); try { ObjectName hostON = host.getObjectName(); oname = new ObjectName (hostON.getDomain() + ":type=Deployer,host=" + host.getName()); Registry.getRegistry(null, null).registerComponent (this, oname, this.getClass().getName()); } catch (Exception e) { log.warn(sm.getString("hostConfig.jmx.register", oname), e); } // 如果catalina.base/webapps不是目录,则deployApps()方法也不需要执行了 if (!appBase().isDirectory()) { log.error(sm.getString("hostConfig.appBase", host.getName(), appBase().getPath())); host.setDeployOnStartup(false); host.setAutoDeploy(false); } // 如果catalina.base/webapps是目录,deployOnStartup默认值为true if (host.getDeployOnStartup()) deployApps(); }
start()方法做了一些较验,如果catalina.base/webapps不是目录,则不再做任何事情,此时进入deployApps()方法 。
protected void deployApps() { //catalina.base + webapps File appBase = appBase(); // catalina.base + conf + EngineName + HostName File configBase = configBase(); String[] filteredAppPaths = filterAppPaths(appBase.list()); // Deploy XML descriptors from configBase // 描述符部署 // configBase= /Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost deployDescriptors(configBase, configBase.list()); // Deploy WARs // war包部署 deployWARs(appBase, filteredAppPaths); // appBase = /Users/quyixiao/gitlab/tomcat/webapps // Deploy expanded folders // 文件夹部署 deployDirectories(appBase, filteredAppPaths); // appBase = /Users/quyixiao/gitlab/tomcat/webapps }
上面代码需要注意的是filterAppPaths()这个方法,这个方法的意图是什么呢?
protected String[] filterAppPaths(String[] unfilteredAppPaths) { Pattern filter = host.getDeployIgnorePattern(); if (filter == null || unfilteredAppPaths == null) { return unfilteredAppPaths; } List<String> filteredList = new ArrayList<String>(); Matcher matcher = null; for (String appPath : unfilteredAppPaths) { if (matcher == null) { matcher = filter.matcher(appPath); } else { matcher.reset(appPath); } if (matcher.matches()) { if (log.isDebugEnabled()) { log.debug(sm.getString("hostConfig.ignorePath", appPath)); } } else { filteredList.add(appPath); } } return filteredList.toArray(new String[filteredList.size()]); } public void setDeployIgnore(String deployIgnore) { String oldDeployIgnore; if (this.deployIgnore == null) { oldDeployIgnore = null; } else { oldDeployIgnore = this.deployIgnore.toString(); } if (deployIgnore == null) { this.deployIgnore = null; } else { this.deployIgnore = Pattern.compile(deployIgnore); } support.firePropertyChange("deployIgnore", oldDeployIgnore, deployIgnore); }
deployIgnore这个属性是相对appBase的,因此可以在配置
一个正则表达式。
从截图来看只要被matcher.matches()匹配到的,则不会被加到StandardHost的child中。此时Host 的deployIgnore 属性可以将符合某个正则表达式的Web 应用目录忽略而不进行部署, 如果不指定 , 则所有的目录均进行部署。
Web目录部署,文件描述符布署, WAR包布署,首先来分析WAR包的布署。
WAR包布署
protected void deployWARs(File appBase, String[] files) { if (files == null) return; ExecutorService es = host.getStartStopExecutor(); List<Future<?>> results = new ArrayList<Future<?>>(); for (int i = 0; i < files.length; i++) { // 如果文件名是META-INF或WEB-INF则直接过滤掉 if (files[i].equalsIgnoreCase("META-INF")) continue; if (files[i].equalsIgnoreCase("WEB-INF")) continue; File war = new File(appBase, files[i]); // 如果不以war结尾的文件过滤掉 if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") && // 如果不是文件也过滤掉,如果WAR包已经被排除了,也过滤掉 war.isFile() && !invalidWars.contains(files[i]) ) { ContextName cn = new ContextName(files[i], true); // 已经启动了,则过滤掉 if (isServiced(cn.getName())) { continue; } if (deploymentExists(cn.getName())) { DeployedApplication app = deployed.get(cn.getName()); boolean unpackWAR = unpackWARs; // 如果app属于host的child,且host的unpackWAR为true // 则取StandardContext自己的unpackWAR属性, // 为什么这样做?因为后面有if (!unpackWAR && app != null) 判断,而这个判断的用意就是, // 如果配置了unpackWAR为false,但是appBase/webapps有相同的目录,则打印警告信息 // 因此就分为4种情况, // 1. host的unpackWAR为true , context为true,不需要打印警告信息 // 2. host的unpackWAR为true, context为false, 需要打印警告信息 // 3. host的unpackWAR为false ,context为true , 需要打印警告信息 // 3. host的unpackWAR为false ,context为false,需要打印警告信息 // 因此得出结论,只要 host和context的unpackWAR 任意一个为false ,并且 appBase/webapps有相同的目录 ,则需要打印警告信息 if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) { unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR(); } if (!unpackWAR && app != null) { // Need to check for a directory that should not be there File dir = new File(appBase, cn.getBaseName()); // 如果unpackWAR为false ,但 catalina.base/webapps下有相同的目录 if (dir.exists()) { // 并不是每次后台定时任务检查时都打印,打印一次就够了 if (!app.loggedDirWarning) { log.warn(sm.getString("hostConfig.deployWar.hiddenDir",dir.getAbsoluteFile(),war.getAbsoluteFile())); app.loggedDirWarning = true; } } else { // 这里涉及到一种情况。 // catalina.base/webapps 目录下有一个servelet-test-1.0 打印了一次警告信息 // 我们删除掉servelet-test-1.0目录,此时 app.loggedDirWarning = false; // 当再在catalina.base/webapps 创建一个servelet-test-1.0时,则又会打印一次警告信息 app.loggedDirWarning = false; } } continue; } // Check for WARs with /../ /./ or similar sequences in the name if (!validateContextPath(appBase, cn.getBaseName())) { log.error(sm.getString("hostConfig.illegalWarName", files[i])); invalidWars.add(files[i]); continue; } results.add(es.submit(new DeployWar(this, cn, war))); } } for (Future<?> result : results) { try { result.get(); } catch (Exception e) { log.error(sm.getString("hostConfig.deployWar.threaded.error"), e); } } } public synchronized boolean isServiced(String name) { return (serviced.contains(name)); }
对于上面loggedDirWarning日志打印分析,可以看一个例子,在<Host /> 中加一个属性 unpackWARs=“false”
在log.warn(sm.getString(“hostConfig.deployWar.hiddenDir”,dir.getAbsoluteFile(),war.getAbsoluteFile()));打一个断点,会发现后台程序会进入其中,打印出警告信息。
接下来验证Context 的路径方法 。
private boolean validateContextPath(File appBase, String contextPath) { // More complicated than the ideal as the canonical path may or may // not end with File.separator for a directory StringBuilder docBase; String canonicalDocBase = null; try { // 获取canonicalAppBase的绝对路径 // 如 /Users/quyixiao/gitlab/tomcat/webapps/./ ,调用getCanonicalPath()方法后 // /Users/quyixiao/gitlab/tomcat/webapps // 如 /Users/quyixiao/gitlab/tomcat/webapps/../ ,调用getCanonicalPath()方法后 // /Users/quyixiao/gitlab/tomcat String canonicalAppBase = appBase.getCanonicalPath(); docBase = new StringBuilder(canonicalAppBase); if (canonicalAppBase.endsWith(File.separator)) { docBase.append(contextPath.substring(1).replace( '/', File.separatorChar)); } else { docBase.append(contextPath.replace('/', File.separatorChar)); } // At this point docBase should be canonical but will not end // with File.separator canonicalDocBase = (new File(docBase.toString())).getCanonicalPath(); // If the canonicalDocBase ends with File.separator, add one to // docBase before they are compared if (canonicalDocBase.endsWith(File.separator)) { docBase.append(File.separator); } } catch (IOException ioe) { return false; } // Compare the two. If they are not the same, the contextPath must // have /../ like sequences in it return canonicalDocBase.equals(docBase.toString()); }
如果contextPath为a/b/c/, a/b/c/./ , a/b/c/…/ ,则方法返回false ,而contextPath为a/b/c,则返回true 。 Tomcat内部需要的各种对象,同样,由于部署和加载的工作比较耗时,为了优化多个应用项目的部署时间,使用了线程池和Future机制。接下来进入DeployWar类。
private static class DeployWar implements Runnable { private HostConfig config; private ContextName cn; private File war; public DeployWar(HostConfig config, ContextName cn, File war) { this.config = config; this.cn = cn; this.war = war; } @Override public void run() { config.deployWAR(cn, war); } }
DeployWar实现Runnable接口, 最终通过run()方法来布署WAR包。
protected void deployWAR(ContextName cn, File war) { // Checking for a nested /META-INF/context.xml JarFile jar = null; InputStream istream = null; FileOutputStream fos = null; BufferedOutputStream ostream = null; File xml = new File(appBase(), cn.getBaseName() + "/META-INF/context.xml"); boolean xmlInWar = false; try { jar = new JarFile(war); JarEntry entry = jar.getJarEntry("META-INF/context.xml"); if (entry != null) { // 如果在项目的webapp目录下创建META-INF目录,并在 // META-INF目录下创建context.xml文件,如 // 配置/Users/quyixiao/github/servelet-test/src/main/webapp/META-INF/context.xml // 那么打出来的包servelet-test-1.0.war,在解析时xmlInWar为true xmlInWar = true; } } catch (IOException e) { /* Ignore */ } finally { if (jar != null) { try { jar.close(); } catch (IOException ioe) { // Ignore; } jar = null; } } Context context = null; // 一般是获取Host的deployXML配置 boolean deployThisXML = isDeployThisXML(war, cn); try { // 如果deployXML为true , // /Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0/META-INF/context.xml 文件存在 ,以servelet-test-1.0.war包为例 // unpackWARs为true ,copyXML为false ,则调用digester解析xml if (deployThisXML && xml.exists() && unpackWARs && !copyXML) { synchronized (digesterLock) { try { context = (Context) digester.parse(xml); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployDescriptor.error", war.getAbsolutePath()), e); } finally { digester.reset(); if (context == null) { context = new FailedContext(); } } } context.setConfigFile(xml.toURI().toURL()); // 如果deployThisXML存在,并且war包内存在/META-INF/context.xml // 则调用jar 获取xml流,再解析xml ,得到StandardContext } else if (deployThisXML && xmlInWar) { synchronized (digesterLock) { try { jar = new JarFile(war); JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); istream = jar.getInputStream(entry); context = (Context) digester.parse(istream); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployDescriptor.error", war.getAbsolutePath()), e); } finally { digester.reset(); if (istream != null) { try { istream.close(); } catch (IOException e) { /* Ignore */ } istream = null; } if (jar != null) { try { jar.close(); } catch (IOException e) { /* Ignore */ } jar = null; } if (context == null) { context = new FailedContext(); } context.setConfigFile( UriUtil.buildJarUrl(war, Constants.ApplicationContextXml)); } } // 如果deployThisXML为false,但war包中存在/META-INF/context.xml,打印错误日志 } else if (!deployThisXML && xmlInWar) { // Block deployment as META-INF/context.xml may contain security // configuration necessary for a secure deployment. log.error(sm.getString("hostConfig.deployDescriptor.blocked", cn.getPath(), Constants.ApplicationContextXml, new File(configBase(), cn.getBaseName() + ".xml"))); } else { // 反射创建StandardContext context = (Context) Class.forName("org.apache.catalina.core.StandardContext").newInstance(); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t); } finally { if (context == null) { context = new FailedContext(); } } // 其实上面代码写了那么多,分为3种情况 // 1 如果catalina.base/webapps/servelet-test-1.0/META-INF/context.xml 文件存在,则直接调用digester.parse(xml) ,生成StandardContext // 2. 如果war包中有/META-INF/context.xml,则digester.parse(istream) ,生成StandardContext ,只是parse()参数接收到的是一个流 // 3. 如果不存在/META-INF/context.xml ,则反射创建StandardContext boolean copyThisXml = false; if (deployThisXML) { if (host instanceof StandardHost) { copyThisXml = ((StandardHost) host).isCopyXML(); } // If Host is using default value Context can override it. if (!copyThisXml && context instanceof StandardContext) { copyThisXml = ((StandardContext) context).getCopyXML(); } // 如果war包中存在/META-INF/context.xml , // 并且StandardHost或StandardContext中任意一个配置了copyXML为true // 则将文件内容复制到 catalina.base/conf/EngineName/HostName下 // 本例中,就是将war包内的/META-INF/context.xml复制到 // /Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost目录下,并且文件名为war包名 + ".xml" // 如servelet-test-1.0.xml if (xmlInWar && copyThisXml) { // Change location of XML file to config base xml = new File(configBase(), cn.getBaseName() + ".xml"); try { jar = new JarFile(war); JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); istream = jar.getInputStream(entry); fos = new FileOutputStream(xml); ostream = new BufferedOutputStream(fos, 1024); byte buffer[] = new byte[1024]; while (true) { int n = istream.read(buffer); if (n < 0) { break; } ostream.write(buffer, 0, n); } ostream.flush(); } catch (IOException e) { /* Ignore */ } finally { if (ostream != null) { try { ostream.close(); } catch (IOException ioe) { // Ignore } ostream = null; } if (fos != null) { try { fos.close(); } catch (IOException ioe) { // Ignore } fos = null; } if (istream != null) { try { istream.close(); } catch (IOException ioe) { // Ignore } istream = null; } if (jar != null) { try { jar.close(); } catch (IOException ioe) { // Ignore; } jar = null; } } } } DeployedApplication deployedApp = new DeployedApplication(cn.getName(), xml.exists() && deployThisXML && copyThisXml); long startTime = 0; // Deploy the application in this WAR file if(log.isInfoEnabled()) { startTime = System.currentTimeMillis(); log.info(sm.getString("hostConfig.deployWar", war.getAbsolutePath())); } try { // Populate redeploy resources with the WAR file // 将 war包的/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0.war 绝对路径以及最后修改时间加入到redeployResources 中 // tomcat后台进行热布署时,就监控这些文件最后生修改时间有没有改变,如果有改变,则需要重新布署 deployedApp.redeployResources.put (war.getAbsolutePath(), Long.valueOf(war.lastModified())); if (deployThisXML && xml.exists() && copyThisXml) { // 如果deployThisXML为true ,war包中的/META-INF/context.xml文件存在,并且copyXML为true,则将拷贝的 context.xml文件添加到热布署监听之中 deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); } else { // In case an XML file is added to the config base later // 当然如果war包中不存在/META-INF/context.xml,或者不允许拷贝, // 则将默认的文件名添加到redeployResources中,但修改时间设置为0 // tomcat为什么这么做呢? 假如war包中不存在/META-INF/context.xml ,但过一段时间,向catalina.base/conf/EngineName/HostName中添加了一个文件 // 如servelet-test-1.0.xml,此时热布署是能监测到该文件的,会触发StandardContext重新布署 // 这个要结合checkResources()方法来看 String key = (new File(configBase(),cn.getBaseName() + ".xml")).getAbsolutePath(); deployedApp.redeployResources.put(key, Long.valueOf(0)); } // 1. ContextConfig监听器可能在Digester框架解析server.xml文件生成Context对象时添加 // 2. ContextConfig 监听器可能由HostConfig监听器添加 // ConfigClass默认为ContextConfig Class<?> clazz = Class.forName(host.getConfigClass()); // StandardContext中有一个默认的ContextConfig监听器 LifecycleListener listener =(LifecycleListener) clazz.newInstance(); context.addLifecycleListener(listener); context.setName(cn.getName()); context.setPath(cn.getPath()); context.setWebappVersion(cn.getVersion()); context.setDocBase(cn.getBaseName() + ".war"); host.addChild(context); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t); } finally { // If we're unpacking WARs, the docBase will be mutated after // starting the context // boolean unpackWAR = unpackWARs; if (unpackWAR && context instanceof StandardContext) { unpackWAR = ((StandardContext) context).getUnpackWAR(); } // 如果Host和Context都允许解压包,并且包路径不为空 if (unpackWAR && context.getDocBase() != null) { // 将解压包后的文件夹添加到热布署监听器中 File docBase = new File(appBase(), cn.getBaseName()); deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); addWatchedResources(deployedApp, docBase.getAbsolutePath(), context); // 如果允许解压.war包 // 并且允许布署/META-INF/context.xml ,但不允许 // 复制/META-INF/context.xml 到catalina.base/conf/EngineName/HostName目录下, // 同时war包下存在 /META-INF/context.xml 或catalina.base/war包名/META-INF/context.xml文件存在 // 则将此/META-INF/context.xml添加】监听器中 if (deployThisXML && !copyThisXml && (xmlInWar || xml.exists())) { deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); } } else { // Passing null for docBase means that no resources will be // watched. This will be logged at debug level. addWatchedResources(deployedApp, null, context); } // Add the global redeploy resources (which are never deleted) at // the end so they don't interfere with the deletion process addGlobalRedeployResources(deployedApp); } deployed.put(cn.getName(), deployedApp); if (log.isInfoEnabled()) { log.info(sm.getString("hostConfig.deployWar.finished", war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime))); } }
对于上面
deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified()));这一行代码的解释,来看一个例子吧。
接下来看addWatchedResources()方法 。
protected void addWatchedResources(DeployedApplication app, String docBase, Context context) { // FIXME: Feature idea. Add support for patterns (ex: WEB-INF/*, // WEB-INF/*.xml), where we would only check if at least one // resource is newer than app.timestamp File docBaseFile = null; if (docBase != null) { docBaseFile = new File(docBase); if (!docBaseFile.isAbsolute()) { docBaseFile = new File(appBase(), docBase); } } // 以webapps/servelet-test-1.0.war包为例子 // docBase = /Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0 // 而 watchedResources[0] 默认为 /WEB-INF/web.xml String[] watchedResources = context.findWatchedResources(); for (int i = 0; i < watchedResources.length; i++) { File resource = new File(watchedResources[i]); // 如果文件不是绝对路径 if (!resource.isAbsolute()) { // 如果docBase不存在,又是抽象路径,只能排除掉 // 如果docBase存在,而watchedResources[i]是抽象路径,则用docBase + watchedResources[i] 添加到热布署监听器中 if (docBase != null) { resource = new File(docBaseFile, watchedResources[i]); } else { if(log.isDebugEnabled()) log.debug("Ignoring non-existent WatchedResource '" + resource.getAbsolutePath() + "'"); continue; } } if(log.isDebugEnabled()) log.debug("Watching WatchedResource '" + resource.getAbsolutePath() + "'"); // 详看checkResources()方法 app.reloadResources.put(resource.getAbsolutePath(), Long.valueOf(resource.lastModified())); } }
我们知道将watchedResources添加到热布署监听器中,那watchedResources又是从何而来呢?
public String[] findWatchedResources() { synchronized (watchedResourcesLock) { return watchedResources; } }
是不是陷入僵局了。 但你不用担心,像tomcat这么好的框架,代码肯定也是很有规率的,直接找到watchedResources的set方法,在里面打断点 。
那从哪里调用的呢?
原来是 host.addChild(context);方法调用时添加的resource,那WEB-INF/web.xml从何而来呢?
当然 host.addChild(context);这行代码的内部如何实现,后面再来分析,我们知道默认watchedResources来源于/Users/quyixiao/gitlab/tomcat/conf/context.xml即可。
当然,当不允许解压时WEB-INF/web.xml 则不会被添加到热布署监听器文件中。
当然,来看全局热布署文件监听器。 addGlobalRedeployResources()方法的实现。
因此addGlobalRedeployResources()内部判断两个文件catalina.base/conf/EnginaName/HostName/context.xml/default.xml 和 catalina.base/conf/context.xml 是否存在,如果存在则将其添加到app热布署文件监听器列表中。
总结一下每个WAR 包做了哪些操作。 对于每个WAR 包进行如下操作。
- 如果Host的deployXML属性为true,且在WAR 包同名的目录去除扩展名, 下存在META-INF/context.xml文件,同时在Context的copyXML 属性为false 。则使用该描述文件创建Context 实例,用于WAR 包解压目录位于部署目录的情况) 。如果Host的deployXML属性为true,且在WAR 包压缩文件下存在META-INF/context.xml文件,则使用该描述文件创建Context 对象 。如果deployXML属性值为false,但是在WAR 包压缩文件下存在META-INF/context.xml文件,则构造FailedContext实例(Catalina的空模式,用于表示Context部署失败)其他情况下,根据Host的contextClass属性指定类型创建Context对象,如不指定,则为org.apache.catalina.core.StandardContext ,此时所有的Context 属性均采用默认配置, 除name ,path ,webappVersion,docBase会根据WAR 包的路径及名称进行设置外。
- 如果deployXML为true,且META-INF/context.xml存在于WAR 包中, 同时Context的copyXML属性为true , 则将context.xml文件复制到
$CATALINA_BASE/config/<Engine名称>/<Host名称> 目录下,文件名称同WAR 包名(去除扩展名)。 - 为Context 实例添加ContextConfig生命周期监听器。
- 通过Host 的addChild()方法将Context 实例添加到Host,该方法会判断Host是否已经启动,如果是,则直接启动Context .
- 将Context 描述文件,WAR包及web.xml等添加到守护资源 , 以便文件发生变更时重新部署或者加载Web应用 。
将文件描述符以及目录布署分析完后再来分析host.addChild(context);做哪些事情 。
文件描述符布署
先来看一个例子。
进入源码分析
protected void deployDescriptors(File configBase, String[] files) { if (files == null) return; ExecutorService es = host.getStartStopExecutor(); List<Future<?>> results = new ArrayList<Future<?>>(); for (int i = 0; i < files.length; i++) { File contextXml = new File(configBase, files[i]); // 如果文件不以xml结尾,则以文件描述符布署 if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".xml")) { ContextName cn = new ContextName(files[i], true); if (isServiced(cn.getName()) || deploymentExists(cn.getName())) continue; results.add(es.submit(new DeployDescriptor(this, cn, contextXml))); } } for (Future<?> result : results) { try { result.get(); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployDescriptor.threaded.error"), e); } } }
deployDescriptors()方法的逻辑很简单,就是判断catalina.base/conf/EngineName/HostName下的文件是否以.xml结尾,如果不以.xml结尾则过滤掉,当然这里同样用了线程池提高性能。 接下来进入DeployDescriptor类。
private static class DeployDescriptor implements Runnable { private HostConfig config; private ContextName cn; private File descriptor; public DeployDescriptor(HostConfig config, ContextName cn, File descriptor) { this.config = config; this.cn = cn; this.descriptor= descriptor; } @Override public void run() { config.deployDescriptor(cn, descriptor); } }
进入描述符布署方法 。
@SuppressWarnings("null") // context is not null protected void deployDescriptor(ContextName cn, File contextXml) { // 描述符部署 // 部署一个应用本身比较简单,分为 // 1. 注册ContextConfig: // 2. 将context添加到host中 DeployedApplication deployedApp = new DeployedApplication(cn.getName(), true); long startTime = 0; // Assume this is a configuration descriptor and deploy it if(log.isInfoEnabled()) { startTime = System.currentTimeMillis(); log.info(sm.getString("hostConfig.deployDescriptor", contextXml.getAbsolutePath())); } Context context = null; boolean isExternalWar = false; boolean isExternal = false; File expandedDocBase = null; FileInputStream fis = null; try { // 解析catalina.base/conf/EngineName/HostName/ContextName.xml fis = new FileInputStream(contextXml); synchronized (digesterLock) { try { context = (Context) digester.parse(fis); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployDescriptor.error", contextXml.getAbsolutePath()), e); context = new FailedContext(); } finally { digester.reset(); } } // 解析ContextName.xml时得到StandContext Class<?> clazz = Class.forName(host.getConfigClass()); // ContextConfig LifecycleListener listener = (LifecycleListener) clazz.newInstance(); context.addLifecycleListener(listener); context.setConfigFile(contextXml.toURI().toURL()); context.setName(cn.getName()); context.setPath(cn.getPath()); context.setWebappVersion(cn.getVersion()); // Add the associated docBase to the redeployed list if it's a WAR if (context.getDocBase() != null) { File docBase = new File(context.getDocBase()); if (!docBase.isAbsolute()) { docBase = new File(appBase(), context.getDocBase()); } // If external docBase, register .xml as redeploy first // 如果docBase指定的路径不是tomcat的webapps目录,那么就表示指向的tomcat外部 if (!docBase.getCanonicalPath().startsWith(appBase().getAbsolutePath() + File.separator)) { isExternal = true; // 如果ContextName.xml的<Context标签的docBase属性指向tomcat外部路径,则将catalina.base/conf/EngineName/HostName/ContextName.xml添加到热布署监听文件列表中 deployedApp.redeployResources.put(contextXml.getAbsolutePath(),Long.valueOf(contextXml.lastModified())); // 将 catalina.base/conf/EngineName/HostName/ContextName.xml中的<Context />标签的docBase属性配置的内容 // 添加到热布署监听器文件列表中 deployedApp.redeployResources.put(docBase.getAbsolutePath(),Long.valueOf(docBase.lastModified())); // 如果docBase属性是绝对路径,并且以.war包结尾,则设置isExternalWar为true if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) { isExternalWar = true; } } else { log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified", docBase)); // Ignore specified docBase,如果docBase指向catalina.base/webapps目录下,则设置DocBase为null context.setDocBase(null); } } // 将StandardContext添加到host的childs中 host.addChild(context); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("hostConfig.deployDescriptor.error", contextXml.getAbsolutePath()), t); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { // Ignore } } // Get paths for WAR and expanded WAR in appBase // default to appBase dir + name // 默认扩展文件目录为catalina.base + webapps + ContextName ,本例子中expandedDocBase = /Users/quyixiao/gitlab/tomcat/webapps/mytest expandedDocBase = new File(appBase(), cn.getBaseName()); // 如果catalina.base + webapps + ContextName.xml的<Context /> 标签的docBase属性不为空并且不以.war包结尾 if (context.getDocBase() != null && !context.getDocBase().toLowerCase(Locale.ENGLISH).endsWith(".war")) { // first assume docBase is absolute ,重新定义expandedDocBase 为 ContextName.xml的<Context /> 标签的docBase属性配置内容 expandedDocBase = new File(context.getDocBase()); // 如果expandedDocBase不是绝对路径,则加上catalina.base + webapps前缀 if (!expandedDocBase.isAbsolute()) { // if docBase specified and relative, it must be relative to appBase expandedDocBase = new File(appBase(), context.getDocBase()); } } // Host和Context的unpackWAR都为true时,unpackWAR才为true,否则 unpackWAR为false boolean unpackWAR = unpackWARs; if (unpackWAR && context instanceof StandardContext) { unpackWAR = ((StandardContext) context).getUnpackWAR(); } // Add the eventual unpacked WAR and all the resources which will be // watched inside it,如果docBase指向tomcat外部 if (isExternalWar) { // 并且允许解压war包 if (unpackWAR) { // 将解压之后的目录添加到热布署监听器文件列表中 deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(), Long.valueOf(expandedDocBase.lastModified())); // 将解压之后的目录下的WEB-INF/web.xml添加到热布署监听器文件列表中 addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context); } else { // 如果不允许解压,那么war包内的WEB-INF/web.xml文件将不被添加到热布署监听器文件列表中 addWatchedResources(deployedApp, null, context); } } else { // Find an existing matching war and expanded folder // 如果 docBase 指向tomcat文件内部,无论docBase指向的是一个文件还是war包 if (!isExternal) { File warDocBase = new File(expandedDocBase.getAbsolutePath() + ".war"); // 如果docBase属性值+.war包存在 if (warDocBase.exists()) { // 则将docBase属性值+.war 添加到热布署监听器文件列表中 deployedApp.redeployResources.put(warDocBase.getAbsolutePath(), Long.valueOf(warDocBase.lastModified())); } else { // Trigger a redeploy if a WAR is added ,如果不存在 .war包,也将docBase属性值+.war名称添加到热布署文件列表中,只不过修改时间为0 , // 也就意味着,如果docBase属性值+.war之前不存在,过一段时间,手动在docBase属性值的相同目录下添加了docBase属性值 + .war包,此时也会触发app热布署 deployedApp.redeployResources.put(warDocBase.getAbsolutePath(),Long.valueOf(0)); } } if (unpackWAR) { // 如果允许解压,则将catalina.base/webapps/ContextName目录文件添加到热布署监听器文件列表中 deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(), Long.valueOf(expandedDocBase.lastModified())); // 并且将目录下的WEB-INF/web.xml文件添加到热布署监听器文件列表中 addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context); } else { // 当然,如果不允许解压,则默认的WEB-INF/web.xml文件将不会被添加到热布署监听器文件列表中 addWatchedResources(deployedApp, null, context); } if (!isExternal) { // For external docBases, the context.xml will have been // added above. // 如果指向tomcat内部,则catalina.base/conf/EngineName/HostName/ContextName.xml文件被 // 加入到监听器文件列表中 deployedApp.redeployResources.put(contextXml.getAbsolutePath(),Long.valueOf(contextXml.lastModified())); } } // Add the global redeploy resources (which are never deleted) at // the end so they don't interfere with the deletion process // 添加全局文件到热布署到app的监听器列表中 // 如 catalina.base/conf/EnginaName/HostName/context.xml.default // 和catalina.base/conf/context.xml addGlobalRedeployResources(deployedApp); } if (host.findChild(context.getName()) != null) { // 如果之前没有布署过,则将contextName添加到已经布署的Context列表中 deployed.put(context.getName(), deployedApp); } if (log.isInfoEnabled()) { log.info(sm.getString("hostConfig.deployDescriptor.finished", contextXml.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime))); } }
其实deployDescriptor()方法比较晕的就是向app的热布署文件列表中添加文件,我们分几种情况来分析 。
-
如果指向的是tomcat外部war包,不允许解压
看一下reloadResources的情况。
显然允许解压时,WEB-INF/web.xml不被添加到app的reload资源列表中。 -
如果指向的是tomcat外部war包,允许解压
看一下reloadResources的情况。
大家发现没有,当指向外部目录时,允许解压,解压的目录不是war包所在目录,而是catalina.base/webapps下 ,当然目录下的WEB-INF/web.xml也被添加到app的reload资源列表中。 -
指向tomcat外部的一个目录文件,这就无所谓的解压与不解压了, 【注意】外部和内部是相对于catalina.base/webapps而言的。在本例子中,相对于/Users/quyixiao/gitlab/tomcat/webapps,而不是tomcat目录 /Users/quyixiao/gitlab/tomcat
看一下reloadResources的情况。
无论是否允许解压,目录下的WEB-INF/web.xml都被添加到reload资源列表中。 -
如果指向的是tomcat内部目录(catalina.base/webapps)下的war包,允许解压。
看一下reloadResources的情况。
允许解压catalina.base/webapps/ContextName/WEB-INF/web.xml被添加到reload资源列表中。 -
如果指向的是tomcat内部目录(catalina.base/webapps)下的war包,不允许解压。
看一下reloadResources的情况。
显然不允许解压,war包中的WEB-INF/web.xml不被添加到reload资源列表中。 -
如果指向的是tomcat内部目录(catalina.base/webapps)下的目录
看一下reloadResources的情况。
如果指向的是一个目录,则目录下的WEB-INF/web.xml被添加到reload资源列表中。
通过上面6个测试小例子,可以得知,只要docBase指向的是war包,不允许被解压,则WEB-INF/web.xml不会被添加到reload资源列表中,只要是doc指向的是目录或允许被解压,则WEB-INF/web.xml一定被添加到reload资源列表中。
对于redeploy资源列表,我们从图处中提取出有用的信息。
redeploy资源列表总结一下
-
如果指向的是tomcat外部war包,不允许解压
a) <Context docBase=“/Users/quyixiao/github/servelet-test/target/servelet-test-1.0.war” path=“/test”></Context>
b) redeploy资源列表为
/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/mytest.xml -> 1665644349000
/Users/quyixiao/github/servelet-test/target/servelet-test-1.0.war -> 1665583627000
/Users/quyixiao/gitlab/tomcat/conf/context.xml ->1649583158000 -
如果指向的是tomcat外部war包,允许解压
a)<Context docBase=“/Users/quyixiao/github/servelet-test/target/servelet-test-1.0.war” path=“/test”></Context>
b) redeploy资源列表为
/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/mytest.xml -> 1665644349000
/Users/quyixiao/github/servelet-test/target/servelet-test-1.0.war -> 1665583627000
/Users/quyixiao/gitlab/tomcat/webapps/mytest ->1665644353000
/Users/quyixiao/gitlab/tomcat/conf/context.xml -> 1649583158000 -
指向tomcat外部的一个目录文件
a)<Context docBase=“/Users/quyixiao/Desktop/mytest” path=“/test”></Context>
b) redeploy资源列表为
/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/mytest.xml -> 1665645495000
/Users/quyixiao/Desktop/mytest ->1665635238000
/Users/quyixiao/gitlab/tomcat/conf/context.xml ->1649583158000 -
如果指向的是tomcat内部目录(catalina.base/webapps)下的war包,允许解压。
a) <Context docBase=“mytest.war” path=“/test”></Context>
b) redeploy资源列表为
/Users/quyixiao/gitlab/tomcat/webapps/mytest.war ->1665643972000
/Users/quyixiao/gitlab/tomcat/webapps/mytest -> 1665645744000
/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/mytest.xml -> 1665645750000
/Users/quyixiao/gitlab/tomcat/conf/context.xml ->1649583158000
- 如果指向的是tomcat内部目录(catalina.base/webapps)下的war包,不允许解压。
a) <Context docBase=“mytest.war” path=“/test”></Context>
b) redeploy资源列表为
/Users/quyixiao/gitlab/tomcat/webapps/mytest.war -> 1665643972000
/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/mytest.xml -> 1665645750000
/Users/quyixiao/gitlab/tomcat/conf/context.xml -> 1649583158000
- 如果指向的是tomcat内部目录(catalina.base/webapps)下的目录
a) <Context docBase=“mytest” path=“/test”></Context>
b) redeploy资源列表为
/Users/quyixiao/gitlab/tomcat/webapps/mytest.war -> 1665643972000
/Users/quyixiao/gitlab/tomcat/webapps/mytest ->1665646034000
/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/mytest.xml -> 1665646055000
/Users/quyixiao/gitlab/tomcat/conf/context.xml ->1649583158000
通过这6个例子,我们得出什么结论呢?无论何种情况,描述文件(catalina.base/conf/EngineName/HostName/ContextName.xml)和全局文件(catalina.base/conf/context.xml)都被添加到redeploy资源列表中。 只要被描述文件catalina.base/conf/EngineName/HostName/ContextName.xml 的<Context />标签的docBase属性指向的文件肯定被添加到redeploy资源列表中,如果指向的是一个外部文件,不允许解压,则只有以上的三个文件,如果指向的是一个外部war包文件,并允许解压,则需要多监听一个文件catalina.base/webapps/ContextName的这个从外部war包解压到tomcat内部的ContextName目录文件 。
如果指向tomcat内部,doc指向的资源是一个war包,不允许解压,则redeploy资源列表中只会添加上面所述的3个文件。 否则catalina.base/webapps/ContextName目录文件和 catalina.base/webapps/ContextName.war文件都会被添加到监听列表中。
总结一下文件描述符布署
扫描Host 配置文件的部署过程如下 ,具体可
-
扫描Host 配置文件基础目录 , 即$CATALINA_BASE/config/<Engine名称>/<Host名称>, 对该目录下的每个配置文件,由于线程池完成解析部。。
-
对于每个文件的部署线程,进行如下操作。使用Digester解析配置文件,创建Context实例。更新Context 实例的名称 ,路径 (不考虑webappVersion的情况下,使用文件名),因此<Context>元素中的配置path属性无效。
-
为Context 添加ContextConfig 生命周期监听器 。
-
通过Host的addChild()方法将Context 实例添加到Host , 该方法会判断Host是否已经启动。 如果是,则直接启动Context
-
将Context描述文件,Web 应用目录及web.xml 等添加到守护资源 , 以便文件发生变更时(使用资源文件的上次修改时间进行判断),重新部署或者加载Web 应用 。即便要对Web 应用单独指定目录管理或者对Context 创建进行定制,我们也建议采用该方案或者随后讲到的配置文件备份方案, 而非直接在server.xml文件中配置,它们的功能相同,但是前面两者灵活性要高得多, 而且对服务器侵入要小。
文件夹布署
- 在源码解析之前先来看一个例子,在Host配置如下
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" copyXML="true" />
- 创建一个项目servelet-test-1.0,创建 在/Users/user/gitlab/tomcat/webapps/servelet-test-1.0/META-INF/context.xml文件。文件内容如下
<Context docBase="servelet-test-1.0" path="/my-test"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t "%r" %s %b" /> </Context>
- 启动项目
当然啦,也是正常访问。
进入源码
protected void deployDirectories(File appBase, String[] files) { if (files == null) return; ExecutorService es = host.getStartStopExecutor(); List<Future<?>> results = new ArrayList<Future<?>>(); for (int i = 0; i < files.length; i++) { if (files[i].equalsIgnoreCase("META-INF")) continue; if (files[i].equalsIgnoreCase("WEB-INF")) continue; File dir = new File(appBase, files[i]); // 如果是一个目录,则调用DeployDirectory布署项目 if (dir.isDirectory()) { ContextName cn = new ContextName(files[i], false); // 如果service已经存在,或者已经布署了,则过滤掉此目录 if (isServiced(cn.getName()) || deploymentExists(cn.getName())) continue; results.add(es.submit(new DeployDirectory(this, cn, dir))); } } for (Future<?> result : results) { try { result.get(); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployDir.threaded.error"), e); } } }
同样,为了提高性能,这里也用了线程池。
private static class DeployDirectory implements Runnable { private HostConfig config; private ContextName cn; private File dir; public DeployDirectory(HostConfig config, ContextName cn, File dir) { this.config = config; this.cn = cn; this.dir = dir; } @Override public void run() { config.deployDirectory(cn, dir); } }
DeployDirectory类的实现很简单,实现了Runnable接口,最终异常调用run()方法 ,接下来进入deployDirectory()方法的调用 。
protected void deployDirectory(ContextName cn, File dir) { long startTime = 0; // Deploy the application in this directory if( log.isInfoEnabled() ) { startTime = System.currentTimeMillis(); log.info(sm.getString("hostConfig.deployDir", dir.getAbsolutePath())); } Context context = null; File xml = new File(dir, "META-INF/context.xml"); // META-INF/context.xml // 默认xmlCopy路径为catalina.base/conf/[EngineName]/[HostName]/目录下文件名称.xml // 如本例中/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/servelet-test-1.0.xml File xmlCopy = new File(configBase(), cn.getBaseName() + ".xml"); DeployedApplication deployedApp; // 是否复制xml boolean copyThisXml = isCopyXML(); // 是否布署META-INF/context.xml 文件 ,默认情况下 // 如果java安全机制开启了,那么则deployXML为false // 如果设置为false ,那么Tomcat 不会解析Web 应用中的用于设置Context 元素的META-INF/context.xml文件,出于安全原因,如果不希望Web // 应用中包含Tomcat 的配置元素 。 就可以把这个属性设置 为false . 在这种情况 // 应该/conf/[enginename]/[hostname] 一设置Context 元素,该属性默认值为true // 但 boolean deployThisXML = isDeployThisXML(dir, cn); try { // 如果允许布署这个xml文件,并且xml文件存在 // 本例中xml路径为/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0/META-INF/context.xml tag1 if (deployThisXML && xml.exists()) { synchronized (digesterLock) { try { // 解析xml得到StandardContext context = (Context) digester.parse(xml); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployDescriptor.error", xml), e); context = new FailedContext(); } finally { digester.reset(); if (context == null) { context = new FailedContext(); } } } // StandardHost或StandardContext任意一个允许复制这个xml // 则将catalina.base/webapps/目录名称/META-INF/context.xml 复制到 // catalina.base/conf/[EngineName]/[HostName]/目录名称.xml // 本例中就是将/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0/META-INF/context.xml 文件复制为 // /Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/servelet-test-1.0.xml 文件 if (copyThisXml == false && context instanceof StandardContext) { // Host is using default value. Context may override it. copyThisXml = ((StandardContext) context).getCopyXML(); } if (copyThisXml) { InputStream is = null; OutputStream os = null; try { is = new FileInputStream(xml); os = new FileOutputStream(xmlCopy); IOTools.flow(is, os); // Don't catch IOE - let the outer try/catch handle it } finally { try { if (is != null) is.close(); } catch (IOException e){ // Ignore } try { if (os != null) os.close(); } catch (IOException e){ // Ignore } } context.setConfigFile(xmlCopy.toURI().toURL()); } else { context.setConfigFile(xml.toURI().toURL()); } // 如果不允许布署xml } else if (!deployThisXML && xml.exists()) { // Block deployment as META-INF/context.xml may contain security // configuration necessary for a secure deployment. log.error(sm.getString("hostConfig.deployDescriptor.blocked", cn.getPath(), xml, xmlCopy)); context = new FailedContext(); } else { // contextClass的默认值是org.apache.catalina.core.StandardContext context = (Context) Class.forName(contextClass).newInstance(); } // 当然ConfigClass的默认值是ContextConfig Class<?> clazz = Class.forName(host.getConfigClass()); LifecycleListener listener = (LifecycleListener) clazz.newInstance(); context.addLifecycleListener(listener); context.setName(cn.getName()); context.setPath(cn.getPath()); context.setWebappVersion(cn.getVersion()); context.setDocBase(cn.getBaseName()); host.addChild(context); // 这行代码里会启动context } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("hostConfig.deployDir.error", dir.getAbsolutePath()), t); } finally { deployedApp = new DeployedApplication(cn.getName(), xml.exists() && deployThisXML && copyThisXml); // 以下是去记录以下哪些文件或文件夹的变动需要重新进行部署 // 以上的这些文件或文件夹都表示一个应用,所以只要发生了变动就要进行重新部署 // Fake re-deploy resource to detect if a WAR is added at a later // point // 即使布署的是目录,但和目录同级别的war包也添加到热布署监听列表中 // 如果中途添加一个和目录相同名称的war包,则会触发热布署 // 如之前的目录名称是/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0 // 此时添加/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0.war包 // 则会触发热布署 deployedApp.redeployResources.put(dir.getAbsolutePath() + ".war", Long.valueOf(0)); // 当前目录被加到热布署监听文件列表中 deployedApp.redeployResources.put(dir.getAbsolutePath(),Long.valueOf(dir.lastModified())); if (deployThisXML && xml.exists()) { // 如果允许布署并且xml文件存在 if (copyThisXml) { // 如果允许复制catalina.base/webapps/目录名称/META-INF/context.xml到catalina.base/conf/[EngineName]/[HostName]/目录名称.xml // 则将 catalina.base/conf/[EngineName]/[HostName]/目录名称.xml 添加到热布署监听文件列表中。【注意】注意原来目录下的/META-INF/context.xml 不会被添加到监听文件列表 deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(),Long.valueOf(xmlCopy.lastModified())); } else { // 不允许复制xml // 则catalina.base/webapps/目录名称/META-INF/context.xml到catalina.base/conf/[EngineName]/[HostName]/目录名称.xml 和 // catalina.base/conf/[EngineName]/[HostName]/目录名称.xml 都会被 // 添加到监听文件列表中,注意的是 // 因为catalina.base/conf/[EngineName]/[HostName]/目录名称.xml 文件不存在 // 所以,catalina.base/conf/[EngineName]/[HostName]/目录名称.xml的修改时间设置为0 // 这样,如果tomcat运行过程中,中途添加catalina.base/conf/[EngineName]/[HostName]/目录名称.xml文件 // 会触发热布署,发现没有,如果不允许复制文件,热布署监听文件还多一个 deployedApp.redeployResources.put(xml.getAbsolutePath(),Long.valueOf(xml.lastModified())); // Fake re-deploy resource to detect if a context.xml file is // added at a later point deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(),Long.valueOf(0)); } } else { // Fake re-deploy resource to detect if a context.xml file is // added at a later point // 如果不允许布署catalina.base/webapps/目录名称/META-INF/context.xml 文件 // 或 catalina.base/webapps/目录名称/META-INF/context.xml 文件不存在 // 则只会将 catalina.base/conf/[EngineName]/[HostName]/目录名称.xml // 文件添加到监听器文件列表中 deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(),Long.valueOf(0)); // 当然,catalina.base/webapps/目录名称/META-INF/context.xml // 为什么也要将 catalina.base/webapps/目录名称/META-INF/context.xml // 文件添加到热布署监听文件列表中呢? if (!xml.exists()) { deployedApp.redeployResources.put(xml.getAbsolutePath(),Long.valueOf(0)); } } // 添加资源到reloadResources中,表示如果这些资源发生了变动就要进行reload,重新加载 addWatchedResources(deployedApp, dir.getAbsolutePath(), context); // 添加两个全局的context.xml到redeployResources中来 // Add the global redeploy resources (which are never deleted) at // the end so they don't interfere with the deletion process addGlobalRedeployResources(deployedApp); } deployed.put(cn.getName(), deployedApp); if( log.isInfoEnabled() ) { log.info(sm.getString("hostConfig.deployDir.finished", dir.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime))); } }
上面有一段加粗代码有点费解 ,如果deployThisXML 为false 或 xml不存在,则会触发下面这段代码。
if (!xml.exists()) { deployedApp.redeployResources.put(xml.getAbsolutePath(),Long.valueOf(0)); }
假如deployThisXML为true, xml不存在,则加到热布署监听中那是理所当然的,但如果deployThisXML为false,同时xml不存在,再加到热布署监听文件列表中,是不是多此一举了,Tomcat肯定不会做这样的事情,那先理着Tomcat的思路来分析 ,如果deployThisXML为false,同时xml不存在,此时添加了catalina.base/webapps/目录名称/META-INF/context.xml文件到热布署文件列表中,如果deployThisXML依然为false, 请看tag1处代码,/META-INF/context.xml 不会被解析,所以不会影响到StandardContext的启动,如果此时deployThisXML变为true了呢? 又是什么情况下会导致deployThisXML改变呢?请看deployThisXML的获取方式isDeployThisXML()方法。
public static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); /** * deploy Context XML config files property. * 如果java安全机制开启了,那么则deployXML为false * 如果设置为false ,那么Tomcat 不会解析Web 应用中的用于设置Context 元素的META-INF/context.xml文件,出于安全原因,如果不希望Web * 应用中包含Tomcat 的配置元素 。 就可以把这个属性设置 为false . 在这种情况下, 应该/conf/[enginename]/[hostname] 一设置Context 元素,该属性默认值为 * true * */ private boolean deployXML = !Globals.IS_SECURITY_ENABLED; private boolean isDeployThisXML(File docBase, ContextName cn) { boolean deployThisXML = isDeployXML(); if (Globals.IS_SECURITY_ENABLED && !deployThisXML) { //当在SecurityManager下运行时,deployXML可能会通过授予特定权限而基于每个上下文被覆盖 Policy currentPolicy = Policy.getPolicy(); if (currentPolicy != null) { URL contextRootUrl; try { contextRootUrl = docBase.toURI().toURL(); CodeSource cs = new CodeSource(contextRootUrl, (Certificate[]) null); PermissionCollection pc = currentPolicy.getPermissions(cs); Permission p = new DeployXmlPermission(cn.getBaseName()); if (pc.implies(p)) { deployThisXML = true; } } catch (MalformedURLException e) { // Should never happen log.warn("hostConfig.docBaseUrlInvalid", e); } } } return deployThisXML; } public boolean isDeployXML() { return (this.deployXML); }
java安全机制开启了,那么则deployXML为false,但可以对某个文件目录授予特定权限,因此全局deployXML为false,但catalina.base/webapps/文件目录可以授予特定权限,此时这个目录的deployXML为true,万一deployThisXML被改成了true了呢 ,因此catalina.base/webapps/目录名称/META-INF/context.xml还是要添加到热布署监听文件列表中。
再遇热布署
接着来看之前的热布署方法 。
protected synchronized void checkResources(DeployedApplication app, boolean skipFileModificationResolutionCheck) { String[] resources = app.redeployResources.keySet().toArray(new String[0]); // Offset the current time by the resolution of File.lastModified() long currentTimeWithResolutionOffset = System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS; for (int i = 0; i < resources.length; i++) { File resource = new File(resources[i]); if (log.isDebugEnabled()) log.debug("Checking context[" + app.name + "] redeploy resource " + resource); long lastModified = app.redeployResources.get(resources[i]).longValue(); // 如果lastModifed为0,之前在布署时,这个文件或目录不存在 if (resource.exists() || lastModified == 0) { // File.lastModified() has a resolution of 1s (1000ms). The last // modified time has to be more than 1000ms ago to ensure that // modifications that take place in the same second are not // missed. See Bug 57765. // 文件中当前上一次修改时间不等于map中记录的上一次修改时间 并且 (host没有开启热部署 或者 文件中当前的上一次修改时间小于1秒前 或者 跳过文件修改检查为true) // 其中需要注意的是,每次进行热部署时只会对一秒之前修改的资源文件进行检查,如果每次都是检查当前是否修改了,那么很有可能还没有修改完就被部署了 // 当然,如果skipFileModificationResolutionCheck为true,从而只要资源文件修改了就会进行检查 if (resource.lastModified() != lastModified && (!host.getAutoDeploy() || resource.lastModified() < currentTimeWithResolutionOffset || skipFileModificationResolutionCheck)) { System.out.println("热部署过程中"+resource.getAbsolutePath()+"发生了修改或添加"); if (resource.isDirectory()) { // No action required for modified directory // 如果是文件目录发生了修改,那么只需要更新map中value为最新的上一次修改时间,不用做其他事情 app.redeployResources.put(resources[i], Long.valueOf(resource.lastModified())); } else if (app.hasDescriptor && resource.getName().toLowerCase(Locale.ENGLISH).endsWith(".war")) { // 如果是通过描述符部署的war,也就是在context.xml中的docbase指定一个war的方式,如果是这种情况下的war包发生了修改 // Modified WAR triggers a reload if there is an XML // file present // The only resource that should be deleted is the // expanded WAR (if any) Context context = (Context) host.findChild(app.name); String docBase = context.getDocBase(); // 这种情况到底会怎么产生? if (!docBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) { // This is an expanded directory File docBaseFile = new File(docBase); if (!docBaseFile.isAbsolute()) { docBaseFile = new File(appBase(), docBase); } reload(app, docBaseFile, resource.getAbsolutePath()); } else { // 如果就是war包发生了修改,那么进行热加载 reload(app, null, null); } // Update times app.redeployResources.put(resources[i], Long.valueOf(resource.lastModified())); app.timestamp = System.currentTimeMillis(); boolean unpackWAR = unpackWARs; if (unpackWAR && context instanceof StandardContext) { unpackWAR = ((StandardContext) context).getUnpackWAR(); } if (unpackWAR) { addWatchedResources(app, context.getDocBase(), context); } else { addWatchedResources(app, null, context); } return; } else { // Everything else triggers a redeploy // (just need to undeploy here, deploy will follow) undeploy(app); deleteRedeployResources(app, resources, i, false); return; } } } else { // There is a chance the the resource was only missing // temporarily eg renamed during a text editor save try { Thread.sleep(500); } catch (InterruptedException e1) { // Ignore } // 再次确认文件是否被删除 if (resource.exists()) { continue; } // Undeploy application System.out.println("热部署过程中"+resource.getAbsolutePath()+"发生了删除"); undeploy(app); deleteRedeployResources(app, resources, i, true); return; } } // 监测web.xml是否发生改动 resources = app.reloadResources.keySet().toArray(new String[0]); boolean update = false; for (int i = 0; i < resources.length; i++) { File resource = new File(resources[i]); if (log.isDebugEnabled()) { log.debug("Checking context[" + app.name + "] reload resource " + resource); } long lastModified = app.reloadResources.get(resources[i]).longValue(); // File.lastModified() has a resolution of 1s (1000ms). The last // modified time has to be more than 1000ms ago to ensure that // modifications that take place in the same second are not // missed. See Bug 57765. if ((resource.lastModified() != lastModified && (!host.getAutoDeploy() || resource.lastModified() < currentTimeWithResolutionOffset || skipFileModificationResolutionCheck)) || update) { if (!update) { System.out.println("热部署流程中发现web.xml文件发生了改变"); // Reload application reload(app, null, null); update = true; } // Update times. More than one file may have been updated. We // don't want to trigger a series of reloads. app.reloadResources.put(resources[i], Long.valueOf(resource.lastModified())); } app.timestamp = System.currentTimeMillis(); } }
我们对deployDescriptors(),deployWARs(),deployDirectories() 3个方法分析完后, 再来看这个方法是不是清晰很多了,只要添加到热布署监听文件列表中的文件或目录发生修改,将会触发执行,但目录最后修改时间修改不会触发热布署,当然重新布署或重新加载文件后,会将最新文件或目录的修改时间保存到redeployResources和reloadResources中。
// 这种情况到底会怎么产生? if (!docBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) { // This is an expanded directory File docBaseFile = new File(docBase); if (!docBaseFile.isAbsolute()) { docBaseFile = new File(appBase(), docBase); } reload(app, docBaseFile, resource.getAbsolutePath()); } else { // 如果就是war包发生了修改,那么进行热加载 reload(app, null, null); }
对于这段代码,来举两个例子,当然这段代码是文件描述符布署方式触发的热加载 。
例一
-
catalina.base/conf/[EngineName]/[HostName]/目录名称.xml下配置<Context />标签
-
重新生成war包,新增加或替换掉catalina.base/conf/[EngineName]/[HostName]/目录名称.war
触发热加载 。
例二
- 配置catalina.base/conf/[EngineName]/[HostName]/[ContextName].xml的<Context />标签的docBase指向catalina.base/webapps/[ContextName].war包
- 【注意 】 <Host /> 的unpackWARs="false"字段设置为false
<Host name="localhost" appBase="webapps" unpackWARs="false" autoDeploy="true" copyXML="true" deployIgnore="[1][3-9][0-9]{9}">
接下来,来看热布署的reload()方法 。
private void reload(DeployedApplication app, File fileToRemove, String newDocBase) { if(log.isInfoEnabled()) log.info(sm.getString("hostConfig.reload", app.name)); Context context = (Context) host.findChild(app.name); // 如果Context的状态为STARTING, STARTED 或STOPPING_PREP ,表示StandardContext正在运行着 // 因此调用context.reload()方法 if (context.getState().isAvailable()) { if (fileToRemove != null && newDocBase != null) { context.addLifecycleListener( new ExpandedDirectoryRemovalListener(fileToRemove, newDocBase)); } // Reload catches and logs exceptions context.reload(); } else { // If the context was not started (for example an error // in web.xml) we'll still get to try to start if (fileToRemove != null && newDocBase != null) { // 如果处于未启动状态,则直接删除旧的docBase ,设置新的docBase即可 ExpandWar.delete(fileToRemove); context.setDocBase(newDocBase); } try { // 如果StandardContext本身就是停止状态或未启动,直接调用start()启动StandardContext context.start(); } catch (Exception e) { log.error(sm.getString("hostConfig.context.restart", app.name), e); } } }
我们以例1为例来分析reload()代码,首先要清楚,当以文件描述符方式布署,并且catalina.base/conf/[EngineName]/[HostName]/文件目录.xml的<Context /> 标签的docBase指向的是catalina.base/webapps/文件目录时,当修改catalina.base/webapps/文件目录.war包,此时fileToRemove和newDocBase两个参数到底是什么?
从截图来看, fileToRemove为/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0,newDocBase为/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0.war,之前catalina.base/conf/[EngineName]/[HostName]/文件目录.xml内配置的docBase 为 servelet-test-1.0,而新的docBase将被替换成servelet-test-1.0.war,当fileToRemove和newDocBase不为空时,则会向StandardContext中添加监听器ExpandedDirectoryRemovalListener。而ExpandedDirectoryRemovalListener代码如下 。
private static class ExpandedDirectoryRemovalListener implements LifecycleListener { private final File toDelete; private final String newDocBase; public ExpandedDirectoryRemovalListener(File toDelete, String newDocBase) { this.toDelete = toDelete; this.newDocBase = newDocBase; } @Override public void lifecycleEvent(LifecycleEvent event) { if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType())) { // The context has stopped. Context context = (Context) event.getLifecycle(); // Remove the old expanded WAR. // java不像linux系统一样,直接rm -rf 就能直接删除整个目录 // java需要递归的删除文件及目录 ExpandWar.delete(toDelete); // Reset the docBase to trigger re-expansion of the WAR. // 用新的newDocBase替换棹原来StandardContext的docBase(toDelete)目录 context.setDocBase(newDocBase); // Remove this listener from the Context else it will run every // time the Context is stopped. context.removeLifecycleListener(this); } } }
toDelete表示需要被删除的文件,newDocBase表示重新设置StandardContext的docBase的值 ,但需要值得注意的是ExpandedDirectoryRemovalListener监听器只处理AFTER_STOP_EVENT事件 ,那什么时候会触发该事件呢?当然在reload()方法中调用了StandardContext的stop()方法 。
当然ExpandedDirectoryRemovalListener是临时性监听器,使用完之后需要被移除掉。
当然移除代码的逻辑也很简单,因为listeners是一个数组类型,所以看上去一大堆代码,实际上就是找到listeners中与ExpandedDirectoryRemovalListener相等的listener的索引,然后重新创建一个数组,将非ExpandedDirectoryRemovalListener索引的listener加到新数组中即可,大家发现规率没有,当StandardContext没有处理运行中时,直接删除旧的docBase目录,再设置 StandardContext新的docBase即可,但StandardContext处理运行中时,需要添加一个监听器来删除目录和设置新的docBase呢? 从监听器的顺序得知,即使添加监听器ExpandedDirectoryRemovalListener也是加到StandardContext的监听器的末尾,而且还只处理after_stop事件。因此需要等到,ContextConfig , StandardHost$MemoryLeakTrackingListener , TldConfig , NamingContextListener , MapperListener , ThreadLocalLeakPreventionListener这些监听器执行完before_stop,stop和after_stop事件之后才轮到
ExpandedDirectoryRemovalListener执行after_stop事件,而此时StandardContext处理停止状态了, 所以再删除docBase将不会造成异想不到的异常。 此时大家应该知道tomcat用ExpandedDirectoryRemovalListener监听器的意图了吧。
接下来看reload()方法的实现。
public synchronized void reload() { // Validate our current component state if (!getState().isAvailable()) throw new IllegalStateException (sm.getString("standardContext.notStarted", getName())); if(log.isInfoEnabled()) log.info(sm.getString("standardContext.reloadingStarted", getName())); // Stop accepting requests temporarily. // 暂停会导致request请求sleep setPaused(true);// try { // 先停止StandardContext stop(); } catch (LifecycleException e) { log.error( sm.getString("standardContext.stoppingContext", getName()), e); } try { // 启动StandardContext start(); } catch (LifecycleException e) { log.error( sm.getString("standardContext.startingContext", getName()), e); } // 继续处理request请求 setPaused(false); if(log.isInfoEnabled()) log.info(sm.getString("standardContext.reloadingCompleted", getName())); }
reload()方法的处理也很简单了,暂停处理请求,stop StandardContext容器,再启动StandardContext,删除暂停标识,关于暂停会对request请求有何影响,在后面的博客再来分析。
接下来,对 undeploy(app) , deleteRedeployResources()方法分析,在分析这两个方法之前,先来看一个例子。
例三
如下图所示 ,过一段时间修改新打包一个servlet-test-1.0.war,并替换掉tomcat的webapps目录下的servlet-test-1.0.war
再来看undeploy()方法 。
private void undeploy(DeployedApplication app) { if (log.isInfoEnabled()) log.info(sm.getString("hostConfig.undeploy", app.name)); Container context = host.findChild(app.name); try { // 将context从host子节点下移除 // 方便deploymentExists()方法检查不到StandardContext,触发重新重新布署 host.removeChild(context); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.warn(sm.getString ("hostConfig.context.remove", app.name), t); } // 将app从deployed中移除 // 方便deploymentExists()方法检查不到StandardContext,触发重新重新布署 deployed.remove(app.name); }
undeploy()方法并没有做过多的事情 ,只是将app和StandardContext分别从deployed和host移除,方便在deployApps()时,检测不到StandardContext已经被布署过了,触发StandardContext重新启动。
继续看deleteRedeployResources()方法的实现。
private void deleteRedeployResources(DeployedApplication app, String[] resources, int i, boolean deleteReloadResources) { // Delete other redeploy resources // 删除其他的热部署资源 for (int j = i + 1; j < resources.length; j++) { File current = new File(resources[j]); // Never delete per host context.xml defaults // 如果当前资源名为 context.xml.default,则不允许删除 if (Constants.HostContextXml.equals(current.getName())) { continue; } // Only delete resources in the appBase or the // host's configBase ,如果当前文件可删除,则递归删除当前文件 if (isDeletableResource(app, current)) { if (log.isDebugEnabled()) { log.debug("Delete " + current); } ExpandWar.delete(current); } } // Delete reload resources (to remove any remaining .xml descriptor) if (deleteReloadResources) { String[] resources2 = app.reloadResources.keySet().toArray(new String[0]); for (int j = 0; j < resources2.length; j++) { File current = new File(resources2[j]); // Never delete per host context.xml defaults if (Constants.HostContextXml.equals(current.getName())) { continue; } // Only delete resources in the appBase or the host's // configBase if (isDeletableResource(app, current)) { if (log.isDebugEnabled()) { log.debug("Delete " + current); } ExpandWar.delete(current); } } } }
deleteRedeployResources()方法重点就是哪些资源可删除,哪些资源不可删除的判断方法isDeletableResource(),接下来进入其中 。
private boolean isDeletableResource(DeployedApplication app, File resource) { // The resource may be a file, a directory or a symlink to a file or // directory. // Check that the resource is absolute. This should always be the case. // 如果文件不是绝对路径,肯定不做删除 if (!resource.isAbsolute()) { log.warn(sm.getString("hostConfig.resourceNotAbsolute", app.name, resource)); return false; } // Determine where the resource is located // 获取资源的上一个目录的位置 , // 如resource为/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost/servelet-test-1.0.xml // canonicalLocation为/Users/quyixiao/gitlab/tomcat/conf/Catalina/localhost String canonicalLocation; try { canonicalLocation = resource.getParentFile().getCanonicalPath(); } catch (IOException e) { log.warn(sm.getString( "hostConfig.canonicalizing", resource.getParentFile(), app.name), e); return false; } String canonicalAppBase; try { // appBase目录 /Users/quyixiao/gitlab/tomcat/webapps canonicalAppBase = appBase().getCanonicalPath(); } catch (IOException e) { log.warn(sm.getString( "hostConfig.canonicalizing", appBase(), app.name), e); return false; } // 如果appBase目录和文件的上一个目录是同一个目录,则可以删除 if (canonicalLocation.equals(canonicalAppBase)) { // Resource is located in the appBase so it may be deleted return true; } String canonicalConfigBase; try { canonicalConfigBase = configBase().getCanonicalPath(); } catch (IOException e) { log.warn(sm.getString( "hostConfig.canonicalizing", configBase(), app.name), e); return false; } // 如果resource的上一个目录和configBase目录是同一个目录,则可以删除 if (canonicalLocation.equals(canonicalConfigBase) && resource.getName().endsWith(".xml")) { // Resource is an xml file in the configBase so it may be deleted return true; } // All other resources should not be deleted return false; }
综合上述,凡是属于app私有的文件及目录是可以删除的,全局配置文件,如catalina.base/conf/context.xml是不允许删除的。
StandardHost的启动,以及热布署相关的源码已经分析完毕,如果看到这里,我相信你对tomcat的启动有新的认识了。
StandardContext启动
public void addChild(Container child) { if (!(child instanceof Context)) throw new IllegalArgumentException (sm.getString("standardHost.notContext")); // 4. MemoryLeakTrackingListener 监听器则是在HostConfig监听器调用addChild方法把Context 容器添加到Host容器时添加,每个监听器 // 负责详细的工作分别又有哪些? child.addLifecycleListener(new MemoryLeakTrackingListener()); // Avoid NPE for case where Context is defined in server.xml with only a // docBase Context context = (Context) child; if (context.getPath() == null) { ContextName cn = new ContextName(context.getDocBase(), true); context.setPath(cn.getPath()); } super.addChild(child); }
既然分析到这里,就先来看一个ContextName的结构,及一些属性。
public final class ContextName { private static final String ROOT_NAME = "ROOT"; private static final String VERSION_MARKER = "##"; private static final String FWD_SLASH_REPLACEMENT = "#"; private final String baseName; private final String path; private final String version; private final String name; public ContextName(String name, boolean stripFileExtension) { String tmp1 = name; // Convert Context names and display names to base names // Strip off any leading "/" ,如果以 / 开头, 则删除开头/ if (tmp1.startsWith("/")) { tmp1 = tmp1.substring(1); } // 将所有的/替换成# tmp1 = tmp1.replaceAll("/", "#"); // Insert the ROOT name if required if (tmp1.startsWith("##") || "".equals(tmp1)) { tmp1 = "ROOT" + tmp1; } // Remove any file extensions if (stripFileExtension && (tmp1.toLowerCase(Locale.ENGLISH).endsWith(".war") || tmp1.toLowerCase(Locale.ENGLISH).endsWith(".xml"))) { tmp1 = tmp1.substring(0, tmp1.length() -4); } baseName = tmp1; String tmp2; // Extract version number int versionIndex = baseName.indexOf("##"); if (versionIndex > -1) { version = baseName.substring(versionIndex + 2); tmp2 = baseName.substring(0, versionIndex); } else { version = ""; tmp2 = baseName; } // 应用名字为ROOT,那么path为"" if (ROOT.equals(tmp2)) { path = ""; } else { path = "/" + tmp2.replaceAll("#", "/"); } if (versionIndex > -1) { this.name = path + ## + version; } else { this.name = path; } } }
还是有很多地方用到了ContextName的构造函数,ContextName构造函数内部都字符串的操作,我们举几个例子来理解ContextName的baseName,path,version,name 4 个属性。
-
如果传入参数name为/// ,stripFileExtension为true
beanName : ROOT##
path :
version :
name : ## -
如果传入参数name为// ,stripFileExtension为true
beanName : #
path : //
version :
name : // -
如果传入参数name为test##2 ,stripFileExtension为true
beanName : test##2
path : /test
version : 2
name : /test##2 -
如果传入参数name为test#2 ,stripFileExtension为true
beanName : test#2
path : /test/2
version :
name : /test/2 -
如果传入参数name为test2 ,stripFileExtension为true
beanName : test2
path : /test2
version :
name : /test2
理解了这些基础知识后, 继续分析super.addChild()方法。
public void addChild(Container child) { if (Globals.IS_SECURITY_ENABLED) { PrivilegedAction<Void> dp = new PrivilegedAddChild(child); AccessController.doPrivileged(dp); } else { addChildInternal(child); } }
直接调用addChildInternal()方法 。
private void addChildInternal(Container child) { if( log.isDebugEnabled() ) log.debug("Add child " + child + " " + this); // 防止并发 synchronized(children) { if (children.get(child.getName()) != null) throw new IllegalArgumentException("addChild: Child name '" + child.getName() + "' is not unique"); child.setParent(this); // May throw IAE children.put(child.getName(), child); } // Start child // Don't do this inside sync block - start can be a slow process and // locking the children object can cause problems elsewhere try { if ((getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState())) && startChildren) { // 如果child处于启动前状态,则启动child child.start(); } } catch (LifecycleException e) { log.error("ContainerBase.addChild: start: ", e); throw new IllegalStateException("ContainerBase.addChild: start: " + e); } finally { // 触发addChild fireContainerEvent(ADD_CHILD_EVENT, child); } }
进入start()方法 。
public final synchronized void start() throws LifecycleException { if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) || LifecycleState.STARTED.equals(state)) { if (log.isDebugEnabled()) { Exception e = new LifecycleException(); log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e); } else if (log.isInfoEnabled()) { log.info(sm.getString("lifecycleBase.alreadyStarted", toString())); } return; } if (state.equals(LifecycleState.NEW)) { init(); } else if (state.equals(LifecycleState.FAILED)) { stop(); } else if (!state.equals(LifecycleState.INITIALIZED) && !state.equals(LifecycleState.STOPPED)) { invalidTransition(Lifecycle.BEFORE_START_EVENT); } try { // 触发before_start事件 setStateInternal(LifecycleState.STARTING_PREP, null, false); startInternal(); if (state.equals(LifecycleState.FAILED)) { // This is a 'controlled' failure. The component put itself into the // FAILED state so call stop() to complete the clean-up. stop(); } else if (!state.equals(LifecycleState.STARTING)) { // Shouldn't be necessary but acts as a check that sub-classes are // doing what they are supposed to. invalidTransition(Lifecycle.AFTER_START_EVENT); } else { setStateInternal(LifecycleState.STARTED, null, false); } } catch (Throwable t) { // This is an 'uncontrolled' failure so put the component into the // FAILED state and throw an exception. ExceptionUtils.handleThrowable(t); setStateInternal(LifecycleState.FAILED, null, false); throw new LifecycleException(sm.getString("lifecycleBase.startFail", toString()), t); } }
start()方法的结构始终不变,但不同的容器启动,所做的事情都不一样,对于这个方法,我们不要掉以轻心,依然采用地毯式的搜索,不放过任何一个细节 。
public final synchronized void init() throws LifecycleException { if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { // 会触发before_init事件 setStateInternal(LifecycleState.INITIALIZING, null, false); initInternal(); // 会触发after_init事件 setStateInternal(LifecycleState.INITIALIZED, null, false); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); setStateInternal(LifecycleState.FAILED, null, false); throw new LifecycleException( sm.getString("lifecycleBase.initFail",toString()), t); } }
之前在HostConfig的deployDescriptors() , deployWARs(), 和deployDirectories()方法中会添加ContextConfig 监听器到StandardContext中。
而在addChild()方法的addLifecycleListener()方法中,又添加了MemoryLeakTrackingListener监听器,因此StandardContext此时有2个监听器。
而ContextConfig只对configure_start,before_start,after_start,configure_stop,after_init,after_destroy处理。
MemoryLeakTrackingListener只对after_start事件做处理。
因此StandardContext的before_init不做任何事情 。
protected void initInternal() throws LifecycleException { BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<Runnable>(); // 开启、停止容器的线程池 startStopExecutor = new ThreadPoolExecutor( getStartStopThreadsInternal(), getStartStopThreadsInternal(), 10, TimeUnit.SECONDS, startStopQueue, new StartStopThreadFactory(getName() + "-startStop-")); startStopExecutor.allowCoreThreadTimeOut(true); super.initInternal(); // 将容器注册到jmx中 } protected void initInternal() throws LifecycleException { // 调用继承方法,初始化线程池相关数据 super.initInternal(); if (processTlds) { // processTlds默认值为true , 添加TldConfig监听器 this.addLifecycleListener(new TldConfig()); } // Register the naming resources,初始化JNDI相关资源 if (namingResources != null) { namingResources.init(); } // Send j2ee.object.created notification if (this.getObjectName() != null) { Notification notification = new Notification("j2ee.object.created", this.getObjectName(), sequenceNumber.getAndIncrement()); broadcaster.sendNotification(notification); } }
在initInternal()方法需要注意的是,此时添加了TldConfig事件监听器,而StandardContext已经有3个事件监听器。
通过 init()方法得知,在调用 initInternal()方法后,会触发after_init事件,之前的ContextConfig是对 after_init事件做监听的。
在initInternal()方法中添加了TldConfig监听器,TldConfig监听器同样对after_init事件做监听 。
先分析ContextConfig的init()方法 。
protected void init() { // Called from StandardContext.init() // 1. 创建Digester对象,指定解析规则 Digester contextDigester = createContextDigester(); contextDigester.getParser(); if (log.isDebugEnabled()) log.debug(sm.getString("contextConfig.init")); context.setConfigured(false); ok = true; // 2. 解析context.xml文件,注意,并不是节点,节点在解析Server.xml的时候就被解析了 // 而是用第1步创建的Digester对象按顺序解析conf/context.xml , conf/[EngineName]/[HostName]/context.xml.default // /META-INF/context.xml等文件 contextConfig(contextDigester); // 创建解析web.xml文件和web-fragment.xml 文件的Digester createWebXmlDigester(context.getXmlNamespaceAware(), context.getXmlValidation()); }
先来看第一步,创建context.xml 或 context.xml.default文件的解析器。 创建Digester对象,指定解析规则,因为在HostConfig监听器中只是根据<Context>节点属性创建了一个Context对象,但其实<Context>节点还有很多的子节点需要解析并设置到Context对象中,另外,Tomcat 中还有两个默认的Context配置文件需要设置到Context对象作为默认的属性,一个为conf/context.xml文件,另一个为config/[EngineName]/[HostName]/context.xml.default文件,所以Digester的解析工作分为两部分,一部分是解析默认配置文件,二部分为解析<Context>子节点,子节点包括InstanceListener,Listener,Loader,Manager,Store,Parameter,Realm,Resources , ResourceLink, Value, WatchedResource , WrapperLifecycle,WrapperListener ,JarScanner,Ejb,Environment,LocalEjb,Resource ,ResourceEnvRef,ServiceRef,Transaction元素。
protected Digester createContextDigester() { Digester digester = new Digester(); digester.setValidating(false); digester.setRulesValidation(true); Map<Class<?>, List<String>> fakeAttributes = new HashMap<Class<?>, List<String>>(); List<String> objectAttrs = new ArrayList<String>(); objectAttrs.add("className"); fakeAttributes.put(Object.class, objectAttrs); // Ignore attribute added by Eclipse for its internal tracking List<String> contextAttrs = new ArrayList<String>(); contextAttrs.add("source"); fakeAttributes.put(StandardContext.class, contextAttrs); digester.setFakeAttributes(fakeAttributes); RuleSet contextRuleSet = new ContextRuleSet("", false); digester.addRuleSet(contextRuleSet); RuleSet namingRuleSet = new NamingRuleSet("Context/"); digester.addRuleSet(namingRuleSet); return digester; }
对于ContextRuleSet解析规则, new ContextRuleSet(“”, false)的第二个参数是false, 这一点我们需要注意 。
再来看contextConfig()方法 。
protected void contextConfig(Digester digester) { // 可以单独使用文件的方式配置Context的属性 // 先看Context节点上是否配置了defaultContextXml属性 if( defaultContextXml==null && context instanceof StandardContext ) { defaultContextXml = ((StandardContext)context).getDefaultContextXml(); } // set the default if we don't have any overrides // 如果Context节点上没有配置defaultContextXml属性的话,那么则取默认值 conf/context.xml if( defaultContextXml==null ) getDefaultContextXml(); if (!context.getOverride()) { File defaultContextFile = new File(defaultContextXml); // defaultContextXml如果是相对于路径,那么则相对于getBaseDir() // 所以默认请求情况下就是取得catalina.base路径下得conf/context.xml if (!defaultContextFile.isAbsolute()) { defaultContextFile =new File(getBaseDir(), defaultContextXml); } // 解析context.xml文件 if (defaultContextFile.exists()) { try { URL defaultContextUrl = defaultContextFile.toURI().toURL(); processContextConfig(digester, defaultContextUrl); } catch (MalformedURLException e) { log.error(sm.getString( "contextConfig.badUrl", defaultContextFile), e); } } // 取Host级别下的context.xml.default文件 File hostContextFile = new File(getHostConfigBase(), Constants.HostContextXml); // 解析context.xml.default文件 if (hostContextFile.exists()) { try { URL hostContextUrl = hostContextFile.toURI().toURL(); processContextConfig(digester, hostContextUrl); } catch (MalformedURLException e) { log.error(sm.getString( "contextConfig.badUrl", hostContextFile), e); } } } // configFile属性是在Tomcat部署应用时设置的,对应的文件比如:catalina.base\conf\Catalina\localhost\ContextName.xml // 解析configFile文件 if (context.getConfigFile() != null) { System.out.println(context.getConfigFile()); processContextConfig(digester, context.getConfigFile()); } }
解析顺序
- catalina.base/conf/context.xml
- catalina.base/conf/engine名称/host名称
- context.xml.defaultcatalina.base/conf/engine名称/host名称/Context名称.xml
- /META-INF/context.xml
为什么解析顺序是这样的呢? 先用全局配置设置默认属性,再用Host级别配置设置属性,最后用Context级别配置设置属性,这种顺序保证了特定属性值可以覆盖默认属性值,例如对于相同的属性reloadable ,Context级别配置文件设为true,而全局配置文件设为false, 于是Context的reloadable属性最终的值为true,现在知道为什么按这个解析顺序了吧。
接下来看WebXml文件Digester的创建 。
public void createWebXmlDigester(boolean namespaceAware, boolean validation) { boolean blockExternal = context.getXmlBlockExternal(); // 创建web.xml解析器 webRuleSet = new WebRuleSet(false); webDigester = DigesterFactory.newDigester(validation, namespaceAware, webRuleSet, blockExternal); webDigester.getParser(); // 创建web-fragment.xml解析器 webFragmentRuleSet = new WebRuleSet(true); webFragmentDigester = DigesterFactory.newDigester(validation, namespaceAware, webFragmentRuleSet, blockExternal); webFragmentDigester.getParser(); }
在web.xml和web-fragment.xml解析器的区别就是WebRuleSet类,一个构造函数参数传入true,一个构造函数参数传入false 。
接下来看TldConfig的lifecycleEvent()方法 。
public void lifecycleEvent(LifecycleEvent event) { try { context = (Context) event.getLifecycle(); } catch (ClassCastException e) { log.error(sm.getString("tldConfig.cce", event.getLifecycle()), e); return; } if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) { init(); } else if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { try { execute(); } catch (Exception e) { log.error(sm.getString( "tldConfig.execute", context.getName()), e); } } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { taglibUris.clear(); webxmlTaglibUris.clear(); listeners.clear(); } }
在TldConfig的after_init事件中, 会调用其初始化方法 。
private void init() { if (tldDigester == null){ tldDigester = createTldDigester(context.getTldValidation(), context.getXmlBlockExternal()); } } private static synchronized Digester createTldDigester(boolean validation, boolean blockExternal) { Digester digester; int cacheIndex = 0; if (validation) { cacheIndex += 1; } if (blockExternal) { cacheIndex += 2; } digester = tldDigesters[cacheIndex]; if (digester == null) { digester = DigesterFactory.newDigester(validation, true, new TldRuleSet(), blockExternal); digester.getParser(); tldDigesters[cacheIndex] = digester; } return digester; }
因为validation和blockExternal可能为true,也可能为false,因此tldDigester中可能会存在4个digester,tldDigesters[0],tldDigesters[1],tldDigesters[2],tldDigesters[3] ,再看tld文件的解析规则TldRuleSet。
而.tld文件中可能存在taglib标签,taglib/uri标签,例子如下图所示 。
关于.tld文件的解析及使用,后面再来分析,先初步理解 。
接着继续看 setStateInternal()方法,而TldConfig只对after_init和after_init事件做监听,而ContextConfig是对before_start做监听的,我们进入其beforeStart()方法 。
protected synchronized void beforeStart() { try { fixDocBase(); } catch (IOException e) { log.error(sm.getString( "contextConfig.fixDocBase", context.getName()), e); } antiLocking(); }
BEFORE_START_EVENT事件 。该事件在Context 启动之前触发,用于更新Context的docBase属性和解决Web 目录锁的问题。更新Context的docBase属性主要是为了满足WAR部署的情况,当Web应用为一个WAR压缩包且需要解压部署(Host的unpackWAR为true,且Context的unpcakWAR为true)时,docBase属性指向的是解压后的文件目录,而非WAR包路径 。
protected void fixDocBase() throws IOException { Host host = (Host) context.getParent(); // 默认appBase为webapps String appBase = host.getAppBase(); File canonicalAppBase = new File(appBase); if (canonicalAppBase.isAbsolute()) { canonicalAppBase = canonicalAppBase.getCanonicalFile(); } else { // 在我的项目中baseDir = /Users/quyixiao/gitlab/tomcat canonicalAppBase = new File(getBaseDir(), appBase) .getCanonicalFile(); } String docBase = context.getDocBase(); if (docBase == null) { // Trying to guess the docBase according to the path String path = context.getPath(); if (path == null) { return; } ContextName cn = new ContextName(path, context.getWebappVersion()); docBase = cn.getBaseName(); } File file = new File(docBase); // 如果文件是相对路径,则docBase为catalina.base/webapps/docBase // 如果只在webapps下放一个war包,则 // docBase = servelet-test-1.0.war if (!file.isAbsolute()) { // 经过拼接后变为/Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0.war docBase = (new File(canonicalAppBase, docBase)).getCanonicalPath(); } else { // getCanonicalPath()方法能将/../ , /./ 去掉 // 如 /Users/quyixiao/gitlab/tomcat/webapps/aaa/../servelet-test-1.0.war // 经过CanonicalPath()方法调用之后变为了 // /Users/quyixiao/gitlab/tomcat/webapps/servelet-test-1.0.war docBase = file.getCanonicalPath(); } file = new File(docBase); String origDocBase = docBase; ContextName cn = new ContextName(context.getPath(), context.getWebappVersion()); String pathName = cn.getBaseName(); // 只要StandardHost或StandardContext中任意一个unpackWARs为false ,将不进行war的解压 boolean unpackWARs = true; if (host instanceof StandardHost) { unpackWARs = ((StandardHost) host).isUnpackWARs(); if (unpackWARs && context instanceof StandardContext) { unpackWARs = ((StandardContext) context).getUnpackWAR(); } } if (docBase.toLowerCase(Locale.ENGLISH).endsWith(".war") && !file.isDirectory()) { URL war = UriUtil.buildJarUrl(new File(docBase)); // 如果允许解压 if (unpackWARs) { // 解压war包 docBase = ExpandWar.expand(host, war, pathName); file = new File(docBase); docBase = file.getCanonicalPath(); if (context instanceof StandardContext) { // 保存之前的docBase ((StandardContext) context).setOriginalDocBase(origDocBase); } } else { // 如果不需要解压,则验证war包 ExpandWar.validate(host, war, pathName); } } else { File docDir = new File(docBase); if (!docDir.exists()) { // 如果docBase为一个有效的目录,而且存在与该目录同名的WAR包,同时需要解压部署,则重新解压WAR包 File warFile = new File(docBase + ".war"); if (warFile.exists()) { URL war = UriUtil.buildJarUrl(warFile); // 3.如果docBase为一个有效目录,而且存在与该目录同名的WAR包,同时需要解压部署 if (unpackWARs) { // 3.1 解压WAR文件 docBase = ExpandWar.expand(host, war, pathName); file = new File(docBase); // 3.2 将Context的docBase更新为解压后的路径 (基于appBase的相对路径) docBase = file.getCanonicalPath(); } else { // 3.3 如果不需要解压部署,只检测WAR包,docBase为WAR包路径 docBase = warFile.getCanonicalPath(); ExpandWar.validate(host, war, pathName); } } if (context instanceof StandardContext) { ((StandardContext) context).setOriginalDocBase(origDocBase); } } } if (docBase.startsWith(canonicalAppBase.getPath() + File.separatorChar)) { docBase = docBase.substring(canonicalAppBase.getPath().length()); docBase = docBase.replace(File.separatorChar, '/'); if (docBase.startsWith("/")) { docBase = docBase.substring(1); } } else { docBase = docBase.replace(File.separatorChar, '/'); } context.setDocBase(docBase); }
当WAR包不允许解压,则进入war包验证。
public static void validate(Host host, URL war, String pathname) throws IOException { // Make the appBase absolute File appBase = new File(host.getAppBase()); if (!appBase.isAbsolute()) { appBase = new File(System.getProperty(Globals.CATALINA_BASE_PROP), host.getAppBase()); } File docBase = new File(appBase, pathname); // Calculate the document base directory String canonicalDocBasePrefix = docBase.getCanonicalPath(); if (!canonicalDocBasePrefix.endsWith(File.separator)) { canonicalDocBasePrefix += File.separator; } JarURLConnection juc = (JarURLConnection) war.openConnection(); juc.setUseCaches(false); JarFile jarFile = null; try { jarFile = juc.getJarFile(); Enumeration<JarEntry> jarEntries = jarFile.entries(); while (jarEntries.hasMoreElements()) { JarEntry jarEntry = jarEntries.nextElement(); String name = jarEntry.getName(); File expandedFile = new File(docBase, name); if (!expandedFile.getCanonicalPath().startsWith( canonicalDocBasePrefix)) { // Entry located outside the docBase // Throw an exception to stop the deployment throw new IllegalArgumentException( sm.getString("expandWar.illegalPath",war, name, expandedFile.getCanonicalPath(), canonicalDocBasePrefix)); } } } catch (IOException e) { throw e; } finally { if (jarFile != null) { try { jarFile.close(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } jarFile = null; } } }
大家可能在想,WAR包验证,到底想验证什么 ,当WAR包不允许解压时, 需要验证WAR包, 难道是验证WAR包里面的class对不对,显然不是。 上面加粗代码就是判断WAR包是否正确的条件,如果文件名中包含/…/ 或/./时,将会导致异常抛出。
但遗憾的是,在mac环境中,/…/文件名被解析成了 :…:
name = “/…/” + name; // 新加的测试代码,使得程序报错。
我觉得,validate()方法的目的应该是验证有没有文件名导致文件路径修改,如果修改的文件名超出docBase ,则抛出异常, 但如果文件名中包含的/…/修改文件路径不足以超出docBase,则不会抛出异常,如下图所示 。
接下来,看文件解压代码 。
public static String expand(Host host, URL war, String pathname) throws IOException { // Make sure that there is no such directory already existing File appBase = new File(host.getAppBase()); if (!appBase.isAbsolute()) { appBase = new File(System.getProperty(Globals.CATALINA_BASE_PROP), host.getAppBase()); } if (!appBase.exists() || !appBase.isDirectory()) { throw new IOException (sm.getString("hostConfig.appBase", appBase.getAbsolutePath())); } File docBase = new File(appBase, pathname); if (docBase.exists()) { // War file is already installed return (docBase.getAbsolutePath()); } // Create the new document base directory if(!docBase.mkdir() && !docBase.isDirectory()) throw new IOException(sm.getString("expandWar.createFailed", docBase)); // Expand the WAR into the new document base directory String canonicalDocBasePrefix = docBase.getCanonicalPath(); if (!canonicalDocBasePrefix.endsWith(File.separator)) { canonicalDocBasePrefix += File.separator; } JarURLConnection juc = (JarURLConnection) war.openConnection(); juc.setUseCaches(false); JarFile jarFile = null; InputStream input = null; boolean success = false; try { jarFile = juc.getJarFile(); Enumeration<JarEntry> jarEntries = jarFile.entries(); while (jarEntries.hasMoreElements()) { JarEntry jarEntry = jarEntries.nextElement(); String name = jarEntry.getName(); File expandedFile = new File(docBase, name); // 如果文件名中包含/../ 超出了docBase+name的范围 ,则抛出异常,和war包的较验一样 if (!expandedFile.getCanonicalPath().startsWith( canonicalDocBasePrefix)) { // Trying to expand outside the docBase // Throw an exception to stop the deployment throw new IllegalArgumentException( sm.getString("expandWar.illegalPath",war, name, expandedFile.getCanonicalPath(), canonicalDocBasePrefix)); } int last = name.lastIndexOf('/'); if (last >= 0) { File parent = new File(docBase, name.substring(0, last)); if (!parent.mkdirs() && !parent.isDirectory()) { throw new IOException( sm.getString("expandWar.createFailed", parent)); } } if (name.endsWith("/")) { continue; } input = jarFile.getInputStream(jarEntry); if(null == input) throw new ZipException(sm.getString("expandWar.missingJarEntry", jarEntry.getName())); // Bugzilla 33636 expand(input, expandedFile); long lastModified = jarEntry.getTime(); if ((lastModified != -1) && (lastModified != 0)) { expandedFile.setLastModified(lastModified); } input.close(); input = null; } success = true; } catch (IOException e) { throw e; } finally { if (!success) { // If something went wrong, delete expanded dir to keep things // clean deleteDir(docBase); } if (input != null) { try { input.close(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } input = null; } if (jarFile != null) { try { jarFile.close(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } jarFile = null; } } // Return the absolute path to our new document base directory return (docBase.getAbsolutePath()); } private static void expand(InputStream input, File file) throws IOException { BufferedOutputStream output = null; try { output = new BufferedOutputStream(new FileOutputStream(file)); byte buffer[] = new byte[2048]; while (true) { int n = input.read(buffer); if (n <= 0) break; output.write(buffer, 0, n); } } finally { if (output != null) { try { output.close(); } catch (IOException e) { // Ignore } } } }
这个就是解压文件的代码,其实不难,但是有两点需要注意,第一点,还是和WAR包的较验一样,第二点, 如果在catalina.base/conf/[EngineName]/[HostName]/[ContextName].xml 文件中的<Context/>标签中的docBase指向tomcat之外的WAR包,并且 unpackWARs为true时,会将文件解压到tomcat内部目录 catalina.base/webapps/下。
因为CSDN的博客字数不能超过19万字,如果想继续观看,请到 Tomcat 源码解析一容器加载-大寂灭指(中)