读书人

用Restful2ActionMapper让Struts2支持

发布时间: 2012-11-03 10:57:43 作者: rapoo

用Restful2ActionMapper让Struts2支持REST风格的URL映射
一、概述

REST是由 Roy Fielding 在他的论文《Architectural Styles and the Design of Network-based Software Architectures》中提出的一个术语。关于REST,请参考:http://www.redsaga.com/opendoc/REST_cn.pdf

在REST的定义中,一个Web应用总是使用固定的URI向外部世界呈现(或者说暴露)一个资源,并使用不同的HTTP请求方法来处理对资源的CRUD(创建、读取、更新和删除)操作。除了我们所熟知的GET和POST这两种HTTP请求方法,HTTP还有HEAD、PUT、DELETE等请求方法。我们在Web应用中处理来自客户端的请求时,通常只考虑GET和POST,并使用某种URL映射将URL映射到对资源的某种操作。而REST架构风格则要求使用HTTP的GET、POST、PUT、DELETE来分别表示资源的读取、创建、更新、删除,而URI保持不变。举例来说,/article/2007/8/a001这个URI表示一篇文章,表示形式为:/article/{year}/{month}/{id},对这个资源的CRUD操作如下(下面的表示形式中,我省去了http://host/context/namespace这样的前缀):

读取:GET /article/2007/8/a001

创建:POST /article/2007/8/a001

更新:PUT /article/2007/8/a001

删除:DELETE /article/2007/8/a001

如果我们用传统的struts或webwork的开发方法,我们可能会定义一个ArticleAction,定义好CRUD的method,并使用不同的URI映射来表示这几种操作。比如,我们可能会使用这样的URI来读取article:/getArticle.action?year=2007&month=8&id=a001,并使用这样的URI来删除article:/deleteArticle.action?year=2007&month=8&id=a001,或者,把这几种操作用相同形式的URI来表示:/article.action?method=get&year=2007&month=8&id=a001。显然,REST风格的URI表示更友好。

Struts2和Webwork2都带了一个RestfulActionMapper来支持REST风格的URI映射,但是它的功能太弱了,表现形式也很呆板。Struts2(我使用的是Struts 2.0.9)中还有一个Restful2ActionMapper,可以更好地支持REST风格。

从struts2的官方文档中可以找到关于Restful2ActionMapper的说明: Improved restful action mapper that adds several ReST-style improvements to action mapping, but supports fully-customized URL's via XML.

我查看了Restful2ActionMapper的源码,对它有些地方的处理有异议,所以改写了这个类。以下的配置中,请使用文后附上的Restful2ActionMapper代替Struts2原来的类。

二、配置和使用

现在,我们配置struts2使它使用Restful2ActionMapper。在Web项目中,修改struts.properties文件(它最终会发布到你的web应用的WEB-INF/classes目录中):

OK!现在已经可以使用这个action了。当然,这还需要浏览器客户端的支持。当你的客户端以GET来请求/article/2007/8/a001时,struts2就会调用ArticleAction的view方法,而PUT请求则会对应到update方法,DELETE请求会对应到remove方法...

但是,如果你的客户端只支持GET和POST怎么办?Restful2ActionMapper的文档中提到:To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML, the HTTP parameter "__http_method" will be used.对于只支持GET和POST的传统网页,我们可以增加一个"__http_method"参数来模拟PUT和DELETE,比如:POST /article/2007/8/a001&__http_method=DELETE。随着Javascript和Ajax框架的发展,我们已经可以使用PUT和DELETE等方法。Ajax使用XmlHttpRequest进行操作时,在发送请求之前,可以通过设置RequestType的方式来完成对请求方法的设定。

三、不足之处
Restful2ActionMapper对REST风格的支持是不完全的。在REST风格中,我们可以使用同一个URI来获取同一个资源的多种表现形式。在发送HTTP请求时,只要我们在请求头中指定一个Accept参数,那么服务器就可以通过判断该参数来决定返回什么类型的数据。如果Accept为text/xml,服务器会返回xml格式的数据,如果Accept为text/json,则会返回json格式的数据,但URI是固定的。而Restful2ActionMapper只是作了URI的映射,并没有考虑返回数据的格式问题。要让struts2支持完全的REST风格,我们不得不对它进行改造,或者,等待它的改进。

另外,Restful2ActionMapper所定义的URL映射规则也有一个小小的“陷阱”。比如,GET /user/1表示读取id为1的user,但按照Restful2ActionMapper的定义,/user/new会对应到action的editNew方法,如果这个"new"就是某个用户的id呢?为了避开这个陷阱,我宁愿使用/user/!editNew这种丑陋的形式。事实上,随着客户端技术的发展,我们完全可以不使用editNew方法而构造输入页面,然后向服务器发送POST来创建资源。同样,edit方法也不是必要的。

(注:我修改后的Restful2ActionMapper去除了/user/new这种形式的映射)

四、其它

有个struts2的插件,叫jsonplugin,可以让struts2很方便地支持json输出。而Adobe Spry Framework、YUI-ext、DOJO等都能很好地支持json,并能很好地支持HTTP的各种请求方法。我推荐struts2的用户使用jsonplugin和Adobe Spry Framework或YUI-ext(或其它UI Framework)。Struts2只输出json格式的结果(最好还能输出xml),而UI和数据装配交给Adobe Spry/YUI-ext等去做。这样的组合会让你更好更方便地使用REST风格。

五、修改后的Restful2ActionMapper

这里我附上修改后的Restful2ActionMapper,大家可以在此基础上进行扩充。比如,我前面提到Restful2ActionMapper不能根据Accept请求头来返回不同格式的数据,其实也是可以进行改进的。我看到已经有人在读过我这篇文章后提出一种方案,类似于这样的:
/user/a001/xml => 返回xml格式的result
/user/a001/json => 返回json格式的result
/user/a001/...

这是一种办法,另外,根据url的扩展名来做,也是一种办法。但是这都不是好方案!我前面已经提过,按照REST风格,一个Web应用总是使用固定的URI向外部世界呈现(或者说暴露)一个资源,而前面这两种方案只是使URL友好点而已,并不真正符合REST风格。当然,这样也不错了,也是不错的方案,其实ROR中也有类似的做法。

但我们还有更好的方案,我提个思路,然后大家自行对Restful2ActionMapper进行改进:

在Action中可以设置一个consumeMime属性,并写好对应的getter/setter方法。在Restful2ActionMapper返回mapping之前,提取request的Accept头信息,然后将该信息放到mapping.params之中。action的各个method最后只返回consumeMime,这样就可以在action的配置文件中按consumeMime来配置result了。

下面,附上修改后的Restful2ActionMapper代码:

java 代码
import com.opensymphony.xwork2.config.ConfigurationManager;   import javax.servlet.http.HttpServletRequest;   import org.apache.commons.logging.Log;   import org.apache.commons.logging.LogFactory;   import org.apache.struts2.dispatcher.mapper.ActionMapping;      public class Restful2ActionMapper extends DefaultActionMapper {          protected static final Log LOG = LogFactory               .getLog(Restful2ActionMapper.class);       public static final String HTTP_METHOD_PARAM = "__http_method";       private static final byte HTTP_METHOD_GET = 1;       private static final byte HTTP_METHOD_POST = 2;       private static final byte HTTP_METHOD_PUT = 3;       private static final byte HTTP_METHOD_DELETE = 4;          public Restful2ActionMapper() {           setSlashesInActionNames("true");       }          /*       * (non-Javadoc)       *        * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)       */       public ActionMapping getMapping(HttpServletRequest request,               ConfigurationManager configManager) {              if (!isSlashesInActionNames()) {               throw new IllegalStateException(                       "This action mapper requires the setting 'slashesInActionNames' to be set to 'true'");           }           ActionMapping mapping = super.getMapping(request, configManager);              if (mapping == null)               return null;              String actionName = mapping.getName();           if ((actionName == null) || (actionName.length() == 0))               return mapping;              // If a method hasn't been explicitly named, try to guess using           // ReST-style patterns           if (mapping.getMethod() != null)               return mapping;              int lastSlashPos = actionName.lastIndexOf('/');           if (lastSlashPos == -1)               return mapping;           String requestMethod = request.getMethod();           String httpMethodParam = request.getParameter(HTTP_METHOD_PARAM);           byte requestMethodCode = 0;           if ("PUT".equalsIgnoreCase(requestMethod))               requestMethodCode = HTTP_METHOD_PUT;           else if ("DELETE".equalsIgnoreCase(requestMethod))               requestMethodCode = HTTP_METHOD_DELETE;           else if ("PUT".equalsIgnoreCase(httpMethodParam))               requestMethodCode = HTTP_METHOD_PUT;           else if ("DELETE".equalsIgnoreCase(httpMethodParam))               requestMethodCode = HTTP_METHOD_DELETE;           else if ("GET".equalsIgnoreCase(requestMethod))               requestMethodCode = HTTP_METHOD_GET;           else if ("POST".equalsIgnoreCase(requestMethod))               requestMethodCode = HTTP_METHOD_POST;              if (requestMethodCode == HTTP_METHOD_GET) {               if (lastSlashPos == actionName.length() - 1)                   mapping.setMethod("index");               else                   mapping.setMethod("view");           } else if (requestMethodCode == HTTP_METHOD_POST)               mapping.setMethod("create");           else if (requestMethodCode == HTTP_METHOD_PUT)               mapping.setMethod("update");           else if (requestMethodCode == HTTP_METHOD_DELETE)               mapping.setMethod("remove");              return mapping;       }   }   


用Restful2ActionMapper让Struts2支持REST风格的URL映射
http://www.ossez.com/forum.php?mod=viewthread&tid=13600&fromuid=426

读书人网 >软件架构设计

热点推荐