欢迎您光临本小站。希望您在这里可以找到自己想要的信息。。。

基于servlet实现一个web框架

Java Web water 3106℃ 0评论

servlet作为一个web规范,其本身就算做一个web开发框架,但是其web action
(响应某个URI的实现)的实现都是基于类的,不是很方便,并且3.0之前的版本还必须通过web.xml配置来增加新的action。servlet中
有一个filter的功能,可以配置所有URI的功能都经过filter。我们可以基于filter的功能来实现一个简单的web框架。在这个框架中,主
要改进URI action的映射,就像play
framework
中route的配置:

[plain] view plaincopy

  1. GET     /hello      com.codemacro.webdemo.test.TestController.hello  

  2. GET     /route      com.codemacro.webdemo.test.TestController.route  

  3. 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的编写如:

[java] view plaincopy

  1. public class TestController extends BaseController {  

  2.   // 返回字符串  

  3.   public Result index() {  

  4.     return ok("hello world");  

  5.   }  

  6.   

  7.   // HTTP 404  

  8.   public Result code404() {  

  9.     return status(404"not found");  

  10.   }  

  11.   

  12.   // 使用JSP模板渲染  

  13.   public Result template() {  

  14.     String[] langs = new String[] {"c++""java""python"};  

  15.     return ok(jsp("index.jsp")  

  16.         .put("name""kevin")  

  17.         .put("langs",  langs)  

  18.         );  

  19.   }  

  20. }  

有了action之后,配置route文件映射URI即可:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片

  1. GET /index  com.codemacro.webdemo.test.TestController.index  

  2. GET /404    com.codemacro.webdemo.test.TestController.code404  

  3. GET /index.jsp com.codemacro.webdemo.test.TestController.template  

