1. 程式人生 > >servlet 工作原理

servlet 工作原理

Servlet容器工作原理講解(1)
本文介紹一個簡單 servlet 容器的基本原理。
Servlet容器工作原理講解本文介紹一個簡單 servlet 容器的基本原理。現有兩個servlet容器,第一個很簡單,第二個則是根據第一個寫出。為了使第一個容器儘量簡單,所以沒有做得很完整。複雜一些的 servlet容器(包括TOMCAT4和5)在TOMCAT執行內幕的其他章節有介紹。 

兩個servlet容器都處理簡單的 servlet及staticResource。您可以使用 webroot/ 目錄下的 PrimitiveServlet 來測試它。複雜一些的 servlet會超出這些容器的容量,您可以從 TOMCAT 執行內幕 一書學習建立複雜的 servlet 容器。 


兩個應用程式的類都封裝在ex02.pyrmont 包下。在理解應用程式如何運作之前,您必須熟悉 javax.servlet.Servlet 介面。首先就來介紹這個介面。隨後,就介紹servlet容器服務servlet的具體內容。 

javax.servlet.Servlet 介面

servlet 程式設計,需要引用以下兩個類和介面:javax.servlet 和 javax.servlet.http,在這些類和介面中,javax.servlet.Servlet介面尤為重要。所有的 servlet 必須實現這個介面或繼承已實現這個介面的類。 

Servlet 介面有五個方法,如下 

public void init(ServletConfig config) throws ServletException
public void service(ServletRequest request, ServletResponse response)
throws ServletException, java.io.IOException
  public void destroy()
  public ServletConfig getServletConfig()
  public java.lang.String getServletInfo()


init、 service和 destroy 方法是 Servlet 生命週期的方法。當 Servlet 類例項化後,容器載入 init,以通知 servlet 它已進入服務行列。init 方法必須被載入,Servelt 才能接收和請求。如果要載入資料庫驅動程式、初始化一些值等等,程式設計師可以重寫這個方法。在其他情況下,這個方法一般為空。 

service 方法由 Servlet 容器呼叫,以允許 Servlet 響應一個請求。Servlet 容器傳遞 javax.servlet.ServletRequest 物件和 javax.servlet.ServletResponse 物件。ServletRequest 物件包含客戶端 HTTP 請求資訊,ServletResponse 則封裝servlet 響應。這兩個物件,您可以寫一些需要 servlet 怎樣服務和客戶怎樣請求的程式碼。 


從service中刪除Servlet例項之 前,容器呼叫destroy方法。在servlet容器關閉或servlet容器需要更多的記憶體時,就呼叫它。這個方法只有在servlet的 service方法內的所有執行緒都退出的時候,或在超時的時候才會被呼叫。在 servlet 容器呼叫 destroy方法之後,它將不再呼叫servlet的service方法。destroy 方法給了 servlet 機會,來清除所有候住的資源(比如:記憶體,檔案處理和執行緒),以確保在記憶體中所有的持續狀態和 servlet的當前狀態是同步的。Listing 2.1 包含了PrimitiveServlet 的程式碼,此servlet非常簡單,您 可以用它來測試本文中的servlet容器應用程式。 

PrimitiveServlet 類實現了javax.servlet.Servlet 並提供了五個servlet方法的介面 。它做的事情也很簡單:每次呼叫 init,service 或 destroy方法的時候,servlet就向控制口寫入方法名。service 方法也從ServletResponsec物件中獲得java.io.PrintWriter 物件,併發送字串到瀏覽器。 

Listing 2.1.PrimitiveServlet.java
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
public class PrimitiveServlet implements Servlet 
{
 public void init(ServletConfig config) throws ServletException 
{
   System.out.println("init");
    }
 public void service(ServletRequest request, ServletResponse  response)
 throws ServletException, IOException 
{
      System.out.println("from service");
      PrintWriter out = response.getWriter();
      out.println("Hello.Roses are red.");
      out.print("Violets are blue.");
    }
 public void destroy() 
{
    System.out.println("destroy");
    }
    public String getServletInfo()
 {
   return null;
    }

 public ServletConfig getServletConfig() 
{
    return null;
    }
}


Application 1

現在,我們從 servlet容器的角度來看看 servlet 程式設計。一個功能健全的 servlet容器對於每個 servlet 的HTTP請求會完成以下事情: 

