StandardContext
StandardContext
Context实例表示一个具体的Web应用程序,其中包含一个或者多个Wrapper实例,每个Wrapper表示一个具体的Servlet定义,Context还需要其他组件的支持,典型的如加载器 和 Session管理器,下面就对org.apache.catlainia.core.StandardContext类的工作机制进行详细记录,该类是Context的标准实现,先来简单回顾一下StandardContext类的实例化 和 配置,然后 在了解下与StandardContext类相关的StandardContextMapper类(存在Tomcat4中),和ContextConfig类,然后呢学习一下对于每一个引入的HTTP请求的方法的方法的调用序列,然后在了解下StandardContext类的几个重要属性
StandardContext的配置
在创建了StandardContext实例之后,必须调用其start()方法来为引入的每个http请求提供服务,但是可能会因为某种原因,StandardContext对象可能会启动失败,这时StandardContext类的available属性会被设置为false,available属性表明属性StandardContext对象是否可用,展示一下其生命变量 以及其设置 与获取的方法
/** * 该 {@link Context} 的应用程序可用标志 */ private boolean available = false;
1 /** 2 * 返回这个{@link Context} 应用程序的可用标志 3 */ 4 public boolean getAvailable() { 5 6 return (this.available); 7 8 } 9 10 /** 11 * 12 * <dd>设置这个{@link Context} 应用程序的可用标志</dd> 13 * 14 * @param available 15 * 应用程序新的可用标志 16 */ 17 public void setAvailable(boolean available) { 18 19 boolean oldAvailable = this.available; 20 this.available = available; 21 // 触发属性改变的监听事件 22 support.firePropertyChange("available", new Boolean(oldAvailable), new Boolean(this.available)); 23 24 }
若是start()方法正确执行,则表明StandardContext对象配置正确,在Tomcat的实际部署中,配置StandardContext对象需要一系列的操作,正确的设置后,StandardContext对象才能读取并解析默认的web.xml文件,该文件位于%CATALINA_HOME%/conf目录下,该文件的内容会应用到所有部署到Tomcat中的应用程序中,这也保证了StandardContext实例可以处理应用程序级的web.xml文件,此外还会配置验证器阀和许可阀。
StandardContext类的configured属性是一个布尔变量,表明 StandardContext实例是否正确设置,StandardContext类使用一个事件监听器作为其配置器,当调用StandardContext实例的start方法时,其中要做的第一件事就是触发一个声明周期事件,该事件调用监听器,对StandardContext实例进行配置,若配置成功,监听器会将configured属性设置为true,否则StandardContext实例会拒绝启动,也就无法为http请求提供服务了。
下面我们从StandardContext的类构造器来开始了解它的工作原理
1 /** 2 * 3 * 用默认的基本阀 创建一个新的标准Context组件 4 */ 5 public StandardContext() { 6 7 super(); 8 pipeline.setBasic(new StandardContextValve()); 9 namingResources.setContainer(this); 10 11 }
构造函数中最重要的事情是为StandardContext实例的管道对象设置基础阀,其类型是 org.apache.catalina.core.StandardContextValue类,该基础阀会处理从连接器中接收到的每个Http请求。
启动 StandardContext实例
start方法会初始化StandardContext对象,用生命周期监听器配置StandardContext实例,当配置成功后,监听器会将其configured属性设置为true,最后,start方法会将available属性设置为true或者false,true表明StandardContext对象设置正确,与其相关联的子容器和组件都正确启动,因此,StandardContext实例可以准备为引入的Http请求提供服务了,若期间 发生任何错误,可用属性 available会被设置为false。在Tomcat实际部署中,负责配置StandardContext实例的生命周期监听器是org.apache.catalina.core.startup.ContextConfig类。这个类我会在后续的随笔中详细讨论。
StandardContext使用一个初始化为false的布尔类型变量configured来表明StandardContext对象是否正确配置,如果生命周期监听器成功了执行其配置StandardContext实例的任务,生命周期监听器就会将StandardContext对象的configured属性设置为true,在StandardContext类的
start方法末尾,会检查StandardContext对象的configured属性的值,若configured属性的值为true,则StandardContext实例启动成功,否则将调用stop方法,关闭在start方法中启动的所有组件。
1 /** 2 * 启动这个 {@link Context} 组件 3 * 4 * @exception LifecycleException 5 * 如果在启动时发生错误 6 */ 7 public synchronized void start() throws LifecycleException { 8 if (started) 9 throw new LifecycleException(sm.getString("containerBase.alreadyStarted", logName())); 10 11 if (debug >= 1) 12 log("Starting"); 13 14 // 第一步:触发生命监听器的 BEFORE_START_EVENT 事件 15 lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); 16 17 if (debug >= 1) 18 log("Processing start(), current available=" + getAvailable()); 19 // 先将Context 的available 和 configured 属性都设置为false 20 // 表示当前的StandardContext实例不可以用 且 没有配置成功 21 setAvailable(false); 22 setConfigured(false); 23 // start方法中 正确执行的状态值 由于下面的代码可能会出现某些意外情况导致 启动失败 ok为 下面代码是否可以正确执行的标志 24 boolean ok = true; 25 // --------------配置资源 26 if (getResources() == null) { // (1) Required by Loader 27 if (debug >= 1) 28 log("Configuring default Resources"); 29 try { 30 if ((docBase != null) && (docBase.endsWith(".war"))) 31 setResources(new WARDirContext()); 32 else 33 setResources(new FileDirContext()); 34 } catch (IllegalArgumentException e) { 35 log("Error initializing resources: " + e.getMessage()); 36 ok = false; 37 } 38 } 39 if (ok && (resources instanceof ProxyDirContext)) { 40 DirContext dirContext = ((ProxyDirContext) resources).getDirContext(); 41 if ((dirContext != null) && (dirContext instanceof BaseDirContext)) { 42 ((BaseDirContext) dirContext).setDocBase(getBasePath()); 43 ((BaseDirContext) dirContext).allocate(); 44 } 45 } 46 // ------------------设置载入器 47 if (getLoader() == null) { // 若没有配置好的加载器 48 if (getPrivileged()) { 49 if (debug >= 1) 50 log("Configuring privileged default Loader"); 51 // 使用默认的加载器 52 setLoader(new WebappLoader(this.getClass().getClassLoader())); 53 } else { 54 if (debug >= 1) 55 log("Configuring non-privileged default Loader"); 56 setLoader(new WebappLoader(getParentClassLoader())); 57 } 58 } 59 // ----------------设置Session管理器 60 if (getManager() == null) { // 若没有配置好的Session管理器 设置默认的管理器 61 if (debug >= 1) 62 log("Configuring default Manager"); 63 setManager(new StandardManager()); 64 } 65 66 // 初始化字符集映射器 67 getCharsetMapper(); 68 69 // Post work directory 70 postWorkDirectory(); 71 72 // Reading the "catalina.useNaming" environment variable 73 String useNamingProperty = System.getProperty("catalina.useNaming"); 74 if ((useNamingProperty != null) && (useNamingProperty.equals("false"))) { 75 useNaming = false; 76 } 77 78 if (ok && isUseNaming()) { 79 if (namingContextListener == null) { 80 namingContextListener = new NamingContextListener(); 81 namingContextListener.setDebug(getDebug()); 82 namingContextListener.setName(getNamingContextName()); 83 addLifecycleListener(namingContextListener); 84 } 85 } 86 87 // Binding thread 88 ClassLoader oldCCL = bindThread(); 89 90 // Standard container startup 91 if (debug >= 1) 92 log("Processing standard container startup"); 93 94 if (ok) { 95 96 try { 97 98 addDefaultMapper(this.mapperClass); 99 started = true; 100 101 // ------------ 启动我们的附属组件,如果有的话 102 if ((loader != null) && (loader instanceof Lifecycle)) 103 ((Lifecycle) loader).start(); 104 if ((logger != null) && (logger instanceof Lifecycle)) 105 ((Lifecycle) logger).start(); 106 107 // Unbinding thread 108 unbindThread(oldCCL); 109 110 // Binding thread 111 oldCCL = bindThread(); 112 113 if ((cluster != null) && (cluster instanceof Lifecycle)) 114 ((Lifecycle) cluster).start(); 115 if ((realm != null) && (realm instanceof Lifecycle)) 116 ((Lifecycle) realm).start(); 117 if ((resources != null) && (resources instanceof Lifecycle)) 118 ((Lifecycle) resources).start(); 119 120 // 启动该StandardContext关联的映射器 121 Mapper mappers[] = findMappers(); 122 for (int i = 0; i < mappers.length; i++) { 123 if (mappers[i] instanceof Lifecycle) 124 ((Lifecycle) mappers[i]).start(); 125 } 126 127 // 启动该StandardContext拥有的所有生命周期子容器 128 Container children[] = findChildren(); 129 for (int i = 0; i < children.length; i++) { 130 if (children[i] instanceof Lifecycle) 131 ((Lifecycle) children[i]).start(); 132 } 133 134 // 启动管道对象 135 if (pipeline instanceof Lifecycle) 136 ((Lifecycle) pipeline).start(); 137 138 // 触发Start监听事件,在这里(ContextConfig)监听器 139 // 会执行一些配置操作,若设置成功,ContextConfig实例就会将StandardContext实例的configured 140 // 设置为true。 141 lifecycle.fireLifecycleEvent(START_EVENT, null); 142 143 // 启动设置好的Session管理器 144 if ((manager != null) && (manager instanceof Lifecycle)) 145 ((Lifecycle) manager).start(); 146 147 } finally { 148 // Unbinding thread 149 unbindThread(oldCCL); 150 } 151 152 } 153 // 检查Configured的值 若为false(也就是在上面的Start监听事件中 ContextConfig实例没有正确配置 154 // StandContext)所以会将 ok 标志位赋值为false 表示启动失败 155 if (!getConfigured()) 156 ok = false; 157 158 // We put the resources into the servlet context 159 if (ok) 160 getServletContext().setAttribute(Globals.RESOURCES_ATTR, getResources()); 161 162 // Binding thread 163 oldCCL = bindThread(); 164 165 // Create context attributes that will be required 166 if (ok) { 167 if (debug >= 1) 168 log("Posting standard context attributes"); 169 postWelcomeFiles(); 170 } 171 172 // Configure and call application event listeners and filters 173 if (ok) { 174 if (!listenerStart()) 175 ok = false; 176 } 177 if (ok) { 178 if (!filterStart()) 179 ok = false; 180 } 181 182 // Load and initialize all "load on startup" servlets 183 if (ok) 184 loadOnStartup(findChildren()); 185 186 // Unbinding thread 187 unbindThread(oldCCL); 188 189 // Set available status depending upon startup success 190 if (ok) { 191 if (debug >= 1) 192 log("Starting completed"); 193 setAvailable(true); 194 } else { 195 log(sm.getString("standardContext.startFailed")); 196 try { 197 stop(); 198 } catch (Throwable t) { 199 log(sm.getString("standardContext.startCleanup"), t); 200 } 201 setAvailable(false); 202 } 203 204 // 触发 AFTER——START事件 205 lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); 206 207 }
start方法主要做了以下工作
- 触发生命周期监听器的 BEFORE_START_EVEN事件
- 将代表StandardContext实例是否可用的标志属性available 赋值为false 代表还没有启动成功 在方法最后会对该值进行重新赋值;
- 将代表StandardContext实例是否正确得到配置的标志属性configured 设置false;代表还没有成功配置。在方法中触发生命周期事件的START事件时 ContextConfig实例会对configured属性重新赋值;
- 获取并配置资源
- 获取配置的载入器并设置载入器
- 获取配置的Session管理器并设置Session管理器
- 初始化默认字符映射集 插入点细致的分析这一步 因为看了 所以就记录下来吧 要不就白看了
先看看获取字符映射集
// 初始化字符集映射器 getCharsetMapper();
/** * * 返回此Context的字符集映射器的区域设置。 */ public CharsetMapper getCharsetMapper() { // 在第一次调用该方法时 创建映射器 if (this.charsetMapper == null) { try { Class clazz = Class.forName(charsetMapperClass); this.charsetMapper = (CharsetMapper) clazz.newInstance(); } catch (Throwable t) { this.charsetMapper = new CharsetMapper(); } } return (this.charsetMapper); }
StandardContext类 用一个名为 charsetMapperClass 的String类型 成员变量表示 默认字符映射集类的 的完全限定名
/** * 要创建的字符集类的Java类名. */ private String charsetMapperClass = "org.apache.catalina.util.CharsetMapper";
有对应的get与set方法 但是set方法最好在 StandardContext类的START方法执行前 设置 看上面可以了解到 字符映射集类 是在start方法中被创建的,又或者可以写一个属性监听器来做这件事情 因为StandardContext中大多数set方法都会触发
属性更改监听事件,可以在监听器中 将StandardContext的 charsetMapper属性重置。
public void setCharsetMapper(CharsetMapper mapper) { CharsetMapper oldCharsetMapper = this.charsetMapper; this.charsetMapper = mapper; support.firePropertyChange("charsetMapper", oldCharsetMapper, this.charsetMapper); }
那么下面看下默认的字符集映射的默认实现类
package org.apache.catalina.util; import java.io.InputStream; import java.util.Locale; import java.util.Properties; /** * * <p> * <b>Title:CharsetMapper.java</b> * </p> * <p> * Copyright:ChenDong 2018 * </p> * <p> * Company:仅学习时使用 * </p> * <p> * 类功能描述: 实用程序类,当请求头集合中不包括Content-Type时,尝试从Locale映射到用于解释输入文本(或生成输出文本)的相应字符集。 * 您可以通过修改它加载的映射数据,或者通过子类化(以改变方法法),然后为特定的Web应用程序使用您自己的版本,来定制这个类的行为 * </p> * * @author 陈东 * @date 2018年12月3日 下午8:35:28 * @version 1.0 */ public class CharsetMapper { // ---------------------------------------------------- Manifest Constants /** * 默认属性资源名称。 */ public static final String DEFAULT_RESOURCE = "/org/apache/catalina/util/CharsetMapperDefault.properties"; // ---------------------------------------------------------- Constructors /** * 使用默认属性资源构造一个新的字符集 */ public CharsetMapper() { this(DEFAULT_RESOURCE); } /** * * 使用指定的属性资源来构造一个新的字符集 * * @param name * 要被加载属性资源的完全限定名 * * @exception IllegalArgumentException * 如果这个指定的属性资源因为任何原因不能被加载的话 */ public CharsetMapper(String name) { try { InputStream stream = this.getClass().getResourceAsStream(name); map.load(stream); stream.close(); } catch (Throwable t) { throw new IllegalArgumentException(t.toString()); } } // ---------------------------------------------------- Instance Variables /** * 已经从默认的资源属性或者指定资源 加载了资源并且初始化了的字符集合 */ private Properties map = new Properties(); // ------------------------------------------------------- Public Methods /** * 根据指定的区域设置获取字符集编码 * * @param locale * 用于计算字符集的区域设置 */ public String getCharset(Locale locale) { String charset = null; // 首先, 尝试全名匹配(语言和国家) charset = map.getProperty(locale.toString()); if (charset != null) return (charset); // 其次, 尝试语言匹配 charset = map.getProperty(locale.getLanguage()); return (charset); } }
看下 CharsetMapperDefault.properties的内容
ar=ISO-8859-6
be=ISO-8859-5
bg=ISO-8859-5
ca=ISO-8859-1
cs=ISO-8859-2
da=ISO-8859-1
de=ISO-8859-1
el=ISO-8859-7
en=ISO-8859-1
es=ISO-8859-1
et=ISO-8859-1
fi=ISO-8859-1
fr=ISO-8859-1
hr=ISO-8859-2
hu=ISO-8859-2
is=ISO-8859-1
it=ISO-8859-1
iw=ISO-8859-8
ja=Shift_JIS
ko=EUC-KR
lt=ISO-8859-2
lv=ISO-8859-2
mk=ISO-8859-5
nl=ISO-8859-1
no=ISO-8859-1
pl=ISO-8859-2
pt=ISO-8859-1
ro=ISO-8859-2
ru=ISO-8859-5
sh=ISO-8859-5
sk=ISO-8859-2
sl=ISO-8859-2
sq=ISO-8859-2
sr=ISO-8859-5
sv=ISO-8859-1
tr=ISO-8859-9
uk=ISO-8859-5
zh=GB2312
zh_TW=Big5
- 启动StandContext的相互关联的组件
- 启动该StandardContext拥有的所有生命周期子容器
- 启动管道对象
- 启动生命周期STRART事件ContextConfig对象就是在这里为StandardContext对象进行配置
- 启动设置好的Session管理器
- 检查configured标志是否为true也就是检查ContextConfig配置StandardContext对象是否成功若不成功 会导致 不会讲available标志重新赋值为true导致启动失败 该StandardContext不可用
- 触发AFTERSTART事件
invoke方法
在Tomcat4中,StandardContext类的invoke方法由其相关联的连接器调用,或者当StandardContext类实例是Host容器的一个子容器时,由HOST实例的invoke方法调用,StandardContext类的invoke方法首先会检查应用程序是否正在重载过程中,若是
则等待应用程序重载完成,然后,它将调用其父类ContainBase的invoke方法
/** * * 根据特定容器的设计,处理指定的请求,并生成相应的响应。 * * @param request * 将被处理的请求 * @param response * 生产的响应 * * @exception IOException * if an input/output error occurred while processing * @exception ServletException * if a ServletException was thrown while processing this * request */ public void invoke(Request request, Response response) throws IOException, ServletException { // 检查当前StandardContext实例是否在重载 若是 则等待一段之后重新检查 一直到 重载之后 才会进行以后的操作 while (getPaused()) { try { Thread.sleep(1000); } catch (InterruptedException e) { ; } } // Normal request processing if (swallowOutput) { try { SystemLogHandler.startCapture(); super.invoke(request, response); } finally { String log = SystemLogHandler.stopCapture(); if (log != null && log.length() > 0) { log(log); } } } else { // 调用其父类的invoke方法 其实就是开始调用管道的的invoke方法了 super.invoke(request, response); } }
在Tomcat5中,StandardContext类并没有提供invoke方法的实现 。因此会执行其父类ContainerBase类的invoke方法,检查应用程序是否在重载的工作就移动到了 ContainerBase的invoke方法中
StandardContextMapper类
对于每一个引入的HTTP请求,都会调用StandardContext实例的管道对象的基础阀的invoke方法来处理,StandardContex实例(就是StandardContext实例的管道对象)的基础阀 是org.apache.catalina.core.StandardContextValue类的实例,
StandardContextValue类的invoke方法要做的第一件事就是获取一个要处理HTTP请求的Wrapper实例。
在Tomcat4中,StandardContextValue实例在它包含的属性StandardContext中查找,StandardContextValue实例会使用StandardContext实例的映射器找到一个合适的Wrapper实例。获得了对应的Wrapper实例之后,他就会调用Wrapper实例的invoke方法,在深入无挖掘StandardContextValue类的工作原理之前,先介绍一些映射器组件。
ContainerBase类是StandardContext类的父类,前者定义了一个addDefaultMapper()方法用来添加一个默认的映射器。
/** * 如果没有显式配置,则添加默认Mapper实现 * * @param mapperClass * Mapper实现的java完全限定类名 */ protected void addDefaultMapper(String mapperClass) { // 若限定名为null 则证明我们不需要映射器 直接返回 if (mapperClass == null) return; //如果已经存在了mapper 则也直接返回 if (mappers.size() >= 1) return; // 根据指定的限定名 初始化并添加一个 映射器默 try { Class clazz = Class.forName(mapperClass); Mapper mapper = (Mapper) clazz.newInstance(); //固定http协议 mapper.setProtocol("http"); addMapper(mapper); } catch (Exception e) { log(sm.getString("containerBase.addDefaultMapper", mapperClass), e); } }
StandardContext类在其start方法中调用 addDefaultMapper方法,并传入变量mapperClass的值;
public synchronized void start() throws LifecycleException { /***/ if (ok) { try { addDefaultMapper(this.mapperClass); }
所以通过设置StandardContext的mapperClass的值就可以控制 使用自定义的映射器。
那么StandardContext 的mapperClass的默认值如下;
/** * * 与该容器相关联的默认映射器对象的完全限定名 */ private String mapperClass = "org.apache.catalina.core.StandardContextMapper";
必须要调用映射器的setContainer方法,通过传入一个容器的实例,将映射器和容器相关联,在Catalina中,org.apache.catalina.Mapper接口的实现类是org,apache.catalina.core.StandardContextMapper类。StandardContextMapper实例只能与Context级别容器相互关联,看下其setContaliner方法
/** * * 设置一个与该映射器相关联的容器 * * @param container * 关联的新容器 * * @exception IllegalArgumentException * 如果这个容器不是一个StandardContext实现则抛出错误 */ public void setContainer(Container container) { if (!(container instanceof StandardContext)) throw new IllegalArgumentException(sm.getString("httpContextMapper.container")); context = (StandardContext) container; }
映射器中最重要的就是map方法,该方法返回用来处理HTTP请求的子容器。方法签名如下
public Container map(Request request, boolean update);
在StandardContextMapper类中,map方法返回一个Wrapper实例,用于处理请求,若找不到适合的Wrapper实例,则返回null
对于引入的每一个http请求,StandardContextValue实例调用Context容器的map方法,并传入一个org.apache.catalina.Request对象,map方法(实际上是定义在其父类ContainerBase类中的)会针对某个特定的协议调用findMapper方法返回一个映射器对象,然后调用映射器对象的map方法获取Wrapper实例。
/** * * 根据请求的特性,返回应该用于处理该请求的子容器。如果无法标识此类子容器,则返回<code>null</code>。 * * @param request * Request being processed * @param update * Update the Request to reflect the mapping selection? */ public Container map(Request request, boolean update) { // 根据指定请求的协议返回一个Mapper Mapper mapper = findMapper(request.getRequest().getProtocol()); if (mapper == null) return (null); // 使用这个Mapper获取一个处理该请求的Wrapper实例 return (mapper.map(request, update)); }
那么在看下StandardContextMapper类的map方法实现
/** * * 根据指的request 从 StandardContext对象的子容器中 找到 匹配的 Wrapper容器,若无则返回null * * @param request * 要被处理的request * @param update * 是否更新request中的Wrapper * * @exception IllegalArgumentException * 如果路径的相对部分不能被URL解码 */ public Container map(Request request, boolean update) { int debug = context.getDebug(); // 这个请求已经被映射了一个wrapper对象了么? if (update && (request.getWrapper() != null)) return (request.getWrapper()); //先获取到相对于Context的URI 就是将请求的整个URI截掉Context的URI 后剩下的URI, String contextPath = ((HttpServletRequest) request.getRequest()).getContextPath(); String requestURI = ((HttpRequest) request).getDecodedRequestURI(); String relativeURI = requestURI.substring(contextPath.length()); if (debug >= 1) context.log("Mapping contextPath='" + contextPath + "' with requestURI='" + requestURI + "' and relativeURI='" + relativeURI + "'"); // 应用规范中的标准请求URI映射规则 Wrapper wrapper = null; String servletPath = relativeURI; String pathInfo = null; String name = null; // 规则 1 -- 精确匹配 if (wrapper == null) { if (debug >= 2) context.log(" Trying exact match(试着精确匹配)"); if (!(relativeURI.equals("/"))) //根据相对于Context的URI 从Context容器的serveletMapping集合中找到对应wrapper的名字 name = context.findServletMapping(relativeURI); if (name != null) //如果扎到了名字 则利用Context的 findChild方法 从其子容器中根据名字 找到对应wrapper wrapper = (Wrapper) context.findChild(name); if (wrapper != null) { servletPath = relativeURI; pathInfo = null; } } // 规则 2 -- 前缀匹配 if (wrapper == null) { if (debug >= 2) context.log(" Trying prefix match(试着前缀匹配)"); servletPath = relativeURI; while (true) { //前缀匹配 就是 把 相对Context的URI 作为前缀 后面加上/*看能不能找到name name = context.findServletMapping(servletPath + "/*"); if (name != null) wrapper = (Wrapper) context.findChild(name); if (wrapper != null) { pathInfo = relativeURI.substring(servletPath.length()); if (pathInfo.length() == 0) pathInfo = null; break; } int slash = servletPath.lastIndexOf('/'); if (slash < 0) break; //逐一减掉最后的/之后的URI servletPath = servletPath.substring(0, slash); } } // Rule 3 -- 扩展匹配 if (wrapper == null) { if (debug >= 2) context.log(" Trying extension match(试着扩展匹配)"); //最后一个斜杠的位置 int slash = relativeURI.lastIndexOf('/'); //如果存在一个斜杠 if (slash >= 0) { //截取最后一个斜杠之后的URI String last = relativeURI.substring(slash); //斜杠之后URI中最后一个.的位置 int period = last.lastIndexOf('.'); //如果斜杠之后URI存在 . if (period >= 0) { //匹配字符串 = * + 斜杠URI 最后一个.之后的URI String pattern = "*" + last.substring(period); //根据 扩展匹配规则 寻找name name = context.findServletMapping(pattern); if (name != null) wrapper = (Wrapper) context.findChild(name); if (wrapper != null) { servletPath = relativeURI; pathInfo = null; } } } } // 规则 4 -- 默认匹配规则 if (wrapper == null) { if (debug >= 2) context.log(" Trying default match(试着默认匹配)"); //直接斜杠匹配 name = context.findServletMapping("/"); if (name != null) wrapper = (Wrapper) context.findChild(name); if (wrapper != null) { servletPath = relativeURI; pathInfo = null; } } // 更新请求中的Wrapper(如果请求 update为true ),然后返回此包装器 if ((debug >= 1) && (wrapper != null)) context.log(" Mapped to servlet '" + wrapper.getName() + "' with servlet path '" + servletPath + "' and path info '" + pathInfo + "' and update=" + update); //如果需要更新则 将新匹配到的wrpper 更新到request中 if (update) { request.setWrapper(wrapper); ((HttpRequest) request).setServletPath(servletPath); ((HttpRequest) request).setPathInfo(pathInfo); } return (wrapper); }
也许会有疑问 但是目前本人还是得很清楚关于Context实例是如何得到这些信息用来映射servlet的。Context的addServletMapping方法。
context.addServletMapping("/Primitive", "Primitive");
context.addServletMapping("/Modern", "Modern");
在Tomcat5 中,Mapper接口以及其相关类已经被移除了,事实上,StandardContextValue类的invoke方法会从request对象中获取适合的Wrapper实例:
对重载的支持
StandardContext类定义了 reloadable属性来指明该应用程序是否启用了重载功能,当启用了重载功能的时候,当web.xml文件发生变化或者WEB-INF/classes目录下的其中一个文件被重新编译后,应用程序会重载。
StandardContext类时通过载入器实现应用程序重载的,在Tomcat4中,StandardContext对象中的WebappLoader类实现了Loader接口,并使用了一个线程检查WEB-INF目录中的所有类和jar文件的时间戳。只需要调用其setContainer方法将WebappLoader对象与StandardContext对象相关联 就可以启动该检查线程,下面是Tomcat4中的WebappLoader类的setContainer方法的实现代码
1 /** 2 * 3 * 4 * <p> 5 * Title: setContainer 6 * </p> 7 * 8 * @date 2018年11月17日 下午8:46:22 9 * 10 * <p> 11 * 功能描述: 设置与该加载器关联的 {@link Container}容器 12 * </p> 13 * 14 * @param container 15 */ 16 public void setContainer(Container container) { 17 18 //如果当前组件存在关联容器 且 关联的容器 为 Context 实例,则将该监听器 从 旧容器中移除 19 if ((this.container != null) && (this.container instanceof Context)) 20 ((Context) this.container).removePropertyChangeListener(this); 21 22 // 处理这个 container 值 更改 所触发的事件(监听当前WebappLoader的 属性 改变事件监听器) 23 Container oldContainer = this.container; 24 this.container = container; 25 // 触发监听 container 属性值改变 事件 26 support.firePropertyChange("container", oldContainer, this.container); 27 28 // 如果当前container不为空 且 container是Context的实例 则 调用setRealoadable方法 将 Context中的realoadable的值设置当该对象中 29 if ((this.container != null) && (this.container instanceof Context)) { 30 setReloadable(((Context) this.container).getReloadable()); 31 ((Context) this.container).addPropertyChangeListener(this); 32 } 33 34 }
注意最后一个if控制块如果 传入的Container容器是一个 Context 容器的话 就会同步Context容器中的readloader属性到该加载器中。下面看下 setRealoaderable方法
/** * 为当前 loader 设置 是否自动重载的标志 * * @param reloadable * 新的自动重载标志 */ public void setReloadable(boolean reloadable) { // 通知 监听自动重载值变换的监听器 boolean oldReloadable = this.reloadable; this.reloadable = reloadable; support.firePropertyChange("reloadable", new Boolean(oldReloadable), new Boolean(this.reloadable)); // 根据 reloadable 的值 来决定是否需要 启动 或者 停止 当前的 后台线程(是为了自动重载 周期性检查 已经载入的类 是否发生变化) if (!started) return; //如果旧的自动重载标志为 不支持自动重载 且 新的自动重载标志 为 支持自动重载 则 启动 后台线程 进行周期性检查 已经载入的类 是否发生变化 if (!oldReloadable && this.reloadable) threadStart(); //如果旧的自动重载标志位 支持自动重载 且新的自动重载标志 为 不支持自动重载。则停止后台线程的 的周期性检查 else if (oldReloadable && !this.reloadable) threadStop(); }
若reaoladable属性的值从false修改为true,则会调用threadStart方法;若reaoladable属性从true修改为false,则会调用threadStop方法 ,threadStart方法启动一个专门用来不断检查WEB-INF/classes目录下的类 和 JAR文件的时间戳,而threadStop方法则会终止检查线程,
在Tomcat5中,为支持StandardContext重载而进行的检查 类 和jar文件的时间戳的工作改为由backgroundprocess方法执行。
backgroundProcess方法
Context容器的运行需要其他组件的支持,例如载入器 和 session管理器,通常来说,这件组件需要使用各自的线程来执行一些后台处理任务,例如。为了支持自动重载,载入器需要使用一个线程来周期性的检查WEB-INF/clasees目录下的java类和 jar文件的时间戳;
Session管理器使用一个线程来周期性的检查它所管理的所有Session对象的过期时间,在Tomcat4中,这些组件最终拥有各自的线程。
为了节省资源,在Tomcat5中,使用了不同的方法,所有的后台处理共享一个线程,若某个组件或servlet容器需要周期性的执行一个操作,只需要将代码写到其backgroundProcess方法中即可。
这个共享线程在ContainerBase对象中创建,ContainerBase类在其start方法中(即当该容器启动时)调用其threadStart方法启动该后台线程,
/** * 后台线程是否已经关闭 */ protected boolean threadDone = true; /** * 后台线程周期间隔 事件 秒为单位 */ protected int backgroundProcessorDelay; public int getBackgroundProcessorDelay() { return this.backgroundProcessorDelay; } /** * * <p> * <b>Title:ContainerBase.java</b> * </p> * <p> * Copyright:ChenDong 2018 * </p> * <p> * Company:仅学习时使用 * </p> * <p> * 类功能描述:Context容器的运行需要其他组件的支持,例如载入器,Session管理器。通常来说这些组件需要使用各自的线程执行一些 * 自己的后台任务,例如,载入器为了 实现自动重载,载入器需要使用一个线程来周期性的检查WEb-INF/class目录下的所有类 和 * JAR文件的时间戳,Session管理器需要使用一个线程来周期性的检查它所管理的 * Session对象的过期时间,在Tomcat4中这些任务都是由组件自己拥有的线程来执行。 * 但是为了节省资源,在Tomcat5中,所有的后台处理共享同一个线程,若某个组件 * 或者servlet容器需要周期性的执行一个操作,只需要将代码写到其backgroundProcess方法中 即可。下面就是 该共享线程的实现 * </p> * * @author 陈东 * @date 2018年12月15日 下午3:49:18 * @version 1.0 */ protected class ContainerBackgroundProcessor implements Runnable{ /** * <p>Title: run</p> * @date 2018年12月15日 下午3:48:54 * <p>功能描述: </p> */ @Override public void run() { //若后台线程关闭标志 为 未关闭 while(!threadDone){ try { Thread.sleep(backgroundProcessorDelay * 1000L); } catch (InterruptedException e) { ; } if(!threadDone){ Container parent = getParent();//tomcat4中暂且没有getMappingObject ClassLoader cl = Thread.currentThread().getContextClassLoader(); if(parent.getLoader()!=null) cl = parent.getLoader().getClassLoader(); processChildren(parent, cl); } } } protected void processChildren(Container container, ClassLoader cl) { try { if (container.getLoader() != null) { Thread.currentThread().setContextClassLoader(container.getLoader().getClassLoader()); } container.backgroundProcess(); } catch (Throwable e) { log("调用周期性操作的异常", e); } finally { Thread.currentThread().setContextClassLoader(cl); } //执行每个子容器的 backgroundProcess Container[] children = container.findChildren(); for (int i = 0; i < children.length; i++) { if (children[i].getBackgroundProcessorDelay() <= 0) { processChildren(children[i], cl); } } } }
ContainerBackgroundProcessor类实际上是ContainerBase类的内部类,在其run方法中是一个while循环,周期性的调用 其processChildren方法。而processChildren方法会调用自身对象的backgroundProcess方法和 其每一个子容器的processChildren方法,通过实现
backgroundProcess方法,ContainerBase类的子类可以使用一个专用线程来执行周期性任务,例如检查类的时间戳 或者session对象的超时时间,下面看下Tomcat5中 StandardContext中backgroundProcess方法的实现;
1 /** 2 3 * <p>Title: backgroundProcess</p> 4 5 * @date 2018年12月15日 下午5:56:54 6 7 * <p>功能描述: </p> 8 9 10 */ 11 @Override 12 public void backgroundProcess() { 13 if(!started) 14 return; 15 count = (count + 1) % managerChecksFrequency; 16 if(getManager()!=null&& count==0){ 17 18 try{ 19 //启动session管理器的 backgroundProcess方法 20 getManager().backgroundProcess(); 21 }catch(Exception e){ 22 23 } 24 25 } 26 27 if (getLoader() != null) { 28 if (reloadable && getLoader().modified()) { 29 try { 30 Thread.currentThread().setContextClassLoader(StandardContext.class.getClassLoader()); 31 //启动重载方法 32 reload(); 33 } finally { 34 Thread.currentThread().setContextClassLoader(getLoader().getClassLoader()); 35 } 36 } 37 38 } 39 40 if (getLoader() instanceof WebappLoader) { 41 ((WebappLoader) getLoader()).closeJARs(false); 42 } 43 44 }