tomcat的url-pattern的源碼分析

#1 靜態文件的處理前言分析html

最近想把SpringMVC對於靜態資源的處理策略弄清楚,如它和普通的請求有什麼區別嗎?web

有人可能就要說了,如今有些靜態資源都不是交給這些框架來處理,而是直接交給容器來處理,這樣更加高效。我想說的是,雖然是這樣,處理靜態資源也是MVC框架應該提供的功能,而不是依靠外界。spring

這裏以tomcat容器中的SpringMVC項目爲例。整個靜態資源的訪問,效果圖以下:apache

資源訪問圖

能夠分紅以下2個大的過程tomcat

  • tomcat根據url-pattern選擇servlet的過程
  • SpringMVC對靜態資源的處理過程(這個留到下一篇文章來詳細的源碼說明)

#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>
  • DefaultServlet能夠用來處理tomcat一些資源文件
  • JspServlet則用來處理一些jsp文件,對這些jsp文件進行一些翻譯

咱們能夠修改此配置文件,來添加或者刪除一些默認的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 server,是將你所安裝的tomcat的配置進行復制後,存放在當前eclipse所在工做空間路徑的server項目下,以下: 新建tomcat路徑

因此之後要修改所使用的tomcat信息,就直接在該項目下修改,或者直接去該項目的路徑下,直接修改對應的配置文件

  • 新建的tomcat server的運行環境不是你所安裝的tomcat的webapps目錄下,而是在當前eclipse所在的工做空間的.metadata文件下,具體以下: .metadata\.plugins\org.eclipse.wst.server.core ,這個目錄下會有一個或多個tmp目錄,每一個tmp目錄都對應着一個tomcat的真實運行環境,而後找到那個你所使用的tmp目錄,你就會看到以下的信息 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 ,結果以下: jsp做爲jsp的訪問結果

若是你修改了tomcat的默認配置,去掉JspServlet的話,一樣訪問 http://localhost:8080/a.jsp?name=lg ,結果以下 jsp做爲通常資源文件的訪問結果

這時候就,沒有了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的內容,以下: 訪問項目主頁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/ 結果返回的是

    訪問項目主頁a.jsp

    能夠得出表面結論: 就是tomcat會根據不一樣的擴展名,使用相應的servlet來解析文件,而後返回

###3.3.2 做爲跳轉的階梯

###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文件同樣,進行必定的處理而後再返回,因此又要確認下文件的擴展名是怎樣的

    • 4c1: 嘗試尋找可以處理該文件擴展名的servlet,即進行擴展名匹配,若是找到,則使用對應的servlet
    • 4c2: 若是沒找到,則默認使用defaultWrapper,即DefaultServlet(它只會將文件內容源碼返回,不作任何處理)
  • 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是如何來處理靜態資源的,以及他們的綜合分析。

相關文章
相關標籤/搜索