搞懂 XML 解析,徒手造 WEB 框架

一猿小講發表於2020-04-20

恕我斗膽直言,對開源的 WEB 框架了解多少,有沒有嘗試寫過框架呢?XML 的解析方式有哪些?能答出來嗎?!

心中沒有答案也沒關係,因為通過今天的分享,能讓你輕鬆 get 如下幾點,絕對收穫滿滿。

a)XML 解析的方式;

b)digester 的用法;

c)  Java WEB 框架的實現思路;

d)從 0 到 1 徒手實現一個迷你 WEB 框架。

1. XML 解析方式

在 Java 專案研發過程中,不論專案大小,幾乎都能見到 XML 配置檔案的蹤影。使用 XML 可以進行專案配置;也可以作為對接三方 API 時資料封裝、報文傳輸轉換,等等很多使用場景。

而 XML 檔案該如何解析?則是一個老生常談的問題,也是研發中選型經常面臨的一個問題。通過思維導圖梳理,把問題都扼殺在搖籃裡。

 

 

 

如導圖所示,DOM 和 SAX 是 XML 常見的兩大核心解析方式,兩者的主要區別在於它們解析 XML 檔案的方式不同。使用 DOM 解析,XML 檔案以 DOM 樹形結構載入入記憶體,而 SAX 採用的是事件模型。

基於這兩大解析方式,衍生了一系列的 API,也就是造出了一大批輪子,到底用哪款輪子呢?下面就叨咕叨咕。

 

 

 

上面羅列的這些,你都知道或者用過嗎?為了便於你記憶,我們們就聊聊發展歷史吧。

首先 JAXP 的出現是為了彌補 JAVA 在 XML 標準制定上的空白,而制定的一套 JAVA XML 標準 API,是對底層 DOM、SAX 的 API 簡單封裝;而原始 DOM 對於 Java 開發者而言較為難用,於是一批 Java 愛好者為了能讓解析 XML 得心應手,碼出了 jdom;另一批人在 jdom 的基礎上另起爐灶,碼出了 dom4j,由於 jdom 效能不抵 dom4j,dom4j 則獨佔鰲頭,很多開源框架都用 dom4j 來解析配置檔案。

XStream 本不應該出現在這裡,但是鑑於是經驗分享,索性也列了出來,在以往專案中報文轉換時用的稍微多些,尤其是支付 API 對接時用的超級多,使用它可以很容易的實現 Java 物件和 XML 文件的互轉(感興趣的可以自行填補一下)。

digester 是採用 SAX 來解析 XML 檔案,在 Tomcat 中就用 Digester 來解析配置,在 Struts 等很多開源專案,也都用到了 digester 來解析配置檔案,在實際專案研發中,也會用它來做協議解析轉換,所以這塊有必要深入去說一下,對你看原始碼應該會有幫助。

2.  digester 的用法

弱弱問一句:有沒有聽過 digester,若沒有聽過,那勢必要好好讀本文啦。

假如要對本地的 miniframework-config.xml 檔案,採用 digester 的方式進行解析,應該怎麼做?(配置檔案的內容有似曾相識的感覺沒?文末解謎)

<?xml version="1.0" encoding="UTF-8"?>
<action-mappings>
    <action path="/doOne" type="org.yyxj.miniframework.action.OneAction">
        <forward name="one" path="/one.jsp" redirect="false"/>
    </action>
    <action path="/doTwo" type="org.yyxj.miniframework.action.TwoAction">
        <forward name="two" path="/two.jsp" redirect="true"/>
    </action>
</action-mappings>

2.1. 定義解析規則檔案 rule.xml 

digester 進行解析 xml,需要依賴解析規則(就是告訴 digester 怎麼個解析法)。可以使用 Java 硬編碼的方式指定解析規則;也可以採用零配置思想,使用註解的方式來指定解析規則;還可以使用 xml 方式配置解析規則。

為了清晰起見,本次就採用 xml 方式進行配置解析規則,解析規則 rule.xml 內容如下。

