恕我斗膽直言,對開源的 WEB 框架瞭解多少,有沒有嘗試寫過框架呢?XML 的解析方式有哪些?能答出來嗎?! 心中沒有答案也沒關係,因為通過今天的分享,能讓你輕鬆 get 如下幾點,絕對收穫滿滿。 a)XML 解析的方式; b)digester 的用法; c) Java WEB 框架的實現思路; d ...
恕我斗膽直言,對開源的 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 精心為你準備的編程進階資料。