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 }