然后配置web.xml,增加一个filter:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片

  1. <filter>  

  2.   <filter-name>MyWebFilter</filter-name>  

  3.   <filter-class>com.codemacro.webdemo.MyServletFilter</filter-class>  

  4. </filter>  

  5. <filter-mapping>  

  6.   <filter-name>MyWebFilter</filter-name>  

  7.   <url-pattern>/*</url-pattern>  

  8. </filter-mapping>  

最后以war的形式部署到Jetty webapps下即可运行。想想下次要再找个什么lightweight Java web framework,直接用这个demo就够了。接下来讲讲一些关键部分的实现。

servlet basic

基于servlet开发的话,引入servlet api是必须的:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片

  1. <dependency>  

  2.     <groupId>javax.servlet</groupId>  

  3.     <artifactId>servlet-api</artifactId>  

  4.     <version>2.5</version>  

  5.     <type>jar</type>  

  6.     <scope>compile</scope>  

  7. </dependency>  

servlet filter的接口包含:

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public class MyServletFilter implements Filter {  

  2.   // web app启动时调用一次,可用于web框架初始化  

  3.   public void init(FilterConfig conf) throws ServletException { }  

  4.   

  5.   // 满足filter url-pattern时就会调用;req/res分别对应HTTP请求和回应  

  6.   public void doFilter(ServletRequest req, ServletResponse res,  

  7.     FilterChain chain) throws IOException, ServletException { }  

  8.   

  9.   public void destroy() { }  

  10. }  

init接口可用于启动时载入routes配置文件,并建立URI到action的映射。

action manager

ActionManager负责启动时载入routes配置,建立URI到action的映射。一个URI包含了HTTP method和URI String,例如GET /index。action既然映射到了类接口上,那么可以在启动时就同过Java反射找到对应的类及接口。简单起见,每次收到URI的请求时,就创建这个类对应的对象,然后调用映射的接口即可。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. // 例如:registerAction("com.codemacro.webdemo.test.TestController", "index", "/index", "GET");  

  2. public void registerAction(String clazName, String methodName, String uri, String method) {  

  3.   try {  

  4.     uri = "/" + appName + uri;  

  5.     // 载入对应的class  

  6.     Class<? extends BaseController> clazz = (Class<? extends BaseController>) loadClass(clazName);  

  7.     // 取得对应的接口  

  8.     Method m = clazz.getMethod(methodName, (Class<?>[])null);  

  9.     // 接口要求必须返回Result  

  10.     if (m.getReturnType() != Result.class) {  

  11.       throw new RuntimeException("action method return type mismatch: " + uri);  

  12.     }  

  13.     ActionKey k = new ActionKey(uri, getMethod(method));  

  14.     ActionValue v = new ActionValue(clazz, m);  

  15.     logger.debug("register action {} {} {} {}", clazName, methodName, uri, method);  

  16.     // 建立映射  

  17.     actions.put(k, v);  

  18.   } catch (Exception e) {  

  19.     throw new RuntimeException("registerAction failed: " + uri, e);  

  20.   }  

  21. }  

controller都要求派生于BaseController,这样才可以利用BaseController更方便地获取请求数据之类,例如query string/cookie 等。

收到请求时,就需要根据请求的HTTP Method和URI string取得之前建立的映射,并调用之:

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public boolean invoke(HttpServletRequest req, HttpServletResponse resp) throws IOException {  

  2.   String uri = req.getRequestURI();  

  3.   String method = req.getMethod().toUpperCase();  

  4.   try {  

  5.     // 取得之前建立的映射,Map查找  

  6.     ActionValue v = getAction(uri, method);  

  7.     // 创建新的controller对象  

  8.     BaseController ctl = (BaseController) v.clazz.newInstance();  

  9.     ctl.init(req, resp, this);  

  10.     logger.debug("invoke action {}", uri);  

  11.     // 调用绑定的接口  

  12.     Result result = (Result) v.method.invoke(ctl, (Object[]) null);  

  13.     // 渲染结果  

  14.     result.render();  

  15.   } catch (Exception e) {  

  16.     …  

  17.   }  

  18. }  

结果渲染

结果渲染无非就是把框架用户返回的结果渲染为字符串,写进HttpServletResponse。这个渲染过程可以是直接的Object.toString,或者经过模板引擎渲染,或者格式化为JSON。

通过实现具体的Result类,可以扩展不同的渲染方式,例如最基础的Result就是调用返回对象的toString

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public class Result {  

  2.   public void render() throws IOException, ServletException {  

  3.     PrintWriter writer = response.getWriter();  

  4.     // result是controller action里返回的  

  5.     writer.append(result.toString());  

  6.     writer.close();  

  7.   }  

  8. }  

为了简单,不引入第三方库,可以直接通过JSP来完成。JSP本身在servlet容器中就会被编译成一个servlet对象。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public class JSPResult extends Result {  

  2.   …  

  3.   @Override  

  4.   public void render() throws IOException, ServletException {  

  5.     // 传入一些对象到模板中  

  6.     for (Map.Entry<String, Object> entry : content.entrySet()) {  

  7.       request.setAttribute(entry.getKey(), entry.getValue());  

  8.     }  

  9.     // 委托给JSP来完成渲染  

  10.     request.getRequestDispatcher(file).forward(request, response);  

  11.   }  

  12. }  

JSP中可以使用传统的scriptlets表达式,也可以使用新的EL方式,例如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片

  1. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>  

  2. <h4>By EL</h4>  

  3. <c:forEach var="lang" items="${langs}">  

  4.   <span>${lang}</span>|  

  5. </c:forEach>  

  6.   

  7. <% String[] langs = (String[]) request.getAttribute("langs"); %>  

  8. <% if (langs != null) { %>  

  9. <% for (String lang : langs) { %>  

  10.   <span><%= lang %></span>|  

  11. <% } } %>  

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

BaseController

BaseController是一种template pattern实现,其包装了一些方便的接口给具体的controller使用,例如:

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public class BaseController {  

  2.   // 取得/index?name=kevin中的name参数值  

  3.   protected String getQueryString(String key) {  

  4.     return request.getParameter(key);  

  5.   }  

  6.   

  7.   protected Result status(int code, String text) {  

  8.     response.setStatus(code);  

  9.     return new Result(response, text);  

  10.   }  

  11.   

  12.   // 默认是HTTP 200  

  13.   protected Result ok(Object obj) {  

  14.     return new Result(response, obj);  

  15.   }  

  16.   

  17.   protected Result ok(Result result) {  

  18.     return result;  

  19.   }  

  20.   

  21.   protected JSPResult jsp(String file) {  

  22.     return new JSPResult(request, response, file, actionMgr);  

  23.   }  

  24. }  

Reverse routing

Reverse routing指的是在开发web过程中,要引入某个URL时,我们不是直接写这个URL字符串,而是写其映射的接口,以使代码更易维护(因为URL可能会随着项目进展而改变)。并且,servlet app部署后URL会带上这个app的名字前缀,例如/web-demo/index中的/web-demo。在模板文件中,例如要链接到其他URI,更好的方式当然是直接写/index

这里的实现比较丑陋,还是基于字符串的形式,例如:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片

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

通过自定义一个EL function reverse来实现。这里需要引入一个JSP的库:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片

  1. <dependency>  

  2.     <groupId>javax.servlet</groupId>  

  3.     <artifactId>jsp-api</artifactId>  

  4.     <version>2.0</version>  

  5.     <optional>true</optional>  

  6. </dependency>  

首先实现一个SimpleTagSupport,为了支持?name=kevin这种动态参数,还需要implements DynamicAttributes

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public class JSPRouteTag extends SimpleTagSupport implements DynamicAttributes {  

  2.   @Override  

  3.   // 输出最终的URL  

  4.   public void doTag() throws IOException {  

  5.     JspContext context = getJspContext();  

  6.     ActionManager actionMgr = (ActionManager) context.findAttribute(ACTION_MGR);  

  7.     JspWriter out = context.getOut();  

  8.     String uri = actionMgr.getReverseAction(action, attrMap);  

  9.     out.println(uri);  

  10.   }  

  11.   

  12.   @Override  

  13.   // name="kevin" 时调用  

  14.   public void setDynamicAttribute(String uri, String name, Object value) throws JspException {  

  15.     attrMap.put(name, value);  

  16.   }  

  17.   

  18.   // `action="xxx"` 时会调用`setAction`  

  19.   public void setAction(String action) {  

  20.     this.action = action;  

  21.   }  

  22. }  

为了访问到ActionManager,这里是通过写到Request context中实现的,相当hack。

[java] view plaincopy在CODE上查看代码片派生到我的代码片

  1. public JSPResult(HttpServletRequest req, HttpServletResponse resp, String file,   

  2.     ActionManager actionMgr) {  

  3.   super(resp, null);  

  4.   ..  

  5.   put(JSPRouteTag.ACTION_MGR, actionMgr);  

  6. }  

第二步增加一个描述这个新tag的文件 WEB-INF/route_tag.tld

[plain] view plaincopy在CODE上查看代码片派生到我的代码片

  1. <taglib>  

  2.     <tlibversion>1.0</tlibversion>  

  3.     <jspversion>1.1</jspversion>  

  4.     <shortname>URLRouteTags</shortname>  

  5.     <uri>/myweb-router</uri>  

  6.     <info></info>  

  7.   

  8.     <tag>  

  9.         <name>reverse</name>  

  10.         <tagclass>com.codemacro.webdemo.result.JSPRouteTag</tagclass>  

  11.         <bodycontent></bodycontent>  

  12.         <info></info>  

  13.         <attribute>  

  14.             <name>action</name>  

  15.             <required>true</required>  

  16.         </attribute>  

  17.         <dynamic-attributes>true</dynamic-attributes>  

  18.     </tag>  

  19. </taglib>  

最后在需要使用的JSP中引入这个自定义tag:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片

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

参考资料

转载请注明:学时网 » 基于servlet实现一个web框架

喜欢 (0)or分享 (0)

您必须 登录 才能发表评论!