當servlet 第一次被呼叫的時候,載入了 servlet類並呼叫它的init方法(僅呼叫一次) 

響應每次請求的時候 ,構建一個javax.servlet.ServletRequest 和 javax.servlet.ServletResponse例項。 

啟用servlet的service方法,傳遞 ServletRequest 和 ServletResponse 物件。 

當servlet類關閉的時候,呼叫servlet的destroy方法,並解除安裝servlet類。 

發生在 servlet 容器內部的事就複雜多了。只是這個簡單的servlet容器的功能不很健全,所以,這它只能執行非常簡單的servelt ,並不能呼叫servlet的init和destroy方法。然而,它也執行了以下動作: 

等待HTTP請求。 

構建ServletRequest和ServletResponse物件 

如果請求的是一個staticResource,就會啟用StaticResourceProcessor例項的 process方法,傳遞ServletRequest 和 ServletResponse 物件。 

如果請求的是一個servlet ,載入該類,並激活它的service方法,傳遞ServletRequest和ServletResponse 物件。注意:在這個servlet 容器,每當 servlet被請求的時候該類就被載入。 

在第一個應用程式中,servlet容器由六個類組成 。 

HttpServer1 

Request 

Response 

StaticResourceProcessor 

ServletProcessor1 

Constants 



證 如前文中的應用程式一樣,這個程式的進入口(靜態 main 方法)是HttpServer 類。這個方法建立了HttpServer例項,並呼叫它的await方法。這個方法等待 HTTP 請示,然後建立一個 request 物件和 response物件,根據請求是否是staticResource還是 servlet 來分派它們到 StaticResourceProcessor例項或ServletProcessor例項。 

Constants 類包含 static find WEB_ROOT,它是從其他類引用的。 WEB_ROOT 指明 PrimitiveServlet 位置 和容器服務的staticResource。 

HttpServer1 例項等待 HTTP 請求,直到它收到一個 shutdown 命令。釋出 shutdown命令和前文是一樣的。 

Servlet容器工作原理講解(2)
HttpServer1 類 

此應用程式內的 HttpServer1類 與前文簡單的 WEB 伺服器應用程式中的HttpServer 十分相似。但是,此應用程式內的 HttpServer1 能服務靜態資源和 servlet。如果要請求一個靜態資源,請輸入以下 URL: 

http://machineName:port/staticResource 

它就是前文中提到的怎樣在 WEB 伺服器應用程式裡請求靜態資源。如果要請求一個 servlet,請輸入以下 URL: 

http://machineName:port/servlet/servletClass 

如果您想在本地瀏覽器請求一個 PrimitiveServle servlet ,請輸入以下 URL: 

http://localhost:8080/servlet/PrimitiveServlet 

下面 Listing 2.2 類的 await 方法,是等待一個 HTTP 請求,直到一個釋出 shutdown 命令。與前文的 await 方法相似。 

Listing 2.2. HttpServer1 類的 await 方法
public void await() {
ServerSocket serverSocket = null;
int       port  = 8080;

try {
serverSocket =  new ServerSocket(port, 1,
InetAddress.getByName("127.0.0.1"));
    }
catch (IOException e) {
e.printStackTrace();
System.exit(1);
    }

// 迴圈,等待一個請求
while (!shutdown) {
Socket socket       = null;
InputStream input   = null;
OutputStream output = null;

try {
socket = serverSocket.accept();
input  = socket.getInputStream();
output = socket.getOutputStream();

// 建立請求物件並解析
Request request = new Request(input);
request.parse();

// 建立迴應物件
Response response = new Response(output);
response.setRequest(request);

//檢測是否是 servlet 或靜態資源的請求
//servlet 請求以 "/servlet/" 開始 
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
            }
else {
StaticResourceProcessor processor =
new StaticResourceProcessor();
processor.process(request, response);
            }

// 關閉socket
socket.close();

//檢測是否前面的 URI 是一個 shutdown 命令
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
        }
catch (Exception e) {
e.printStackTrace();
System.exit(1);
        }
    }
}


此文 await 方法和前文的不同點就是,此文的 await 方法中的請求排程到StaticResourceProcessor 或 ervletProcessor 。 

如果 URI中包含 "/servlet/.",請求推進到後面,否則,請求傳遞到 StaticResourceProcessor 例項 

Request 類 

