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

1,780 阅读10分钟

恕我斗胆直言,对开源的 WEB 框架了解多少,有没有尝试写过框架呢?XML 的解析方式有哪些?能答出来吗?!

心中没有答案也没关系,因为通过今天的分享,能让你轻松 get 如下几点,绝对收获满满。

  • XML 解析的方式;
  • digester 的用法;
  • Java WEB 框架的实现思路;
  • 从 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.3. 画三个 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.4. 部署、启动 WEB 服务

到这一个迷你版的 WEB 框架就完事啦,把项目打成 war 包,放到 tomcat 里跑起来,验证一下。


4.5. 项目结构一览


蓝色圈住部分可以打成 miniframework.jar 包,当做可复用类库,在其它项目中直接引入,只需编写红色圈住部分的业务 Action 以及页面就好啦。

5. 答疑解谜

本次主要聊了聊 xml 解析的方式,着重分享了 digester 的用法,并站在 digester 解析 xml 的基础之上,徒手模拟了一个 WEB 的迷你版的框架。

如果你研究过 Tomcat 的源码或者使用过 Struts 的话,今天的分享应该很容易掌握,因为它们都用到了 digester 进行解析配置文件。

鉴于目前据我知道的很多公司的老项目,技术栈还停留在 Struts 上,所以有必要进行一次老技术新谈。

坊间这么说「只要会 XML 解析,搞懂反射,熟悉 Servlet,面试问到什么框架都不怕,因为打通了任督二脉,框架看一眼就基本知道原理啦」。

不过,技术更新确实快,稍有不慎就 out,不过在追逐新技术的同时,老技术的思想理念也别全抛在脑后,如果真能打通任督二脉,做到融会贯通那就最好啦。

好了,本次的分享就到这里,希望你能够喜欢。

一猿小讲分享初心:一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。
欢迎关注「一猿小讲」 get 更多项目实战经验!