Tomcat剖析(二):一個簡單的Servlet服務器


Tomcat剖析(二):一個簡單的Servlet服務器

第一部分:概述

這一節基於《深度剖析Tomcat》第二章: 一個簡單的Servlet服務器 總結而成。

上一節,我們了解了一個簡單的Web服務器的總體處理流程是怎樣的;這一節,我們開始搭建一個簡單的Servlet容器,也就是增加實現了servlet的簡單加載執行,而不僅僅是將文件內容輸出到瀏覽器上。當然,這一節的servlet的實現是最簡單的,用來了解整個Servlet的大致工作流程。

最好先到我的github上下載本書相關的代碼,同時去上網下載這本書。

  • 說到servlet,首先得先了解 javax.servlet.Servlet 接口,Servlet接口中的方法描述了servlet的生命周期方法,即init、service和destroy。我們自己所有的servlet必須實現這個接口或者繼承實現了這個接口的類(最一般的情況就是繼承HttpServlet)。對於servlet生命周期的方法就不在這里具體描述了,可以自行查詢關於“servlet生命周期”的資料。在這里只要知道Servlet接口可以完成“在請求第一次到來時初始化servlet、對每次請求調用service方法執行請求、servlet關閉時調用destroy方法銷毀servlet”3個功能就夠了。

    一個Http請求過來時,Servlet服務器經歷以下過程:

    1. Server創建一個serverSocket對象,等待Client發送請求
    2. Client發送請求后,Server獲取用戶socket,從而得到請求的輸入輸出流
    3. 從輸入輸出流中創建request和response對象
    4. 解析request,如果是靜態資源就創建StaticResourceProcessor實例,傳遞請求和響應給對應的方法,具體方法就是:從request中獲取URI,判斷文件是否存在,如果不存在就響應404 ,存在則將文件內容寫到瀏覽器;如果是servlet請求就創建ServletProcessor1實例,同樣傳遞請求和響應給對應的方法,完成加載servlet和調用Servlet的service方法執行。
    5. 關閉用戶socket
    6. 判斷URI是否為/SHUTDOWN,如果不是則重新進入等待請求狀態,回到第2步,否則關閉服務器
  • 整個流程主要包含5個類,HttpServer1、ServletProcessor1、StaticResourceProcessor、Request、Response。

    HttpServer1.java:完成創建ServerSocket對象,獲取Socket對象及其輸入輸出流,解析請求,創建Request對象和Response對象,並將不同類型的請求分派給不同的Processor處理

    ServletProcessor1.java:當請求servlet時創建的對象,用於加載和執行servlet。

    StaticResourceProcessor.java:當請求的資源是靜態資源時創建的對象,調用Response對象的sendStaticResource()處理靜態資源

    Request.java:與上一節基本一樣,是對輸入流解析實現,獲取URI。不同之處在於實現javax.servlet.ServletRequest 接口,這一節中除了解析請求的方法外,其它未實現的方法置為默認值。

    Reponse.java:與上一節基本一樣,完成對瀏覽器的響應,包含對請求文件存在與不存在的處理。不同之處在於實現 javax.servlet.ServletResponse接口,這一節中除了getWriter方法外,其它未實現的方法置為默認值。

  • 注意:在這一節中有些東西看起來是不合理的,以后的章節中會改進:

  1. Servlet接口僅僅是調用了service方法,沒有調用int方法和destroy方法

  2. 每一個servlet被請求時,servlet類就被加載一次

代碼陳述更方便,下面開始將結合代碼講解。

第二部分:代碼講解

HttpServer1.java

這個類和上一節HttpServer的非常相似。

不同之處只有在判斷請求的類型時進行if else處理,而不是直接用上一節的response.sendStaticResource()直接將文件內容寫到瀏覽器

對應的,如果判斷確實是請求靜態資源(即URL不以/servlet/開頭)就調用StaticResourceProcessor處理器的process方法,顯示文件到瀏覽器中

反之,如果判斷是servlet,調用ServletProcessor1的process方法加載和執行servlet。

package ex02.pyrmont;

import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;

public class HttpServer1 {

    // 用於判斷是否需要關閉服務器,默認是false
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    private boolean shutdown = false;

    public static void main(String[] args) {
        HttpServer1 server = new HttpServer1();
        server.await();
    }