Servlet service 方法接受 servlet 容器的 javax.servlet.ServletRequest 和javax.servlet.ServletResponse 例項。因此,容器必須構建 ServletRequest和ServletResponse物件,然後將其傳遞到正在被服務的service 方法。 

ex02.pyrmont.Request 類代表一個請求物件傳遞到 service 方法。同樣地,它必須實現 javax.servlet.ServletRequest 介面。這個類必須提供介面內所有方法的實現。這裡儘量簡化它並只實現幾個方法。要編譯 Request 類的話,必須提供這些方法的空實現。再來看看 request 類,內部所有需要返回一個物件例項都返回null,如下: 

public Object getAttribute(String attribute) {
return null;
  }

public Enumeration getAttributeNames() {
return null;
  }

public String getRealPath(String path) {
return null;
  }


另外,request 類仍需有前文有介紹的 parse 和getUri 方法。 

Response 類 

response 類實現 javax.servlet.ServletResponse,同樣,該類也必須提供介面內所有方法的實現。類似於 Request 類,除 getWriter 方法外,其他方法的實現都為空。 

public PrintWriter getWriter() {
// autoflush is true, println() will flush,
// but print() will not.
writer = new PrintWriter(output, true);
return writer;

}


PrintWriter 類構建器的第二個引數是一個代表是否啟用 autoflush 布林值 ,如果為真,所有呼叫println 方法都 flush 輸出。而 print 呼叫則不 flush 輸出。因此,如果在servelt 的service 方法的最後一行呼叫 print方法,則從瀏覽器上看不到此輸出 。這個不完整性在後面的應用程式內會有調整。 

response 類也包含有前文中介紹的 sendStaticResource方法。 

StaticResourceProcessor 類 

StaticResourceProcessor 類用於服務靜態資源的請求。它唯一的方法是 process。 

Listing 2.3.StaticResourceProcessor 類的 process方法。
public void process(Request request, Response response) {
try {
response.sendStaticResource();
    }
catch (IOException e) {
e.printStackTrace();
    }
}


process 方法接受兩個引數:Request 和 Response 例項。它僅僅是呼叫 response 類的 sendStaticResource 方法。

Servlet容器工作原理講解(3)
ServletProcessor1 類 

ServletProcessor1 類用來處理對 servlet 的 HTTP 請求。 它非常簡單,只包含了一個 process 方法。 而這個方法接受兩個引數: 一個javax.servlet.ServletRequest 例項和一個 avax.servlet.ServletResponse例項。 process 方法也構建了一個 java.net.URLClassLoader 物件並使用它裝載 servlet 類檔案。 在從類裝載器獲得的 Class 物件上,process 方法建立一個 servlet 例項並呼叫它的 service 方法。 

process 方法 

Listing 2.4. ServletProcessor1 類中 process 方法 

public void process(Request request, Response response) {
    String uri            = request.getUri();
    String servletName    = uri.substring(uri.lastIndexOf("/") + 1);
    URLClassLoader loader = null;

    try {
        // create a URLClassLoader
        URLStreamHandler streamHandler = null;

        URL[] urls        = new URL[1];
        File classPath    = new File(Constants.WEB_ROOT);
        String repository = (new URL("file", null, 
            classPath.getCanonicalPath() + File.separator)).toString() 
        urls[0]           = new URL(null, repository, streamHandler);
        loader            = new URLClassLoader(urls);
    }
    catch (IOException e) {
        System.out.println(e.toString());
    }

    Class myClass = null;

    try {
        myClass = loader.loadClass(servletName);
    }
    catch (Exception e) {
        System.out.println(e.toString());
    }

    Servlet servlet = null;
    try {
        servlet = (Servlet) myClass.newInstance();
        servlet.service((ServletRequest) request, (ServletResponse) response);
    }
    catch (Exception e) {
        System.out.println(e.toString());
    }
    catch (Throwable e) {
        System.out.println(e.toString());
    }
}


process方法接受兩個引數:一個 ServletRequest例項和一個 ServletResponse 例項。process方法通過呼叫 getRequestUri 方法從 ServletRequest獲取 URI。 

String uri = request.getUri();切記 URI 的格式: 

/servlet/servletName 

servletName是servlet類的名稱。 

