#1 靜態文件的處理前言分析html
最近想把SpringMVC對於靜態資源的處理策略弄清楚,如它和普通的請求有什麼區別嗎?web
有人可能就要說了,如今有些靜態資源都不是交給這些框架來處理,而是直接交給容器來處理,這樣更加高效。我想說的是,雖然是這樣,處理靜態資源也是MVC框架應該提供的功能,而不是依靠外界。spring
這裏以tomcat容器中的SpringMVC項目爲例。整個靜態資源的訪問,效果圖以下:apache
能夠分紅以下2個大的過程tomcat
#2 tomcat的處理策略 這裏要看tomcat的源碼,因此pom中加入相應依賴,使debug的時候可以定位到源碼文件,目前我所使用的tomcat版本爲7.0.55,你要是使用的不一樣版本,則更換下對應依賴的版本就行mvc
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>7.0.55</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-coyote</artifactId> <version>7.0.55</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>7.0.55</version> <scope>provided</scope> </dependency>
##2.1 tomcat默認註冊的servlet tomcat默認註冊了,映射 '/' 路徑的的DefaultServlet,映射*.jsp和*.jspx的JspServlet,這些內容配置在tomcat的conf/web.xml文件中,以下:app
<servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> </servlet> <servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>*.jsp</url-pattern> <url-pattern>*.jspx</url-pattern> </servlet-mapping>
咱們能夠修改此配置文件,來添加或者刪除一些默認的servlet配置。框架
下面來看下這些servlet的url-pattern的規則是什麼樣的eclipse
##2.2 servlet的url-pattern的規則 對於servlet的url-pattern規則,這裏也有一篇對應的源碼分析文章tomcat的url-pattern源碼分析。webapp
###2.2.1 tomcat源碼中的幾個概念 在分析以前簡單看下tomcat源碼中的幾個概念,Context、Wrapper、Servlet:
Servlet 這個很清楚,就是繼承了HttpServlet,用戶用它的service方法來處理請求
Wrapper 則是Servlet和映射的結合,具體點就是web.xml中配置的servlet信息
<servlet> <servlet-name>mvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>mvc</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>
Context 表示一個應用,包含了web.xml中配置的全部信息,因此當一個請求到來時,它負責找到對應的Servlet,而後調用這個Servlet的service方法,執行咱們所寫的業務邏輯。
Context把上述的根據映射尋找Servlet的過程封裝起來交給了一個org.apache.tomcat.util.http.mapper.Mapper類來完成,因此請求匹配規則都在這個Mapper中來完成。
因此這個Mapper作了2件事情
在初始化web.xml的時候,Mapper須要收集其中的servlet及其映射信息並進行必定的處理,存儲到Mapper的內部類ContextVersion中
在請求到來的時候,它能根據請求地址,選擇出對應的servlet等信息,供使用
Mapper的內部類ContextVersion對映射對應的servlet進行了分類存儲,以下:
protected static final class ContextVersion extends MapElement { public String[] welcomeResources = new String[0]; public Wrapper defaultWrapper = null; public Wrapper[] exactWrappers = new Wrapper[0]; public Wrapper[] wildcardWrappers = new Wrapper[0]; public Wrapper[] extensionWrappers = new Wrapper[0]; //略 }
總共分紅了5種,分別是
welcomeResources 歡迎頁面,就是web.xml中能夠配置的以下內容,待會以案例的形式詳細說明它的做用
<welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> </welcome-file-list>
defaultWrapper 用於存放默認的servlet信息
exactWrappers 用於精確匹配,即要求必須如出一轍
wildcardWrappers 用於通配符匹配 如 /*、/abc/*
extensionWrappers 用於擴展名匹配,即 .jsp、.html等
下面就來看看Mapper是如何進行歸類處理的
###2.2.2 Mapper的歸類處理Servlet和映射信息
protected void addWrapper(ContextVersion context, String path, Object wrapper, boolean jspWildCard, boolean resourceOnly) { synchronized (context) { if (path.endsWith("/*")) { // Wildcard wrapper String name = path.substring(0, path.length() - 2); Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard, resourceOnly); Wrapper[] oldWrappers = context.wildcardWrappers; Wrapper[] newWrappers = new Wrapper[oldWrappers.length + 1]; if (insertMap(oldWrappers, newWrappers, newWrapper)) { context.wildcardWrappers = newWrappers; int slashCount = slashCount(newWrapper.name); if (slashCount > context.nesting) { context.nesting = slashCount; } } } else if (path.startsWith("*.")) { // Extension wrapper String name = path.substring(2); Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard, resourceOnly); Wrapper[] oldWrappers = context.extensionWrappers; Wrapper[] newWrappers = new Wrapper[oldWrappers.length + 1]; if (insertMap(oldWrappers, newWrappers, newWrapper)) { context.extensionWrappers = newWrappers; } } else if (path.equals("/")) { // Default wrapper Wrapper newWrapper = new Wrapper("", wrapper, jspWildCard, resourceOnly); context.defaultWrapper = newWrapper; } else { // Exact wrapper final String name; if (path.length() == 0) { // Special case for the Context Root mapping which is // treated as an exact match name = "/"; } else { name = path; } Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard, resourceOnly); Wrapper[] oldWrappers = context.exactWrappers; Wrapper[] newWrappers = new Wrapper[oldWrappers.length + 1]; if (insertMap(oldWrappers, newWrappers, newWrapper)) { context.exactWrappers = newWrappers; } } } }
上面幾個if else語句就解釋的很清楚
以 /* 結尾的,都歸入通配符匹配,存到ContextVersion的wildcardWrappers中
以 *.開始的,都歸入擴展名匹配中,存到ContextVersion的extensionWrappers中
/ ,做爲默認的,存到ContextVersion的defaultWrapper中
其餘的都做爲精準匹配,存到ContextVersion的exactWrappers中
此時咱們可能會想,url形式多樣,也不會僅僅只有這幾種吧。如/a/*.jsp,即不是以 /* 結尾,也不是以 *. 開始,貌似只能分配到精準匹配中去了,這又不太合理吧。實際上tomcat就把url形式限制死了,它會進行相應的檢查,以下
private boolean validateURLPattern(String urlPattern) { if (urlPattern == null) return (false); if (urlPattern.indexOf('\n') >= 0 || urlPattern.indexOf('\r') >= 0) { return (false); } if (urlPattern.equals("")) { return true; } if (urlPattern.startsWith("*.")) { if (urlPattern.indexOf('/') < 0) { checkUnusualURLPattern(urlPattern); return (true); } else return (false); } if ( (urlPattern.startsWith("/")) && (urlPattern.indexOf("*.") < 0)) { checkUnusualURLPattern(urlPattern); return (true); } else return (false); }
顯然,urlPattern能夠爲"",其餘必須以 *. 或者 / 開頭,而且二者不能同時存在。/a/*.jsp不符合最後一個條件,直接報錯,tomcat啓動失敗,因此咱們不用過多的擔憂servlet標籤中的url-pattern的複雜性。
初始化歸類完成以後,當請求到來時,就須要利用已歸類好的數據進行匹配了,找到合適的Servlet來響應
###2.2.3 Mapper匹配請求對應的Servlet
在Mapper的internalMapWrapper方法中,存在着匹配規則,以下
private final void internalMapWrapper(ContextVersion contextVersion, CharChunk path, MappingData mappingData) throws Exception { //略 // Rule 1 -- Exact Match Wrapper[] exactWrappers = contextVersion.exactWrappers; internalMapExactWrapper(exactWrappers, path, mappingData); // Rule 2 -- Prefix Match boolean checkJspWelcomeFiles = false; Wrapper[] wildcardWrappers = contextVersion.wildcardWrappers; if (mappingData.wrapper == null) { internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting, path, mappingData); //略 } //略 // Rule 3 -- Extension Match Wrapper[] extensionWrappers = contextVersion.extensionWrappers; if (mappingData.wrapper == null && !checkJspWelcomeFiles) { internalMapExtensionWrapper(extensionWrappers, path, mappingData, true); } // Rule 4 -- Welcome resources processing for servlets if (mappingData.wrapper == null) { boolean checkWelcomeFiles = checkJspWelcomeFiles; //略 } //略 // Rule 7 -- Default servlet if (mappingData.wrapper == null && !checkJspWelcomeFiles) { if (contextVersion.defaultWrapper != null) { mappingData.wrapper = contextVersion.defaultWrapper.object; mappingData.requestPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); mappingData.wrapperPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); } //略 } //略 }
長長的匹配規則,有興趣的能夠去仔細研究下,對於Welcome resources匹配,下文會舉2個例子來詳細的分析其規則,其餘的咱們僅僅瞭解下大概的匹配順序就能夠了,匹配順序以下:
(1) 首先精準匹配
(2) 而後是通配符匹配
(3) 而後是擴展名匹配
(4) 而後是歡迎頁面匹配(這裏又細分了不少的規則,下面的案例分析會詳細說明)
(5) 最後是默認匹配
#3 案例分析(結合源碼)
在說明案例以前,須要先將eclipse中的tomcat信息說明白,有時候修改tomcat配置沒起做用就是由於你修改的地方不對致使的
##3.1 前提:eclipse中tomcat的配置信息
因此之後要修改所使用的tomcat信息,就直接在該項目下修改,或者直接去該項目的路徑下,直接修改對應的配置文件
這裏的wtwebapps就是tomcat默認的發佈根目錄,這個是不固定的,可配置的。
##3.2 jsp的訪問案例
舉個簡單例子:tomcat的根路徑下有一個a.jsp文件,就是上述的tomcat發佈的根目錄,在這個根目錄中,咱們放一個jsp文件,文件內容以下:
<%[@page](http://my.oschina.net/u/937418) contentType="text/html"%> <%[@page](http://my.oschina.net/u/937418) pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <h1>JSP Page</h1> Hello ${param.name}! </body> </html>
默認狀況下,即JspServlet存在,訪問 http://localhost:8080/a.jsp?name=lg ,結果以下:
若是你修改了tomcat的默認配置,去掉JspServlet的話,一樣訪問 http://localhost:8080/a.jsp?name=lg ,結果以下
這時候就,沒有了JspServlet,不會進行相應的翻譯工做,而是使用DefaultServlet直接將該文件內容進行返回。
由於tomcat默認配置了,映射 / 的DefaultServlet和映射 *.jsp 的JspServlet。在初始化web.xml的時候,上文講的Mapper類按照歸類規則,DefaultServlet做爲了默認的servlet,JspServlet做爲了擴展名的servlet,它比DefaultServlet的級別高,執行了擴展名匹配,因此返回了翻譯後的jsp的內容。當去掉JspServlet時,使用了DefaultServlet,執行了默認匹配,此時的jsp文件僅僅是一個通常的資源文件,返回了jsp的原始內容。
##3.3 welcome-file-list案例
它是具備兩種做用的,做爲項目的主頁和做爲跳轉的階梯,下面先介紹兩個案例,而後根據源碼分析其緣由。
注意點: 我把項目的根目錄做爲tomcat的發佈的目錄,因此訪問 http://localhost:8080/ 中再也不加入項目名
###3.3.1 做爲項目的主頁
案例1:在項目的根路徑下,放置一個a.html文件,FirstServlet攔截 /first/*,SecondServlet攔截 *.action,web.xml中是以下配置
<servlet> <servlet-name>first</servlet-name> <servlet-class>com.lg.servlet.FirstServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>first</servlet-name> <url-pattern>/first/*</url-pattern> </servlet-mapping> <servlet> <servlet-name>second</servlet-name> <servlet-class>com.lg.servlet.SecondServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>second</servlet-name> <url-pattern>*.action</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>a.html</welcome-file> </welcome-file-list>
這時,咱們訪問 http://localhost:8080/ 即想訪問項目的主頁,就能訪問到a.html的內容,以下:
案例2:對welcome-file-list稍加修改,其餘不變,以下
<welcome-file-list> <welcome-file>a.jsp</welcome-file> </welcome-file-list>
在根目錄下再存放一個a.jsp文件,以下:
<%[@page](http://my.oschina.net/u/937418) contentType="text/html"%> <%[@page](http://my.oschina.net/u/937418) pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <h1>JSP Page</h1> Hello ${param.name}! </body> </html>
訪問 http://localhost:8080/ 結果返回的是
能夠得出表面結論: 就是tomcat會根據不一樣的擴展名,使用相應的servlet來解析文件,而後返回
###3.3.2 做爲跳轉的階梯
案例3:對welcome-file-list再次修改以下,其餘不變:
<welcome-file-list> <welcome-file>a.action</welcome-file> </welcome-file-list>
這裏又能夠分2種狀況,即根目錄下是否存在a.action,然而不管哪一種狀況,訪問http://localhost:8080/,在本案例中都會返回上文配置的SecondServlet的內容(然而他們的執行邏輯倒是不同的),返回內容以下:
案例4:同理,再次修改welcome-file-list以下,訪問http://localhost:8080/,就能夠訪問到上文配置的FirstServlet的內容:
<welcome-file-list> <welcome-file>first/abc</welcome-file> </welcome-file-list>
###3.3.3 源碼解釋
下面咱們就來根據源碼分析分析整個是什麼樣的過程,這一部分詳細的源碼以下:
// Rule 4 -- Welcome resources processing for servlets if (mappingData.wrapper == null) { boolean checkWelcomeFiles = checkJspWelcomeFiles; if (!checkWelcomeFiles) { char[] buf = path.getBuffer(); checkWelcomeFiles = (buf[pathEnd - 1] == '/'); } if (checkWelcomeFiles) { for (int i = 0; (i < contextVersion.welcomeResources.length) && (mappingData.wrapper == null); i++) { path.setOffset(pathOffset); path.setEnd(pathEnd); path.append(contextVersion.welcomeResources[i], 0, contextVersion.welcomeResources[i].length()); path.setOffset(servletPath); // Rule 4a -- Welcome resources processing for exact macth internalMapExactWrapper(exactWrappers, path, mappingData); // Rule 4b -- Welcome resources processing for prefix match if (mappingData.wrapper == null) { internalMapWildcardWrapper (wildcardWrappers, contextVersion.nesting, path, mappingData); } // Rule 4c -- Welcome resources processing // for physical folder if (mappingData.wrapper == null && contextVersion.resources != null) { Object file = null; String pathStr = path.toString(); try { file = contextVersion.resources.lookup(pathStr); } catch(NamingException nex) { // Swallow not found, since this is normal } if (file != null && !(file instanceof DirContext) ) { internalMapExtensionWrapper(extensionWrappers, path, mappingData, true); if (mappingData.wrapper == null && contextVersion.defaultWrapper != null) { mappingData.wrapper = contextVersion.defaultWrapper.object; mappingData.requestPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); mappingData.wrapperPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); mappingData.requestPath.setString(pathStr); mappingData.wrapperPath.setString(pathStr); } } } } path.setOffset(servletPath); path.setEnd(pathEnd); } } // Rule 4d --我暫且叫它 Rule 4d (源碼並無這樣寫) if (mappingData.wrapper == null) { boolean checkWelcomeFiles = checkJspWelcomeFiles; if (!checkWelcomeFiles) { char[] buf = path.getBuffer(); checkWelcomeFiles = (buf[pathEnd - 1] == '/'); } if (checkWelcomeFiles) { for (int i = 0; (i < contextVersion.welcomeResources.length) && (mappingData.wrapper == null); i++) { path.setOffset(pathOffset); path.setEnd(pathEnd); path.append(contextVersion.welcomeResources[i], 0, contextVersion.welcomeResources[i].length()); path.setOffset(servletPath); internalMapExtensionWrapper(extensionWrappers, path, mappingData, false); } path.setOffset(servletPath); path.setEnd(pathEnd); } }
從上面能夠看到,其中的path再也不是原來的path,而是咱們的訪問path+welcome-file中配置的路徑,做爲全新的路徑,對於歡迎資源,又細分了4中規則,分別以下:
4a: 對全新的路徑進行精準匹配
4b: 對全新的路徑進行通配符匹配
4c: 根據全新的路徑,進行查找是否存在相應的文件,若是存在相應的文件,則須要將該文件返回。在返回前咱們須要進一步確認,這個文件是否是講文件內容源碼返回,仍是像jsp文件同樣,進行必定的處理而後再返回,因此又要確認下文件的擴展名是怎樣的
4d: 對全新的路徑進行擴展名匹配(與4c的目的不一樣,4c的主要目的是想返回一個文件的內容,在返回內容前涉及到擴展名匹配,因此4c的前提是存在對應路徑的文件)
有了以上的規則,咱們就來詳細看看上文的4個案例都是走的哪一個規則
案例1: a.html,4a、4b沒有匹配到,到4c的時候,找到了該文件,而後又嘗試擴展名匹配,來決定是走4c1仍是4c2,因爲.html尚未對應的servlet來處理,就使用了默認的DefaultServlet
案例2: a.jsp,同上,在走到4c的時候,找到了處理.jsp對應的servlet,因此走了4c1
案例3: a.action,若是根目錄下有a.action文件,則走到4c1的時候,進行擴展名匹配,匹配到了SecondServlet,即走了4c1,使用SecondServlet來處理請求;若是根目錄下沒有a.action文件,則走到了4d,進行擴展名匹配,一樣匹配到了SecondServlet,即走了4d,一樣使用SecondServlet來處理請求
案例4: first/abc,執行4b的時候,就匹配到了FirstServlet,因此使用FirstServlet來處理請求
至此,就把welcome-file-list完全講清楚了,有什麼問題和疑問,歡迎提問
#4 結束語
瞭解了tomcat的url-pattern的規則後,下一篇文章就要說明SpringMVC是如何來處理靜態資源的,以及他們的綜合分析。