    public void await() {

        ServerSocket serverSocket = null;
        int port = 8080;
        try {
            //創建服務器端的ServerSocket
            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();//Server一直等待直到Client發送請求
                input = socket.getInputStream();//接收請求后獲取輸入輸出流
                output = socket.getOutputStream();

                //創建request對象,傳入input參數用於獲取輸入流的參數
                Request request = new Request(input);
                request.parse();//解析request對象,這一節同樣也只是獲取請求中請求行的URI

                //創建response對象,傳入output對象用於獲取Writer對象
                Response response = new Response(output);
                response.setRequest(request);

                //下面是這個類的關鍵所在
                //當URL是以/servlet/開頭時說明請求servlet,使用ServletProcessor1處理器處理
                //否則說明是請求靜態資源,由StaticResourceProcessor處理器處理
                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);
            }
        }
    }
}

HttpServer1.java的詳細說明

可以參照第一節的HttpServer.java的詳細說明

由此類可以看出:

  1. 要請求一個靜態資源,你可以在你的瀏覽器地址欄或者網址框里邊敲入一個URL:http://machineName:port/staticResource。
  2. 要請求一個 servlet,你可以使用下面的URL:http://machineName:port/servlet/servletClass

ServletProcessor1.java

這個類是這一節的關鍵類,

僅僅只有一個process方法,但方法內部卻沒那么簡單。

先上代碼

package ex02.pyrmont;

import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.io.File;
import java.io.IOException;

import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class ServletProcessor1 {

    public void process(Request request, Response response) {

        String uri = request.getUri();// 獲取URI,如/servlet/className
        // 要知道servlet名,截取第二段,即可獲得className
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);
        URLClassLoader loader = null;

        try {
            //try塊中用於創建URLClassLoader對象
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            //Constants類存儲靜態常量,在本節最后貼上。
            //Constants.WEB_ROOT即System.getProperty("user.dir") + File.separator  + "webroot";
            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);
            //經過以上過程后可以得到類似“file:E:/java/tomcat/servletName/”的路徑
            //最后URLClassLoader對象根據這個url獲取到相應路徑下serlvet的類加載器
            loader = new URLClassLoader(urls);

        } catch (IOException e) {
            System.out.println(e.toString());
        }
        Class myClass = null;
        try {
            myClass = loader.loadClass(servletName);  //根據反射獲取Class對象
        } catch (ClassNotFoundException e) {
            System.out.println(e.toString());
        }

        Servlet servlet = null;
        try {
            servlet = (Servlet) myClass.newInstance();//創建Servlet實例
            servlet.service((ServletRequest) request,//執行servlet
                    (ServletResponse) response);
        } catch (Exception e) {
            System.out.println(e.toString());
        } catch (Throwable e) {
            System.out.println(e.toString());
        }

    }
}

ServletProcess1.java詳細說明:

  • 要加載 servlet,你可以使用 java.net.URLClassLoader 類,它是 java.lang.ClassLoader類的一個直接子類。
  • public URLClassLoader(URL[] urls); 這里 urls 是一個 java.net.URL 的對象數組,這些對象指向了加載類時候查找的位置。任何以/結尾的 URL 都假設是一個目錄。
  • 類加載器必須查找的地方只有一個,如工作目錄下面的 webroot目錄。因此,我們首先創建一個單個 URL 組成的數組。URL 類提供了一系列的構造方法,所以有很多構造一個 URL 對象的方式。
  • 一旦你擁有一個 URLClassLoader 實例,你使用它的 loadClass 方法去加載一個 servlet 類。(實在不懂怎么用可以自己查看API或者百度谷歌一下)
  • 然后,process 方法創建一個 servlet 類加載器的實例, 把它向下轉換(downcast) 為 javax.servlet.Servlet, 並調用 servlet 的 service 方法

這里的servlet是自己定義的,如PrimitiveServlet.java,前面說了,我們自己所有的servlet必須實現這個接口或者繼承實現了這個接口的類

訪問時用http://machineName:port/servlet/PrimitiveServlet訪問即可

package ex02.pyrmont;

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;
    }
}

Request.java、Response.java

這兩個類內容很多而且不是本節關鍵代碼,大家可以根據概述部分對這兩個類的簡單介紹,並結合我的github上的Tomcat4的代碼找到ex02.pyrmont包下的這兩個類,最后與上一節這兩個類的代碼(對應ex01.pyrmont包)對比以下。應該就沒什么大問題,這里就不說了

下面的是Constants.java,打消大家的疑慮

package ex02.pyrmont;

import java.io.File;

public class Constants {
  public static final String WEB_ROOT =
    System.getProperty("user.dir") + File.separator  + "webroot";
}

ServletProcessor2.java

ServletProcess1.java改成ServletProcess2.java,使用RequestFacade和ResponseFacade進行封裝

改變的內容不多,如下

Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
try {
  servlet = (Servlet) myClass.newInstance();
  servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
}

當然,HttpServer1.java改為HttpServer2.java,改變之處只有一個地方,即創建ServletProcess1實例改成ServletProcess2實例

第三部分:代碼安全性問題

