1. 程式人生 > >基於servlet實現一個web框架

基於servlet實現一個web框架

-i writer pattern mic mar ems hack dem 包括

servlet作為一個web規範。其本身就算做一個web開發框架,可是其web action (響應某個URI的實現)的實現都是基於類的,不是非常方便,而且3.0之前的版本號還必須通過web.xml配置來添加新的action。

servlet中有一個filter的功能,能夠配置全部URI的功能都經過filter。我們能夠基於filter的功能來實現一個簡單的web框架。在這個框架中,主要改進URI action的映射,就像play framework中route的配置:

GET     /hello      com.codemacro.webdemo.test.TestController.hello
GET     /route      com.codemacro.webdemo.test.TestController.route
POST    /hello      com.codemacro.webdemo.test.TestController.sayHello

即把某個URI映射到類接口級別。基於servlet實現web框架的優點不僅實現簡單,還能執行在全部支持servlet容器規範的web server上,比如Tomcat、Jetty。

本文提到的web framework demo能夠從我的github 上取得:servlet-web-framework-demo

功能

這個web framework URI action部分(或者說URI routing)如同前面描寫敘述,action的編寫如:

public class TestController extends BaseController {
  // 返回字符串
  public Result index() {
    return ok("hello world");
  }

  // HTTP 404
  public Result code404() {
    return status(404, "not found");
  }

  // 使用JSP模板渲染
  public Result template() {
    String[] langs = new String[] {"c++", "java", "python"};
    return ok(jsp("index.jsp")
        .put("name", "kevin")
        .put("langs",  langs)
        );
  }
}

有了action之後。配置route文件映射URI就可以:

GET /index  com.codemacro.webdemo.test.TestController.index
GET /404    com.codemacro.webdemo.test.TestController.code404
GET /index.jsp com.codemacro.webdemo.test.TestController.template

然後配置web.xml。添加一個filter:

<filter>
  <filter-name>MyWebFilter</filter-name>
  <filter-class>com.codemacro.webdemo.MyServletFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>MyWebFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

最後以war的形式部署到Jetty webapps下就可以執行。想想下次要再找個什麽lightweight Java web framework。直接用這個demo就夠了。接下來講講一些關鍵部分的實現。

servlet basic

基於servlet開發的話。引入servlet api是必須的:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

servlet filter的接口包括:

public class MyServletFilter implements Filter {
  // web app啟動時調用一次。可用於web框架初始化
  public void init(FilterConfig conf) throws ServletException { }

  // 滿足filter url-pattern時就會調用;req/res分別相應HTTP請求和回應
  public void doFilter(ServletRequest req, ServletResponse res,
    FilterChain chain) throws IOException, ServletException { }

  public void destroy() { }
}

init接口可用於啟動時加載routes配置文件,並建立URI到action的映射。

action manager

ActionManager負責啟動時加載routes配置。建立URI到action的映射。一個URI包括了HTTP method和URI String,比如GET /index

action既然映射到了類接口上,那麽能夠在啟動時就同過Java反射找到相應的類及接口。

簡單起見。每次收到URI的請求時,就創建這個類相應的對象,然後調用映射的接口就可以。

// 比如:registerAction("com.codemacro.webdemo.test.TestController", "index", "/index", "GET");
public void registerAction(String clazName, String methodName, String uri, String method) {
  try {
    uri = "/" + appName + uri;
    // 加載相應的class
    Class<? extends BaseController> clazz = (Class<?

extends BaseController>) loadClass(clazName); // 取得相應的接口 Method m = clazz.getMethod(methodName, (Class<?>[])null); // 接口要求必須返回Result if (m.getReturnType() != Result.class) { throw new RuntimeException("action method return type mismatch: " + uri); } ActionKey k = new ActionKey(uri, getMethod(method)); ActionValue v = new ActionValue(clazz, m); logger.debug("register action {} {} {} {}", clazName, methodName, uri, method); // 建立映射 actions.put(k, v); } catch (Exception e) { throw new RuntimeException("registerAction failed: " + uri, e); } }

controller都要求派生於BaseController。這樣才幹夠利用BaseController更方便地獲取請求數據之類,比如query string/cookie 等。

收到請求時,就須要依據請求的HTTP Method和URI string取得之前建立的映射。並調用之:

public boolean invoke(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  String uri = req.getRequestURI();
  String method = req.getMethod().toUpperCase();
  try {
    // 取得之前建立的映射,Map查找
    ActionValue v = getAction(uri, method);
    // 創建新的controller對象
    BaseController ctl = (BaseController) v.clazz.newInstance();
    ctl.init(req, resp, this);
    logger.debug("invoke action {}", uri);
    // 調用綁定的接口
    Result result = (Result) v.method.invoke(ctl, (Object[]) null);
    // 渲染結果
    result.render();
  } catch (Exception e) {
    ...
  }
}

結果渲染

結果渲染無非就是把框架用戶返回的結果渲染為字符串,寫進HttpServletResponse

這個渲染過程能夠是直接的Object.toString,或者經過模板引擎渲染。或者格式化為JSON。

通過實現詳細的Result類,能夠擴展不同的渲染方式,比如最基礎的Result就是調用返回對象的toString

public class Result {
  public void render() throws IOException, ServletException {
    PrintWriter writer = response.getWriter();
    // result是controller action裏返回的
    writer.append(result.toString());
    writer.close();
  }
}

為了簡單,不引入第三方庫,能夠直接通過JSP來完畢。JSP本身在servlet容器中就會被編譯成一個servlet對象。

public class JSPResult extends Result {
  ...
  @Override
  public void render() throws IOException, ServletException {
    // 傳入一些對象到模板中
    for (Map.Entry<String, Object> entry : content.entrySet()) {
      request.setAttribute(entry.getKey(), entry.getValue());
    }
    // 托付給JSP來完畢渲染
    request.getRequestDispatcher(file).forward(request, response);
  }
}

JSP中能夠使用傳統的scriptlets表達式,也能夠使用新的EL方式。比如:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<h4>By EL</h4>
<c:forEach var="lang" items="${langs}">
  <span>${lang}</span>|
</c:forEach>

<% String[] langs = (String[]) request.getAttribute("langs"); %>
<% if (langs != null) { %>
<% for (String lang : langs) { %>
  <span><%= lang %></span>|
<% } } %>

使用EL的話須要引入<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

BaseController

BaseController是一種template pattern實現,其包裝了一些方便的接口給詳細的controller使用,比如:

public class BaseController {
  // 取得/index?name=kevin中的name參數值
  protected String getQueryString(String key) {
    return request.getParameter(key);
  }

  protected Result status(int code, String text) {
    response.setStatus(code);
    return new Result(response, text);
  }

  // 默認是HTTP 200
  protected Result ok(Object obj) {
    return new Result(response, obj);
  }

  protected Result ok(Result result) {
    return result;
  }

  protected JSPResult jsp(String file) {
    return new JSPResult(request, response, file, actionMgr);
  }
}

Reverse routing

Reverse routing指的是在開發web過程中。要引入某個URL時,我們不是直接寫這個URL字符串。而是寫其映射的接口,以使代碼更易維護(由於URL可能會隨著項目進展而改變)。而且,servlet app部署後URL會帶上這個app的名字前綴,比如/web-demo/index中的/web-demo。在模板文件裏。比如要鏈接到其它URI,更好的方式當然是直接寫/index

這裏的實現比較醜陋,還是基於字符串的形式。比如:

<a href=‘<route:reverse action="com.codemacro.webdemo.test.TestController.hello" name="kevin"/>‘>index</a>

通過自己定義一個EL function reverse來實現。

這裏須要引入一個JSP的庫:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.0</version>
    <optional>true</optional>
</dependency>

首先實現一個SimpleTagSupport。為了支持?

name=kevin這樣的動態參數。還須要implements DynamicAttributes

public class JSPRouteTag extends SimpleTagSupport implements DynamicAttributes {
  @Override
  // 輸出終於的URL
  public void doTag() throws IOException {
    JspContext context = getJspContext();
    ActionManager actionMgr = (ActionManager) context.findAttribute(ACTION_MGR);
    JspWriter out = context.getOut();
    String uri = actionMgr.getReverseAction(action, attrMap);
    out.println(uri);
  }

  @Override
  // name="kevin" 時調用
  public void setDynamicAttribute(String uri, String name, Object value) throws JspException {
    attrMap.put(name, value);
  }

  // `action="xxx"` 時會調用`setAction`
  public void setAction(String action) {
    this.action = action;
  }
}

為了訪問到ActionManager。這裏是通過寫到Request context中實現的,相當hack。

public JSPResult(HttpServletRequest req, HttpServletResponse resp, String file, 
    ActionManager actionMgr) {
  super(resp, null);
  ..
  put(JSPRouteTag.ACTION_MGR, actionMgr);
}

第二步添加一個描寫敘述這個新tag的文件 WEB-INF/route_tag.tld

<taglib>
    <tlibversion>1.0</tlibversion>
    <jspversion>1.1</jspversion>
    <shortname>URLRouteTags</shortname>
    <uri>/myweb-router</uri>
    <info></info>

    <tag>
        <name>reverse</name>
        <tagclass>com.codemacro.webdemo.result.JSPRouteTag</tagclass>
        <bodycontent></bodycontent>
        <info></info>
        <attribute>
            <name>action</name>
            <required>true</required>
        </attribute>
        <dynamic-attributes>true</dynamic-attributes>
    </tag>
</taglib>

最後在須要使用的JSP中引入這個自己定義tag:

<%@ taglib prefix="route" uri="/myweb-router" %>

參考資料

  • Servlet生命周期與工作原理
  • JSP/Servlet工作原理
  • EL表達式
  • 使用Servlet、JSP開發Web程序
  • Java Web筆記 – Servlet中的Filter過濾器的介紹和使用 編寫過濾器
  • 實現一個簡單的Servlet容器

基於servlet實現一個web框架