原文位址:JavaSE 手寫 Web 伺服器(二)
部落格位址:http://www.extlight.com
一、背景
在上一篇文章 《JavaSE 手寫 Web 伺服器(一)》 中介紹了編寫 web 伺服器的初始模型,封裝請求與響應和多線程處理的内容。但是,還是遺留一個問題:如何根據不同的請求 url 去觸發不同的業務邏輯。
這個問題将在本篇解決。
二、涉及知識
XML:将配置資訊寫到 XML 檔案,解決寫死問題。
反射:讀取 XML 檔案配置并執行個體化對象。
三、封裝控制器
目前手寫的 web 容器隻能處理一種業務請求,無論發送什麼 url 的請求都是隻傳回一個内容相似的頁面。
為了能很好的擴充 web 容器處理不同請求的功能,我們需要将 request 和 response 封裝到控制器,讓各個業務的控制器處理對應請求和響應。
3.1 抽離控制器
建立一個父類控制器:
public class Servlet {
public void service(Request request, Response response) {
doGet(request,response);
doPost(request,response);
}
protected void doGet(Request request, Response response) {
}
protected void doPost(Request request, Response response) {
}
}
父類中使用了模闆方法的設計模式,将業務處理的行為交給子類去處理。
當我們需要一個控制器去處理登陸請求時,我們建立一個 LoginServlet 類去繼承 Servlet 并重寫 doGet 或 doPost 方法:
public class LoginServlet extends Servlet {
@Override
protected void doPost(Request request, Response response) {
response.println("<!DOCTYPE html>")
.println("<html lang=\"zh\">")
.println(" <head> ")
.println(" <meta charset=\"UTF-8\">")
.println(" <title>測試</title>")
.println(" </head> ")
.println(" <body> ")
.println(" <h3>Hello " + request.getParameter("username") + "</h3>")// 擷取登陸名
.println(" </body> ")
.println("</html>");
}
}
如果需要處理注冊請求,建立一個 RegisterServlet 類繼承 Servlet 并重寫 doGet 或 doPost 方法即可。
3.2 建立 web 容器上下文
為了能更好的管理多個控制器執行個體以及請求 url 與控制器的對應關系,我們需要一個類對其封裝和管理。
/**
* web 容器上下文
* @author Light
*
*/
public class ServletContext {
// 存儲處理不同請求的 servlet
// servletName -> servlet 子類
private Map<String,Servlet> servletMap;
// 存儲請求 url 與 servlet 的對應關系
// 請求 url -> servletName
private Map<String,String> mappingMap;
public ServletContext() {
this.servletMap = new HashMap<String, Servlet>();
this.mappingMap = new HashMap<String, String>();
}
public Map<String, Servlet> getServletMap() {
return servletMap;
}
public void setServletMap(Map<String, Servlet> servletMap) {
this.servletMap = servletMap;
}
public Map<String, String> getMappingMap() {
return mappingMap;
}
public void setMappingMap(Map<String, String> mappingMap) {
this.mappingMap = mappingMap;
}
}
3.3 建立配置類
雖然有了 web 容器上下文,但是 web 容器上下文并不是一開始就存放配置資訊的。配置資訊在 web 容器啟動時被注冊或寫入到上下文中,是以需要一個管理配置的類負責該操作:
/**
* 配置檔案類
* @author Light
*
*/
public class WebApp {
private static ServletContext context;
static {
context = new ServletContext();
Map<String,Servlet> servletMap = context.getServletMap();
Map<String,String> mappingMap = context.getMappingMap();
// 注冊 登陸控制器
servletMap.put("login", new LoginServlet());
mappingMap.put("/login", "login");
// 如有更多請求需要處理,在此配置對應的控制器即可
}
/**
* 通過請求 url 擷取對應的 Servlet 對象
* @param url
* @return
*/
public static Servlet getServlet(String url) {
if (url == null || "".equals(url.trim())) {
return null;
}
String servletName = context.getMappingMap().get(url);
Servlet servlet = context.getServletMap().get(servletName);
return servlet;
}
}
改造 Dispatcher 的 run 方法,從 WebApp 類中擷取控制器執行個體:
@Override
public void run() {
// 擷取控制器
Servlet servlet = WebApp.getServlet(this.request.getUrl());
// 處理請求
servlet.service(request, response);
try {
this.response.pushToClient(code);
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
四、解決寫死問題
在 WebApp 類中,我們配置了 LoginServlet 類以及相關資訊,這種寫法屬于寫死,且這個兩個類發生了耦合。
為了解決上述問題,我們可以将 LoginServlet 類的配置寫到一個 xml 檔案中,WebApp 類負責讀取和解析 xml 檔案中的資訊并将資訊寫入到 web 容器上下文中,在 Dispatcher 類中通過反射執行個體化控制器對象處理請求。
建立 web.xml 配置檔案:
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.light.controller.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
</web-app>
建立兩個類用于封裝 web.xml 配置檔案中的資料,即 <servlet> 和 <servlet-mapping> 相關标簽的内容:
/**
* 封裝 web.xml 中 <servlet> 配置資訊
* @author Light
*
*/
public class ServletXml {
private String servletName;
private String servletClazz;
public String getServletName() {
return servletName;
}
public void setServletName(String servletName) {
this.servletName = servletName;
}
public String getServletClazz() {
return servletClazz;
}
public void setServletClazz(String servletClazz) {
this.servletClazz = servletClazz;
}
}
/**
* 封裝 web.xml 中 <servlet-mapping> 配置資訊
* @author Light
*
*/
public class ServletMappingXml {
private String servletName;
private List<String> urlPattern = new ArrayList<>();
public String getServletName() {
return servletName;
}
public void setServletName(String servletName) {
this.servletName = servletName;
}
public List<String> getUrlPattern() {
return urlPattern;
}
public void setUrlPattern(List<String> urlPattern) {
this.urlPattern = urlPattern;
}
}
建立 xml 檔案解析器,用于解析 web.xml 配置檔案:
/**
* xml檔案解析器
* @author Light
*
*/
public class WebHandler extends DefaultHandler{
private List<ServletXml> servletXmlList;
private List<ServletMappingXml> mappingXmlList;
private ServletXml servletXml;
private ServletMappingXml servletMappingXml;
private String beginTag;
private boolean isMapping;
@Override
public void startDocument() throws SAXException {
servletXmlList = new ArrayList<>();
mappingXmlList = new ArrayList<>();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (qName != null) {
beginTag = qName;
if ("servlet".equals(qName)) {
isMapping = false;
servletXml = new ServletXml();
} else if ("servlet-mapping".equals(qName)){
isMapping = true;
servletMappingXml = new ServletMappingXml();
}
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (this.beginTag != null) {
String str = new String(ch,start,length);
if(isMapping) {
if("servlet-name".equals(beginTag)) {
servletMappingXml.setServletName(str);
} else if ("url-pattern".equals(beginTag)) {
servletMappingXml.getUrlPattern().add(str);
}
} else {
if("servlet-name".equals(beginTag)) {
servletXml.setServletName(str);
} else if ("servlet-class".equals(beginTag)) {
servletXml.setServletClazz(str);
}
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (qName != null) {
if ("servlet".equals(qName)) {
this.servletXmlList.add(this.servletXml);
} else if ("servlet-mapping".equals(qName)){
this.mappingXmlList.add(this.servletMappingXml);
}
}
this.beginTag = null;
}
public List<ServletXml> getServletXmlList() {
return servletXmlList;
}
public List<ServletMappingXml> getMappingXmlList() {
return mappingXmlList;
}
}
改造 ServletContext 類:
// 存儲處理不同請求的 servlet
// servletName -> servlet 子類的全名
private Map<String,String> servletMap;
即 将 private Map<String,Servlet> servletMap 改成 private Map<String,String> servletMap 。
注意,get 和 set 方法都需要修改。
改造 WebApp 類中注冊控制器相關代碼:
/**
* 配置檔案類
* @author Light
*
*/
public class WebApp {
private static ServletContext context;
static {
context = new ServletContext();
Map<String,String> servletMap = context.getServletMap();
Map<String,String> mappingMap = context.getMappingMap();
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser sax = factory.newSAXParser();
WebHandler handler = new WebHandler();
// 讀取和解析 xml 檔案
sax.parse(Thread
.currentThread()
.getContextClassLoader()
.getResourceAsStream("com/light/server/web.xml"),
handler);
// 注冊 servlet
List<ServletXml> servletXmlList = handler.getServletXmlList();
for (ServletXml servletXml : servletXmlList) {
servletMap.put(servletXml.getServletName(), servletXml.getServletClazz());
}
// 注冊 mapping
List<ServletMappingXml> mappingXmlList = handler.getMappingXmlList();
for (ServletMappingXml mapping : mappingXmlList) {
List<String> urls = mapping.getUrlPattern();
for (String url : urls) {
mappingMap.put(url, mapping.getServletName());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 通過請求 url 擷取對應的 Servlet 對象
* @param url
* @return
*/
public static String getServletClazz(String url) {
if (url == null || "".equals(url.trim())) {
return null;
}
String servletName = context.getMappingMap().get(url);
String servletClazz = context.getServletMap().get(servletName);
return servletClazz;
}
}
改造 Dispatcher 類的 run 方法,通過反射執行個體化對象:
@Override
public void run() {
try {
// 擷取控制器包名
String servletClazz = WebApp.getServletClazz(this.request.getUrl());
// 通過反射執行個體化控制器對象
Servlet servlet = (Servlet) Class.forName(servletClazz).newInstance();
// 處理請求
servlet.service(request, response);
this.response.pushToClient(code);
this.socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
五、總結
手寫 web 容器的内容到此結束。
文章中主要介紹編寫 web 容器的大緻流程,代碼中還有許多地方需要考慮(過濾器、監聽器、日志列印等)和優化,僅作為筆者對 web 容器的了解與分享,并不是為了教讀者重複造輪子。
學習東西要知其然,更要知其是以然。
六、源碼
- https://github.com/moonlightL/httpServer