前言
章節循序漸進的講解了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呼叫就這樣完成啦,是不是很簡單呢,來讓我們看看程式碼怎麼去實現!!!
程式碼實現
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回顯的內容就看不到了
)
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