在 ServletProcessor1 類的 process 方法,你向上轉換ex02.pyrmont.Request 實例為 javax.servlet.ServletRequest ,並作為第一個參數傳遞給servlet 的 service 方 法 。 你 也 向 下 轉 換 ex02.pyrmont.Response 實 例 為javax.servlet.ServletResponse,並作為第二個參數傳遞給 servlet 的 service 方法。

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

這會危害安全性。知道這個 servlet 容器的內部運作的 Servlet 程序員可以分別把ServletRequest 和 ServletResponse 實例向下轉換為 ex02.pyrmont.Request 和 ex02.pyrmont.Response,並調用他們的公共方法。擁有一個 Request 實例,它們就可以調用 parse方法。擁有一個 Response 實例,就可以調用 sendStaticResource 方法。

為了解決這個問題,我們增加了兩個 façade 類: RequestFacade 和 ResponseFacade。RequestFacade 實現了 ServletRequest 接口並通過在構造方法中傳遞一個引用了 ServletRequest 對象的 Request 實例作為參數來實例化。ServletRequest 接口中每個方法的實現都調用了 Request 對象的相應方法。然而 ServletRequest 對象本身是私有的,並不能在類的外部訪問。我們構造了一個 RequestFacade 對象並把它傳遞給 service 方法,而不是向下轉換Request 對象為 ServletRequest 對象並傳遞給 service 方法。 Servlet 程序員仍然可以向下轉換ServletRequest 實例為 RequestFacade,不過它們只可以訪問 ServletRequest 接口里邊的公共方法。現在 parseUri 方法就是安全的了。

上面這段話怎么理解呢?

其實就是門面模式(Facade Pattern)。門面模擬式簡單來說就說屏蔽某些方法,讓外部無法訪問。字面上意思就是只看到門口的內容。

現在用上面的例子中的PrimitiveServlet.java,如果在service()添加如下注釋掉的代碼

  • 前兩行注釋:結果是雖然可以強制轉換為Request對象且可以調用parse()這個私有方法,但是是運行不了了,將會拋異常

  • 后兩行注釋:結果是第一行可以正常的轉換,但是第二行是沒有這個方法的,調用不了

    public void service(ServletRequest request, ServletResponse response)
                throws ServletException, IOException {
            System.out.println("from service");
            //Request requestTest = (Request)request;
            //requestTest.parse();  //雖然可以調用,但是是報錯的
            //RequestFacade requestFacadeTest =  (RequestFacade)request;
            //requestFacadeTest.parse(); 發現沒有這個方法 
            PrintWriter out = response.getWriter();
            out.println("Hello. Roses are red.");
            out.print("Violets are blue.");
    }
    

如果還是不懂,下面有個我自己寫來測試的代碼。

  • ServletReq模擬javax.servlet.ServletRequest

  • Req模擬ex02.pyrmont.Request接口(所以這個類實現了模擬Tomcat的標准接口ServletRequest的ServletReq)

  • ReqFacade模擬ex02.pyrmont.RequestFacade(封裝了Req)

  • setAttribute方法模擬了大家都通用的方法

  • main函數中,對應代碼第8行中標准的ServletReq確實可以順利轉換為Req實體類

但是調用方法后,會報如下錯誤:

Exception in thread "main" java.lang.ClassCastException: ex02.pyrmont.ReqFacade cannot be cast to ex02.pyrmont.Req at ex02.pyrmont.MyTest.main(MyTest.java:10)

這就保證了安全性,明白了嗎?

public class MyTest {

    public static void main(String[] args){

        Req request = new Req();
        ReqFacade requestFacade = new ReqFacade(request);
        ServletReq servletReq = (ServletReq)requestFacade;
        Req req = (Req)servletReq;
        req.parse();
    }
}

interface ServletReq{

    void setAttribute();
}

class Req implements ServletReq{

    @Override
    public void setAttribute() {
        System.out.println("AAA");
    }

    public void parse(){

        System.out.println("AAA2");
    }

}
class ReqFacade implements ServletReq{

    private Req b;
    ReqFacade(Req b){

        this.b = b;
    }
    @Override
    public void setAttribute() {

        b.setAttribute();
    }
}

第四部分:小結

這一節,我們完成了根據判斷不同的URI對servlet請求和靜態資源請求分別處理的簡單實現,相比上一節難度大了一些。

下一節開始Tomcat的連接器。

相應代碼可以在我的github找到下載,拷貝到eclipse,然后打開對應包的代碼即可。

如發現編譯錯誤,可能是由於jdk不同版本對編譯的要求不同導致的,可以不管,供學習研究使用。

如果有什么疑問或錯誤,可以發表評論或者加我QQ:1096101803告知,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM