基於servlet實現一個web框架
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框架