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

前言

章節按部就班的講解了tomcat的原理,在接下來的章節中,tomcat都是基於上一章新增功能並完善,
到最後造成一個簡易版tomcat的完成品。因此有興趣的同窗請按順序閱讀,本文爲記錄第二章的知識點
以及源碼實現(造輪子)。
複製代碼

內容回顧

跟我一塊兒動手實現Tomcat(一):實現靜態Web服務器

上一章咱們實現了簡單的靜態資源web服務器,可以讀取到用戶自定義的HTML/css/js/圖片並顯示到瀏覽器以及404頁面的展現等。css

本章內容

本章會實現簡單的Servlet容器,可以根據用戶請求URI調用對應的Servlet的service()方法並執行,init()/destory()方法和HttpServletRequest/HttpServletResponse裏面的大部分方法本章仍未實現,會在下面的幾章逐步完善。java

開始以前

  • javax.servlet.Servletgit

    我們web開發的同窗都知道,剛學習web開發的時候都是先實現這個Servlet接口去自定義本身的
    Servlet類的,那麼在這裏簡單的回顧一下Servlet這個接口。
    複製代碼

    項目加個依賴:github

    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.0.1</version>
    </dependency>
    複製代碼

    Servlet接口方法一覽(具體方法幹嗎的你們應該都懂了,就不介紹了): web

    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();
    }
    複製代碼
  • 如何實現chrome

    在這裏基於上一章的代碼,只要用戶輸入127.0.0.1:8080/servlet/{servletName},咱們就將這個URI提取出具體的servlet名字,使用java.net包下的URLClassLoader將這個Servlet類加載並實例化,而後調用它的service()方法,一次Servlet調用就這樣完成啦,是否是很簡單呢,來讓咱們看看代碼怎麼去實現!!!設計模式

代碼實現

1. 實現相應的接口api

咱們先把上個章節的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

咱們的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回顯的內容就看不到了

)
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文件夾下,打開瀏覽器走一波:

搞定!

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

增強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接口的方法咱們仍未實現
- 其餘未實現的功能
複製代碼

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

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

PS:本章源碼已上傳github SimpleTomcat

相關文章
相關標籤/搜索