1. 程式人生 > >跟我一起動手實現Tomcat(二):實現簡單的Servlet容器

跟我一起動手實現Tomcat(二):實現簡單的Servlet容器

前言

最近筆者讀了《深入剖析tomcat》這本書(原作:《how tomcat works》),發現該書簡單易讀,每個章節
循序漸進的講解了tomcat的原理,在接下來的章節中,tomcat都是基於上一章新增功能並完善,到最後形成
一個簡易版tomcat的完成品。所以有興趣的同學請按順序閱讀,本文為記錄第二章的知識點以及原始碼實現
(造輪子)。

內容回顧

點我閱讀上一章內容
上一章我們實現了簡單的靜態資源web伺服器,能夠讀取到使用者自定義的HTML/css/js/圖片並顯示到瀏覽器以及404頁面的展示等。

本章內容

本章會實現簡單的Servlet容器,能夠根據使用者請求URI呼叫對應的Servlet的service()方法並執行,init()/destory()方法和HttpServletRequest/HttpServletResponse裡面的大部分方法本章仍未實現

,會在下面的幾章逐步完善。

開始之前

  • javax.servlet.Servlet

    咱們web開發的同學都知道,剛學習web開發的時候都是先實現這個Servlet介面去自定義自己的
    Servlet類的,那麼在這裡簡單的回顧一下Servlet這個介面。
    

    專案加個依賴:

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version
    >
    </dependency>

    Servlet介面方法一覽(具體方法幹嘛的大家應該都懂了,就不介紹了):

    public interface Servlet {
    
        public void init(ServletConfig config) throws ServletException;
    
        public ServletConfig getServletConfig();
    
        public void service(ServletRequest req, ServletResponse res)
        throws ServletException, IOException;
    
        public
    String getServletInfo(); public void destroy(); }
  • 如何實現

    在這裡基於上一章的程式碼,只要使用者輸入127.0.0.1:8080/servlet/{servletName},我們就將這個URI提取出具體的servlet名字,使用java.net包下的URLClassLoader將這個Servlet類載入並例項化,然後呼叫它的service()方法,一次Servlet呼叫就這樣完成啦,是不是很簡單呢,來讓我們看看程式碼怎麼去實現!!!

程式碼實現

1. 實現相應的介面

我們先把上個章節的Request、Response分別實現ServletRequest、ServletResponse介面(這是Servlet規範),具體實現的方法咱們什麼都不做,等以後再完善。

public class Request implements ServletRequest {
    ...省略N個方法
}
public class Response implements ServletResponse {
    /*Response只實現這個方法,把我們socket的outputStream封裝成一個PrintWriter*/
    @Override
    public PrintWriter getWriter() throws IOException {
        PrintWriter writer = new PrintWriter(outputStream,true);
        return writer;
    }
}

2. 不同資源使用不同的執行器

我們的tomcat準備要支援servlet呼叫了,那麼servlet和普通靜態資源不一樣,那麼我們在程式碼層面應該將他們隔離開來,以方便日後的擴充套件,在這裡我們實現以下兩個執行器:

    - ServletProcess 專門執行Servlet的執行器
    - StaticResourceProcess 執行靜態資源的執行器

那麼我們看看我們現在一個請求的執行流程:


好吧其實大家可以看到,跟以前變化也不是很大,只是多了個if判斷,然後把相應的執行過程丟到執行器裡面去執行而已~那我們來看看對應的實現:

  • HttpServer

    大家應該還記得HttpServer吧,是我們啟動程式的主入口以及ServerSocket監聽實現。
    它的改動不大,只是加了個if判斷:

public static void main(String[] args) {
    ServerSocket serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
     ....
    //解析使用者的請求
    Request request = new Request();
    request.setRequestStream(inputStream);
    request.parseRequest();
    //生成相應的響應
    Response response = new Response(outputStream, request);
    //根據URI呼叫不同的處理器處理請求  
    if (request.getUri().startsWith("/servlet/")) {
        new ServletProcess().process(request, response);
    } else {
        new StaticResourceProcess().process(request, response);
    }
    ...
}
  • StaticResourceProcess

    StaticResourceProcess也沒幹啥,只是呼叫了上個章節讀取靜態資源的方法