如果要裝載 servlet 類,則需要使用以下程式碼從 URI 獲知 servlet 名稱:String servletName = uri.substring(uri.lastIndexOf("/") + 1);然後 process 方法裝載 servlet。 要做到這些,需要建立一個類裝載器,並告訴裝載器該類的位置, 該 servlet 容器可以指引類裝載器在 Constants.WEB_ROOT 指向的目錄中查詢。 在工作目錄下,WEB_ROOT 指向 webroot/ 目錄。 

如果要裝載一個 servlet,則要使用 java.net.URLClassLoader 類,它是java.lang.ClassLoader 的間接子類。 一旦有了 URLClassLoader 類的例項,就可以使用 loadClass 方法來裝載一個 servlet 類。 例項化 URLClassLoader 是很簡單的。 該類有三個構建器,最簡單的是: 

public URLClassLoader(URL[] urls); 

urls 是一組指向其位置 java.net.URL 物件, 當裝載一個類時它會自動搜尋其位置。任一以 / 結尾的 URL 都被假定為一目錄, 否則,就假定其為 .jar 檔案,在需要時可以下載並開啟。 

在一個 servlet 容器內,類裝載器查詢 servlet 類的位置稱為儲存庫 (repository)。在所舉的應用程式中,類裝載器只可在當前工作目錄下的 webroot/ 目錄查詢,所以,首先得建立一組簡單的 URL。 URL 類提供了多個構建器,因此有許多的方法來構建一個URL 物件。 在這個應用程式內,使用了和 TOMCAT 內另外一個類所使用的相同的構建器。 該構建器頭部 (signature) 如下: 

public URL(URL context, String spec, URLStreamHandler hander) 

throws MalformedURLException 

可以通過傳遞給第二個引數一個規範,傳遞給第一個和第三個引數 null 值來使用這個構建器, 但在些有另外一種可接受三個引數的構建器: 

public URL(String protocol, String host, String file) 

throws MalformedURLException 

因此,如果只寫了以下程式碼,編譯器將不知道是使用的哪個構建器: 

new URL(null, aString, null); 

當然也可以能過告訴編譯器第三個引數的型別來避開這個問題,如: 

URLStreamHandler streamHandler = null; 

new URL(null, aString, streamHandler); 

對於第二個引數,可以傳遞包含儲存庫 (repository) 的 String 。 以下程式碼可建立: 

String repository = (new URL("file", null, 

classPath.getCanonicalPath() + File.separator)).toString(); 

結合起來,以下是構建正確 URLClassLoader 例項的 process 方法的部分程式碼 

// create a URLClassLoader
URLStreamHandler streamHandler = null;
URL[] urls        = new URL[1];
File classPath    = new File(Constants.WEB_ROOT);
String repository = (new URL("file", null, 
    classPath.getCanonicalPath() + File.separator)).toString() 
urls[0]           = new URL(null, repository, streamHandler);
loader            = new URLClassLoader(urls);


建立儲存庫 (repository)的程式碼摘自org.apache.catalina.startup.ClassLoaderFactory內的 createClassLoader 方法,而建立 URL 的程式碼摘自org.apache.catalina.loader.StandardClassLoader 類內的 addRepository 方法。 但在此階段您還沒有必要去關心這些類。 

有了類裝載器,您可以使用loadClass方法裝載servlet類: 

Class myClass = null;
try {
    myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
    System.out.println(e.toString());
}


然後,process方法建立已裝載的 servlet類的例項,傳遞給 javax.servlet.Servlet ,並激活 servlet 的 service 方法: 

Servlet servlet = null;
try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) request, (ServletResponse) response);
}
catch (Exception e) {
    System.out.println(e.toString());
}
catch (Throwable e) {
    System.out.println(e.toString());
}


編譯並執行該應用程式 

如果要編譯該應用程式,在工作目錄下鍵入以下命令: 

