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

Geoffrey.Yip?發表於2017-12-31

前言

章節循序漸進的講解了tomcat的原理,在接下來的章節中,tomcat都是基於上一章新增功能並完善,
到最後形成一個簡易版tomcat的完成品。所以有興趣的同學請按順序閱讀,本文為記錄第二章的知識點
以及原始碼實現(造輪子)。
複製程式碼

內容回顧

跟我一起動手實現Tomcat(一):實現靜態Web伺服器

上一章我們實現了簡單的靜態資源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呼叫就這樣完成啦,是不是很簡單呢,來讓我們看看程式碼怎麼去實現!!!

跟我一起動手實現Tomcat(二):實現簡單的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回顯的內容就看不到了

跟我一起動手實現Tomcat(二):實現簡單的Servlet容器)
ServletProcess大致呼叫流程:

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資料夾下,開啟瀏覽器走一波:

搞定!

不對...其實上面的設計是有很嚴重的缺陷的

跟我一起動手實現Tomcat(二):實現簡單的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(二):實現簡單的Servlet容器

  • 解決方案

    有看過或者閱讀過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介面的方法我們仍未實現
- 其他未實現的功能
複製程式碼

在下一個章節我們會實現Request解析Parameter、HTTPHeader、Cookie等引數並重構架構模式:

跟我一起動手實現Tomcat(三):解析Request請求引數、請求頭、cookie

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

PS:本章原始碼已上傳github SimpleTomcat

相關文章