<?xml version='1.0' encoding='UTF-8'?>
<digester-rules>
    <pattern value="action-mappings">
        <!-- value是匹配的xml標籤的名字,匹配<action>標籤 -->
        <pattern value="action">
            <!--每碰到一個action標籤,就建立指定類的物件-->
            <object-create-rule classname="org.yyxj.miniframework.config.ActionMapping"/>
            <!--
                物件建立後,呼叫ActionMappings的addActionMapping()方法,
                將其加入它上一級元素所對應的物件ActionMappings中
            -->
            <set-next-rule methodname="addActionMapping"/>
            <!--
                將action元素的各個屬性按照相同的名稱
                賦值給剛剛建立的ActionMapping物件
            -->
            <set-properties-rule/>
            <!-- 匹配<forward>標籤 -->
            <pattern value="forward">
                <!--每碰到一個forward標籤,就建立指定類的物件-->
                <object-create-rule classname="org.yyxj.miniframework.config.ForwardBean"/>
                <!--
                    物件建立後,呼叫ActionMapping的addForwardBean()方法,
                    將其加入它上一級元素所對應的物件ActionMapping中
                -->
                <set-next-rule methodname="addForwardBean"/>
                <!--
                    將forward元素的各個屬性按照相同的名稱
                    賦值給剛剛建立的ForwardBean物件
                -->
                <set-properties-rule/>
            </pattern>
        </pattern>
    </pattern>
</digester-rules>

2.2. 建立規則解析依賴的 Java 類

首先是 ActionMappings 類,要提供 addActionMapping 方法以便新增 ActionMapping 物件,考慮到後面會依據請求路徑找 ActionMapping,索性也定義一個 findActionMapping 的方法,程式碼如下。

package org.yyxj.miniframework.config;

import java.util.HashMap;
import java.util.Map;

/**
 * @author 一猿小講
 */
public class ActionMappings {

    private Map<String, ActionMapping> mappings = new HashMap<String, ActionMapping>();

    public void addActionMapping(ActionMapping mapping) {
        this.mappings.put(mapping.getPath(), mapping);
    }

    public ActionMapping findActionMapping(String path) {
        return this.mappings.get(path);
    }

    @Override
    public String toString() {
        return mappings.toString();
    }
}

依據解析規則檔案,接下來會匹配到 miniframework-config.xml 檔案的 action 標籤,要定義對應的 ActionMapping 類,包含請求路徑及讓誰處理的類路徑,當然也要提供 addForwardBean 方法用於新增 ForwardBean 物件,程式碼定義如下。

package org.yyxj.miniframework.config;

import java.util.HashMap;
import java.util.Map;

/**
 * @author 一猿小講
 */
public class ActionMapping {

    private String path;

    private String type;

    private Map<String, ForwardBean> forwards = new HashMap<String, ForwardBean>();

    public void addForwardBean(ForwardBean bean) {
        forwards.put(bean.getName(), bean);
    }

    public ForwardBean findForwardBean(String name) {
        return forwards.get(name);
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return path + "==" + type + "==" + this.forwards.toString();
    }
}

依據解析規則檔案,接下來會匹配到 miniframework-config.xml 檔案的 forward 標籤,那麼就要建立與之對應的 ForwardBean 類,並且擁有 name、path、redirect 三個屬性,程式碼定義如下。

package org.yyxj.miniframework.config;

/**
 * @author 一猿小講
 */
public class ForwardBean {

    private String name;

    private String path;

    private boolean redirect;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public boolean isRedirect() {
        return redirect;
    }

    public void setRedirect(boolean redirect) {
        this.redirect = redirect;
    }

    @Override
    public String toString() {
        return name + "==" + path + "==" + redirect;
    }
}

2.3. 引入依賴包,編寫測試類

<dependency>
    <groupId>commons-digester</groupId>
    <artifactId>commons-digester</artifactId>
    <version>2.1</version>
</dependency>

編寫測試類。

package org.yyxj.miniframework.config;

import org.apache.commons.digester.Digester;
import org.apache.commons.digester.xmlrules.DigesterLoader;
import org.xml.sax.SAXException;

