天天看點

JavaSE 手寫 Web 伺服器(二)

原文位址: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