javac -d . -classpath ./lib/servlet.jar src/ex02/pyrmont/*.java 

如果要在 windows 下執行該應用程式,在工作目錄下鍵入以下命令: 

java -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer1 

在 linux 環境下,使用冒號來隔開類庫: 

java -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer1 

如果要測試該應用程式,請在 URL 或瀏覽器位址列鍵入以下命令: 

http://localhost:8080/index.html 

或者是: 

http://localhost:8080/servlet/PrimitiveServlet 

您將會在瀏覽器中看到以下文字: 

Hello. Roses are red. 

注意:您不能看到第二行字元 (Violets are blue),因為只有第一行字元送入到瀏覽器。 Tomcat 執行工作原理 隨後的章節會告訴您怎樣來解決這個問題。

Servlet容器工作原理講解(4)
Application 2 

第一個應用程式裡存在一個值得注意的問題。 在ServletProcessor1 類的 process 方法裡,上溯 (upcast)ex02.pyrmont.Request 例項到 javax.servlet.ServletRequest,將其作為第一個引數傳遞給 servlet 的 service 方法。 另上溯(upcast) ex02.pyrmont.Response 例項到 javax.servlet.ServletResponse ,並將其作為第二個引數傳遞給 servlet 的 service 方法。 

try {
   servlet = (Servlet) myClass.newInstance();
   servlet.service((ServletRequest) request, (ServletResponse) response);
}


這樣會使安全效能大打折扣。 知道 servlet 容器工作原理的程式設計師可以將 ServletRequest 和 ServletResponse 例項向下轉型 (downcast) 到Request 和 Response ,並呼叫它們的 public 方法。 Request 例項能呼叫它的 parse 方法; Request 例項能呼叫它的 sendStaticResource 方法。 

可以將 parse 和 sendStaticResource 方法設為 private,因為在 ex02.pyrmont 裡將會從其他類裡呼叫它們。 然而,這兩個方法在 servlet 內應該是不可用的。 一個解決方法是:給 Request 和 Response 類一個預設的訪問修飾符,以致他們在 ex02.pyrmont 外不能被使用。 但還有一個更好的解決方法: 使用 facade 類。 

在第二個應用程式內,新增兩個 facade 類:RequestFacade 和 ResponseFacade。 RequestFacade 類實現 ServletRequest 介面,並通過傳遞 Request 例項來例項化, Request 例項將在 ServletRequest 物件的構建器裡被引用 。 ServletRequest 物件本身是 private 型別的,不能在類之外訪問。 就構建 RequestFacade 物件,並將其傳遞給 service 方法,而不上溯 (upcast) Request 物件給 ServletRequest,並將其傳遞給 service 方法。 servlet 程式設計師仍舊可以向下轉型 (downcast) ServletRequest 到 RequestFacade,但是,只要訪問 ServletRequest 介面的可用方法就可以了。 現在,parseUri 就安全了。 

Listing 2.5 顯示 RequestFacade 類部分程式碼: 

Listing 2.5. RequestFacade 類 

package ex02.pyrmont;

public class RequestFacade implements ServletRequest {
    private ServletRequest request = null;

    public RequestFacade(Request request) {
        this.request = request;
    }

    /* implementation of the ServletRequest*/
    public Object getAttribute(String attribute) {
        return request.getAttribute(attribute);
    }

    public Enumeration getAttributeNames() {
        return request.getAttributeNames();
    }

    ...
}


注意 RequestFacade 建構函式。 它會接受一個 Request 物件,即刻分配給私有的 servletRequest 物件引用。 還要注意,RequestFacade 內的每個方法呼叫 ServletRequest 物件內相應的方法。 

ResponseFacade 類也是如此。 

以下是 application 2 所包含的類 

HttpServer2 
Request 
Response 
StaticResourceProcessor 
ServletProcessor2 
Constants 
HttpServer2 類類似於 HttpServer1,
只是它在 await 方法內使用了 ServletProcessor2 而不是ServletProcessor1。
if (request.getUri().startsWith("/servlet/")) {
   ServletProcessor2 processor = new ServletProcessor2();
   processor.process(request, response);
}
else {
    ...
}
ServletProcessor2 類也類似於 ServletProcessor1,
只是在以下 process 方法的部分程式碼有點不同:
Servlet servlet = null;
RequestFacade requestFacade   = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);

try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) requestFacade, 
        (ServletResponse) responseFacade);
}


編譯並執行該應用程式 

如果要編譯該應用程式,在工作目錄下鍵入以下命令: 

javac -d . -classpath ./lib/servlet.jar src/ex02/pyrmont/*.java 

如果要在 windows 下執行該應用程式,在工作目錄下鍵入以下命令: 

java-classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer2 

在linux環境下,使用分號來隔開類庫: 

java -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer2 

您可以使用和 application 1 相同的 URL 以收到同樣的結果。 

總結 

本文討論了簡單的能夠用於服務靜態資源,以及處理如 PrimitiveServlet 一樣簡單的 servlet 的 servlet 容器。 同時也提供 javax.servlet.Servlet 的背景資訊。