import java.io.IOException;

/**
 * Digester用法測試類
 *
 * @author 一猿小講
 */
public class Test {

    public static void main(String[] args) throws IOException, SAXException {
        String rlueFile = "org/yyxj/miniframework/config/rule.xml";
        String configFile = "miniframework-config.xml";
        Digester digester = DigesterLoader.createDigester(
                Test.class.getClassLoader().getResource(rlueFile));
        ActionMappings mappings = new ActionMappings();
        digester.push(mappings);
        digester.parse(Test.class.getClassLoader().getResource(configFile));
        System.out.println(mappings);
    }
}

2.4. 跑起來,看看解析是否 OK?

程式輸出如下:
{/doOne=/doOne==org.yyxj.miniframework.action.OneAction=={one=one==/one.jsp==false}, /doTwo=/doTwo==org.yyxj.miniframework.action.TwoAction=={two=two==/two.jsp==true}}

到這兒 digester 解析 xml 就算達到了預期效果,digester 解析其實起來很簡單,照貓畫虎擼兩遍,就自然而然掌握,所以不要被烏央烏央的程式碼給嚇退縮(程式碼只是方便你施展 CV 大法)。

不過,會用 digester 解析 xml 還不算完事,還想擴充套件一下思路,站在上面程式碼的基礎之上,去嘗試實現一個迷你版的 WEB 框架。

3. WEB 框架的實現思路

此時請忘記 digester 解析的事情,腦海裡只需保留開篇提到的 miniframework-config.xml 檔案,怕你忘記,就再貼一遍。

 

 

圖中紅色圈住部分,其實可以這麼理解,當使用者請求的 path 為 /doOne 時,會交給 OneAction 去處理,處理完之後的返回結果若是 one,則跳轉到 one.jsp,給前端響應。

為了說的更清晰,說清楚思路,還是畫一張圖吧。

 

 

ActionServlet 主要是接收使用者請求,然後根據請求的 path 去 AcitonMappings中尋找對應的 ActionMapping,然後依據 ActionMapping 找到對應的 Action,並呼叫 Action 完成業務處理,然後把響應檢視返回給使用者,多少都透漏著 MVC 設計模式中 C 的角色。

Action 主要是業務控制器,其實很簡單,只需提供抽象的 execute 方法即可,具體怎麼執行交給具體的業務實現類去實現吧。

4. 徒手實現迷你版的 WEB 框架

鑑於 ActionMappings、ActionMapping、ForwardBean 已是可複用程式碼,主要是完成 miniframework-config.xml 檔案的解析,那接下來只需把圖中缺失的類定義一下就 Ok 啦。

4.1. 中央控制器 ActionServlet

package org.yyxj.miniframework.controller;

import org.apache.commons.digester.Digester;
import org.apache.commons.digester.xmlrules.DigesterLoader;
import org.yyxj.miniframework.config.ActionMapping;
import org.yyxj.miniframework.config.ActionMappings;
import org.yyxj.miniframework.config.ForwardBean;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 中央控制器
 * 1. 接收客戶端的請求;
 * 2. 根據請求的 path 找到對應的 Action 來處理業務。
 *
 * @author 一猿小講
 */
public class ActionServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private ActionMappings mappings = new ActionMappings();

    public static final String RULE_FILE = "org/yyxj/miniframework/config/rule.xml";

    public static final String EASY_STRUTS_CONFIG_FILE = "miniframework-config.xml";

    @Override
    public void init() {
        Digester digester = DigesterLoader.createDigester(ActionServlet.class.getClassLoader().getResource(RULE_FILE));
        digester.push(mappings);
        try {
            digester.parse(ActionServlet.class.getClassLoader().getResource(EASY_STRUTS_CONFIG_FILE));
        } catch (Exception e) {
            // LOG
        }
    }

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=utf-8");

        // 獲取請求路徑
        String uri = request.getRequestURI();
        String path = uri.substring(uri.lastIndexOf("/"), uri.lastIndexOf("."));

        // 1:根據請求路徑獲取對應的 Action 來處理具體的業務
        ActionMapping mapping = mappings.findActionMapping(path);
        try {
            Action action = (Action) Class.forName(mapping.getType()).newInstance();
            // 2:進行業務處理,並返回執行的結果
            String result = action.execute(request, response);
            // 3:依據執行結果找到對應的 ForwardBean
            ForwardBean forward = mapping.findForwardBean(result);
            // 4:響應
            if (forward.isRedirect()) {
                response.sendRedirect(request.getContextPath() + forward.getPath());
            } else {
                request.getRequestDispatcher(forward.getPath()).forward(request, response);
            }
        } catch (Exception e) {
            // LOG
            System.err.println(String.format("service ex [%s]", e.getMessage()));
        }
    }
}