public class StaticResourceProcess {
    public void process(Request request, Response response) throws IOException {
        response.accessStaticResources();
    }
}
  • ServletProcess

    ServletProcess持有了一個URLClassLoader靜態變數,專門用來載入Servlet:

    private static final URLClassLoader URL_CLASS_LOADER;
    static {
        /*定位到我們的webroot/servlet/資料夾*/
        URL servletClassPath = new File(HttpServer.WEB_ROOT, "servlet").toURI().toURL();
        //初始化classloader
        URL_CLASS_LOADER = new URLClassLoader(new URL[]{servletClassPath});
    }

    現在我們知道以/servlet/開頭的URI請求是需要呼叫Servlet資源的,那麼我們怎麼提取Servlet的名字並初始化呢?先來看看一個URI:

    /servlet/TestServlet
    

    好像也不是很難提取,直接用String的lastIndexOf和substring方法就可以搞定啦:

    uri = uri.substring(uri.lastIndexOf("/") + 1);

    前面的難題也都解決了,那麼我們看看process是怎麼執行的:

    public void process(Request request, Response response) throws IOException {
    //就是上面的那個字串擷取方法
    String servletName = this.parseServletName(request.getUri());
    //使用URLClassLoader載入這個Servlet並例項化
    Class servletClass = = URL_CLASS_LOADER.loadClass(servletName);
    Servlet servlet = (Servlet) servletClass.newInstance();
    response.getWriter().println(new String(response.responseToByte(HttpStatusEnum.OK)));
    //呼叫servlet的service方法
    servlet.service(request,response);
}

大家可能不太理解倒數第二行的程式碼,它就是呼叫了Response.PrintWriter(我們剛才上面用socket的outputStream封裝的)物件向瀏覽器輸出了一個響應頭(不這麼做傲嬌的chrome會認為這個響應是無效的,servlet回顯的內容就看不到了)

3.準備一個自定義Servlet

我們Servlet容器也算開發完成了,我們搞一個servlet做做實驗吧~

public class TestServlet implements Servlet {
    public void init(ServletConfig config) throws ServletException {
    }
    public ServletConfig getServletConfig() {
        return null;
    }
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        System.out.println("Start invoke TestServlet ... ");
        res.getWriter().println("Hello Servlet!");
    }
    public String getServletInfo() {
        return null;
    }
    public void destroy() {
    }
}

它只是在控制檯輸出一個記錄以及向瀏覽器回顯一句話(是不是覺得不能處理引數很無聊,下面幾章我們就會實現它),把這個類編譯成class檔案,丟到我們resource/webroot/servlet資料夾下,開啟瀏覽器走一波:

搞定!
不對…其實上面的設計是有很嚴重的缺陷的

加強Request、Response安全性

  • 缺陷在哪裡

細心的哥們肯定發現了:我們在ServletProcess呼叫使用者自定義的servlet的時候,是直接將Request/Response作為引數傳入使用者的service方法中(因為我們的reuqest、response實現了ServletRequest、ServletResponse介面),那麼如果我們的這個tomcat拿去釋出給其他人使用的時候,閱讀過我們的tomcat原始碼的人的servlet就可以這樣寫:

public class TestServlet {
    public void service(HttpServletRequest request,HttpServletResponse response){
        ((Request)request).parseRequest("");
        ((Response)response).accessStaticResources();
    }
}

上面那兩個方法我們設計時是提供我們process或者其他時候使用的(所以方法不能設定為private),並不是提供給使用者呼叫的,這就破壞了封裝性了!!

  • 解決方案

    有看過或者閱讀過Tomcat原始碼的時候,發現Tomcat已經用了一種設計模式去解決這個缺陷了,就是外觀設計模式(門面設計模式),具體設計模式大家可以去搜索瞭解一下,在這裡我們也引用這種設計模式處理這個缺陷,UML類圖關係如下:


程式碼也很簡單都是呼叫內部request物件的相應方法:

public class RequestFacade implements ServletRequest{
    private Request request;
    @Override
    public Object getAttribute(String name) {
        return request.getAttribute(name);
    }
    其他實現的方法也類似...
}

在ServletProcess方法呼叫servlet時我們用Facade類包裝一下:

...
Servlet servlet = (Servlet) servletClass.newInstance();
servlet.service(new RequestFacade(request), new ResponseFacade(response));
...

就此大功告成!

使用者頂多只能將ServletRequest/ServletResponse向下轉型為RequestFacade/ResponseFacade 
但是我們沒提供getReuqest()/getResponse()方法,所以它能呼叫的方法還是相應ServletRequest、
ServletResponse介面定義的方法,這樣我們內部的方法就不會被使用者呼叫到啦~

到這裡,咱們的Tomcat 2.0 web伺服器就已經開發完成啦(滑稽臉),已經可以實現簡單的自定義Servlet呼叫,但是很多功能仍未完善:

- 每一次請求就new一次Servlet,Servlet應該在初始化專案時就應該初始化,是單例的。
- 並未遵循Servlet規範實現相應的生命週期,例如init()/destory()方法我們均未呼叫。
- ServletRequest/ServletResponse介面的方法我們仍未實現
- 其他未實現的功能

在下一個章節我們會實現類似Tomcat的聯結器(Connector)功能,並且優化我們解析HTTP協議的方法,敬請期待!