4.2. 業務控制器 Action 及業務實現

package org.yyxj.miniframework.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 業務控制器
 * @author 一猿小講
 */
public abstract class Action {
    public abstract String execute(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

緊接著就定義具體的業務實現唄。

package org.yyxj.miniframework.action;

import org.yyxj.miniframework.controller.Action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 業務實現
 *
 * @author 一猿小講
 */
public class OneAction extends Action {

    @Override
    public String execute(HttpServletRequest request,
                          HttpServletResponse response) throws Exception {
        return "one";
    }
}

TwoAction 與 OneAction 一樣都是繼承了 Action,實現 execute 方法。

package org.yyxj.miniframework.action;

import org.yyxj.miniframework.controller.Action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 業務實現
 *
 * @author 一猿小講
 */
public class TwoAction extends Action {
    @Override
    public String execute(HttpServletRequest request,
                          HttpServletResponse response) throws Exception {
        return "two";
    }
}

4.3. 配置 web.xml,配置服務啟動入口

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
   http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <servlet>
        <servlet-name>ActionServlet</servlet-name>
        <servlet-class>org.yyxj.miniframework.controller.ActionServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ActionServlet</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

4.4. 畫三個 JSP 頁面出來,便於驗證

index.jsp 內容如下。

<%@ page pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>My JSP 'index.jsp' starting page</title>
  </head>
  
  <body>
       <a href="doOne.do">DoOneAction</a><br/>
       <a href="doTwo.do">DoTwoAction</a><br/>
  </body>
</html>

one.jsp 內容如下。

<%@ page pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

一猿小講 say one .... ...

two.jsp 內容如下。

<%@ page pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

一猿小講 say two .... ...

4.5. 部署、啟動 WEB 服務

到這一個迷你版的 WEB 框架就完事啦,把專案打成 war 包,放到 tomcat 裡跑起來,驗證一下。

4.6. 專案結構一覽

 

藍色圈住部分可以打成 miniframework.jar 包,當做可複用類庫,在其它專案中直接引入,只需編寫紅色圈住部分的業務 Action 以及頁面就好啦。

5. 答疑解謎

本次主要聊了聊 xml 解析的方式,著重分享了 digester 的用法,並站在 digester  解析 xml 的基礎之上,徒手模擬了一個 WEB 的迷你版的框架。

如果你研究過 Tomcat 的原始碼或者使用過 Struts 的話,今天的分享應該很容易掌握,因為它們都用到了 digester 進行解析配置檔案。

鑑於目前據我知道的很多公司的老專案,技術棧還停留在 Struts 上,所以有必要進行一次老技術新談。

坊間這麼說「只要會 XML 解析,搞懂反射,熟悉 Servlet,面試問到什麼框架都不怕,因為打通了任督二脈,框架看一眼就基本知道原理啦」。

不過,技術更新確實快,稍有不慎就 out,不過在追逐新技術的同時,老技術的思想理念也別全拋在腦後,如果真能打通任督二脈,做到融會貫通那就最好啦。

好了,本次的分享就到這裡,希望你們喜歡,請多關注一猿小講,後續會輸出更多原創精彩文章,敬請期待!

可以微信搜尋公眾號「 一猿小講 」回覆「1024」get 精心為你準備的程式設計進階資料。

相關文章