使用 Struts 2 开发 RESTful 服务
转载 使用 Struts 2 开发 RESTful 服务
作者:李 刚, 自由撰稿人
原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-struts2rest/
2009 年 8 月 28 日
从V2.1开始,Struts2开始提供Convention插件,它允许根据“约定”来搜索Action,以及管理Action和Result的映射。另外,Struts2.1还提供了REST插件,使Struts2可以支持Rails风格的URL,以对外提供REST风格的资源服务。本文作者通过代码示例演示了这些特性。
REST简介
REST是英文RepresentationalStateTransfer的缩写,这个术语由RoyThomasFielding博士在他的论文《ArchitecturalStylesandtheDesignofNetwork-basedSoftwareArchitectures》中提出。从这篇论文的标题可以看出:REST是一种基于网络的软件架构风格。
提示:国内很多网络资料将REST翻译为“表述性状态转移”,不过笔者对这个翻译不太认同。因为这个专业术语无法传达REST的含义,读者可以先不要理会REST到底该如何翻译,尽量先去理解REST是什么?有什么用?然后再来看这个术语的翻译。关于RoyThomasFielding博士的原文参见如下地址:http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm。
REST架构是针对传统Web应用提出的一种改进,是一种新型的分布式软件设计架构。对于异构系统如何进行整合的问题,目前主流做法都集中在使用SOAP、WSDL和WS-*规范的WebServices。而REST架构实际上也是解决异构系统整合问题的一种新思路。
如果开发者在开发过程中能坚持REST原则,将可以得到一个使用了优质Web架构的系统,从而为系统提供更好的可伸缩性,并降低开发难度。关于REST架构的主要原则如下:
网络上的所有事物都可被抽象为资源(Resource)。
每个资源都有一个唯一的资源标识符(ResourceIdentifier)。
同一资源具有多种表现形式。
使用标准方法操作资源。
通过缓存来提高性能。
对资源的各种操作不会改变资源标识符。
所有的操作都是无状态的(Stateless)。
仅从上面几条原则来看REST架构,其实依然比较难以理解,下面笔者将从如下二个方面来介绍REST。
资源和标识符
现在的Web应用上包含了大量信息,但这些信息都被隐藏在HTML、CSS和JavaScript代码中,对于普通浏览者而言,他们进入这个系统时无法知道该系统包含哪些页面;对于一个需要访问该系统资源的第三方系统而言,同样无法明白这个系统包含多少功能和信息。
URI和URL
与URI相关的概念还有URL,URL是UniformResourceLocator,也就是统一资源定位符的意思。其中http://www.crazyit.org就是一个统一资源定位符,URL是URI的子集。简而言之:每个URL都是URI,但不是每个URI都是URL。
从REST架构的角度来看,该系统里包含的所有功能和信息,都可被称为资源(Resource),REST架构中的资源包含静态页面、JSP和Servlet等,该应用暴露在网络上的所有功能和信息都可被称为资源。
除此之外,REST架构规范了应用资源的命名方式,REST规定对应用资源使用统一的命名方式:REST系统中的资源必须统一命名和规划,REST系统由使用URI(UniformResourceIdentifier,即统一资源标识符)命名的资源组成。由于REST对资源使用了基于URI的统一命名,因此这些信息就自然地暴露出来了,从而避免“信息地窖”的不良后果。
对于当今最常见的网络应用来说,资源标识符就是URI,资源的使用者则根据URI来操作应用资源。当URI发生改变时,表明客户机所使用的资源发生了改变。
从资源的角度来看,当客户机操作不同的资源时,资源所在的Web页(将Web页当成虚拟的状态机来看)的状态就会发生改变、迁移(Transfer),这就是REST术语中ST(StateTranfer)的由来了。
客户机为了操作不同状态的资源,则需要发送一些Representational的数据,这些数据包含必要的交互数据,以及描述这些数据的元数据。这就是REST术语中RE(Representational)的由来了。理解了这个层次之后,至于REST如何翻译、或是否真正给它一个中文术语,读者可自行决定。
操作资源的方式
对于REST架构的服务器端而言,它提供的是资源,但同一资源具有多种表现形式(可通过在HTTPContent-type头中包含关于数据类型的元数据)。如果客户程序完全支持HTTP应用协议,并能正确处理REST架构的标准数据格式,那么它就可以与世界上任意一个REST风格的用户交互。这种情况不仅适用于从服务器端到客户端的数据,反之亦然——倘若从客户端传来的数据符合REST架构的标准数据格式,那么服务器端也可以正确处理数据,而不去关心客户端的类型。
典型情况下,REST风格的资源能以XHTML、XML和JSON三种形式存在,其中XML格式的数据是WebServices技术的数据交换格式,而JSON则是另一种轻量级的数据交换格式;至于XHTML格式则主要由浏览器负责呈现。当服务器为所有资源提供多种表现形式之后,这些资源不仅可以被标准Web浏览器所使用,还可以由JavaScript通过Ajax技术调用,或者以RPC(RemoteProcedureCall)风格调用,从而变成REST风格的WebServices。
REST架构除了规定服务器提供资源的方式之外,还推荐客户端使用HTTP作为GenericConnectorInterface(也就是通用连接器接口),而HTTP则把对一个URI的操作限制在了4个之内:GET、POST、PUT和DELETE。通过使用通用连接器接口对资源进行操作的好处是保证系统提供的服务都是高度解耦的,从而简化了系统开发,改善了系统的交互性和可重用性。
REST架构要求客户端的所有的操作在本质上是无状态的,即从客户到服务器的每个Request都必须包含理解该Request的所有必需信息。这种无状态性的规范提供了如下几点好处:
无状态性使得客户端和服务器端不必保存对方的详细信息,服务器只需要处理当前Request,而不必了解前面Request的历史。
无状态性减少了服务器从局部错误中恢复的任务量,可以非常方便地实现FailOver技术,从而很容易地将服务器组件部署在集群内。
无状态性使得服务器端不必在多个Request中保存状态,从而可以更容易地释放资源。
无状态性无需服务组件保存Request状态,因此可让服务器充分利用Pool技术来提高稳定性和性能。
当然,无状态性会使得服务器不再保存Request的状态数据,因此需要在一系列Request中发送重复数据,从而提高了系统的通信成本。为了改善无状态性带来的性能下降,REST架构填加了缓存约束。缓存约束允许隐式或显式地标记一个Response中的数据,这样就赋予了客户端缓存Response数据的功能,这样就可以为以后的Request共用缓存的数据,部分或全部的消除一些交互,增加了网络的效率。但是用于客户端缓存了信息,也就同时增加了客户端与服务器数据不一致的可能,从而降低了可靠性。
Struts2的REST支持
约定优于配置
Convention这个单词的翻译过来就是“约定”的意思。有RubyOnRails开发经验的读者知道Rails有一条重要原则:约定优于配置。Rails开发者只需要按约定开发ActiveRecord、ActiveController即可,无需进行配置。很明显,Struts2的Convention插件借鉴了Rails的创意,甚至连插件的名称都借鉴了“约定优于配置”原则。
从Struts2.1开始,Struts2改为使用Convention插件来支持零配置。Convention插件彻底地抛弃了配置信息,不仅不需要使用struts.xml文件进行配置,甚至不需要使用Annotation进行配置。而是由Struts2根据约定来自动配置。
Convention这个单词的翻译过来就是“约定”的意思。有RubyOnRails开发经验的读者知道Rails有一条重要原则:约定优于配置。Rails开发者只需要按约定开发ActiveRecord、ActiveController即可,无需进行配置。很明显,Struts2的Convention插件借鉴了Rails的创意,甚至连插件的名称都借鉴了“约定优于配置”原则。
由于Struts2的Convention插件的主要特点是“约定优于配置”,当我们已经习惯了Struts2的基本开发方法之后,如果希望改为使用Convention插件也非常容易,我们只要放弃Stuts2.1应用原有的配置文件,改为按Convention插件的约定来定义Action即可。
以Convention插件为基础,Struts2.1又新增了REST插件,允许Struts2应用对外提供REST服务。REST插件也无需使用XML进行配置管理。Struts2.1通过REST插件完全可以提供让人和机器客户端共同使用的资源,并支持RubyOnRails风格的URL。
RestActionMapper简介
从本质上来看,Struts2依然是一个MVC框架,最初设计Struts2时并没有按REST架构进行设计,因此Struts2本质上并不是一个REST框架。由于Struts2提供了良好的可扩展性,因此允许通过REST插件将其扩展成支持REST的框架。REST插件的核心是RestActionMapper,它负责将Rails风格的URL转换为传统请求的URL。
用WinRAR打开struts2-rest-plugin-2.1.6文件,看到该文件里包含一个struts-plugin.xml文件,该文件中包含如下一行:
<!--定义支持REST的ActionMapper-->
<beantype="org.apache.struts2.dispatcher.mapper.ActionMapper"
name="rest"class="org.apache.struts2.rest.RestActionMapper"/>
通过查看RestActionMapper的API说明,我们发现它可接受如下几个参数:
struts.mapper.idParameterName:用于设置ID请求参数的参数名,该属性值默认是id。
struts.mapper.indexMethodName:设置不带id请求参数的GET请求调用Action的哪个方法。该属性值默认是index。
struts.mapper.getMethodName:设置带id请求参数的GET请求调用Action的哪个方法。该属性值默认是show。
struts.mapper.postMethodName:设置不带id请求参数的POST请求调用Action的哪个方法。该属性值默认是create。
struts.mapper.putMethodName:设置带id请求参数的PUT请求调用Action的哪个方法。该属性值默认是update。
struts.mapper.deleteMethodName:设置带id请求参数的DELETE请求调用Action的哪个方法。该属性值默认是destroy。
struts.mapper.editMethodName:设置带id请求参数、且指定操作edit资源的GET请求调用Action的哪个方法。该属性值默认是edit。
struts.mapper.newMethodName:设置不带id请求参数、且指定操作edit资源的GET请求调用Action的哪个方法。该属性值默认是editNew。
在RestActionMapper的方法列表中,我们看到setIdParameterName、setIndexMethodName、setGetMethodName、setPostMethodName、setPutMethodName、setDeleteMethodName、setEditMethodName、setNewMethodName等方法,这些方法对应为上面列出的方法提供setter支持。
通常情况下,我们没有必要改变RestActionMapper的参数,直接使用这些参数的默认值就可支持Rails风格的REST。根据前面介绍可以看出:支持REST风格的Action至少包含如下7个方法:
index:处理不带id请求参数的GET请求。
show:处理带id请求参数的GET请求。
create:处理不带id请求参数的POST请求。
update:处理带id请求参数的PUT请求。
destroy:处理带id请求参数的DELETE请求。
edit:处理带id请求参数,且指定操作edit资源的GET请求。
editNew:处理不带id请求参数,且指定操作edit资源的GET请求。
如果请求需要向服务器发送id请求参数,直接将请求参数的值附加在URL中即可。表1显示了RestActionMapper对不同HTTP请求的处理结果。
表1.RestActionMapper对HTTP请求的处理
HTTP方法URI调用Action的方法请求参数
GET/bookindex
POST/bookcreate
PUT/book/2updateid=2
DELETE/book/2destroyid=2
GET/book/2showid=2
GET/book/2/editeditid=2
GET/book/neweditNew
不幸地是,标准HTML语言目前根本不支持PUT和DELETE两个操作,为了弥补这种不足,REST插件允许开发者提交请求时额外增加一个_method请求参数,该参数值可以为PUT或DELETE,用于模拟HTTP协议的PUT和DELETE操作。
为Struts2应用安装REST插件
安装REST插件非常简单,只需按如下步骤进行即可:
将Struts2项目下struts2-convention-plugin-2.1.6.jar、struts2-rest-plugin-2.1.6.jar两个JAR包复制到Web应用的WEB-INF\lib路径下。
由于Struts2的REST插件还需要将提供XML、JSON格式的数据,因此还需要将xstream-1.2.2.jar、json-lib-2.1.jar、ezmorph-1.0.3.jar以及Jakarta-Common相关JAR包复制到Web应用的WEB-INF/lib路径下。
通过struts.xml、struts.properties或web.xml改变struts.convention.default.parent.package常量的值,让支持REST风格的Action所在的包默认继承rest-default,而不是继承默认的convention-default父包。
对于第三个步骤而言,开发者完全可以不设置该常量,如果开发者不设置该常量,则意味着开发者必须通过Annotation为每个Action类设置父包。
实现支持REST的Action类
在实现支持REST的Action之前,我们先为系统提供一个Model类:Book,该Book类非常简单,代码如下:
publicclassBook
{
privateIntegerid;
privateStringname;
privatedoubleprice;
//无参数的构造器
publicBook(){}
//id属性的setter和getter方法
publicvoidsetId(Integerid)
{
this.id=id;
}
publicIntegergetId()
{
returnthis.id;
}
//省略name和price的setter和getter方法
...
}
除了提供上面的Book类之外,我们还为该Book类提供一个业务逻辑组件:BookService。为了简单起见,BookService类不再依赖DAO组件访问数据库,而是直接操作内存中的Book数组——简单地说,本系统中状态是瞬态的,没有持久化保存,应用运行过程中这些状态一直存在,但一旦重启该应用,则系统状态丢失。下面是BookService类的代码:
publicclassBookService
{
privatestaticMap<Integer,Book>books
=newHashMap<Integer,Book>();
//保留下本图书的ID
privatestaticintnextId=5;
//以内存中的数据模拟数据库的持久存储
static{
books.put(1,newBook(1
,"疯狂Java讲义",99));
books.put(2,newBook(2
,"轻量级JavaEE企业应用实战",89));
books.put(3,newBook(3
,"疯狂Ajax讲义",78));
books.put(4,newBook(4
,"Struts2权威指南",79));
}
//根据ID获取
publicBookget(intid)
{
returnbooks.get(id);
}
//获取系统中全部图书
publicList<Book>getAll()
{
returnnewArrayList<Book>(books.values());
}
//更新已有的图书或保存新图书
publicvoidsaveOrUpdate(Bookbook)
{
//如果试图保存的图书的ID为null,表明是保存新的图书
if(book.getId()==null)
{
//为新的图书分配ID。
book.setId(nextId++);
}
//将保存book
books.put(book.getId(),book);
}
//删除图书
publicvoidremove(intid)
{
books.remove(id);
}
}
从上面粗体字代码可以看出,BookService提供了4个方法,用于实现对Book对象的CRUD操作。
下面开始定义支持REST的Action类了,这个Action类与前面介绍Struts2的普通Action存在一些差异——因为该Action不再用execute()方法来处理用户请求,而是使用前面介绍的7个标准方法来处理用户请求。除此之外,该Action总是需要处理id请求参数,因此必须提供id请求参数,并为之提供对应的setter和getter方法。
因为本系统已经提供了BookModel类,并且为了更好的模拟Rails中ActiveController(Controller)直接访问ActiveRecord(Model)的方式,本系统采用了ModelDriven的开发方式,下面是本系统中支持REST的Action类的代码。
//定义返回success时重定向到bookAction
@Results(@Result(name="success"
,type="redirectAction"
,params={"actionName","book"}))
publicclassBookControllerextendsActionSupport
implementsModelDriven<Object>
{
//封装id请求参数的属性
privateintid;
privateBookmodel=newBook();
privateList<Book>list;
//定义业务逻辑组件
privateBookServicebookService=newBookService();
//获取id请求参数的方法
publicvoidsetId(intid)
{
this.id=id;
//取得方法时顺带初始化model对象
if(id>0)
{
this.model=bookService.get(id);
}
}
publicintgetId()
{
returnthis.id;
}
//处理不带id参数的GET请求
//进入首页
publicHttpHeadersindex()
{
list=bookService.getAll();
returnnewDefaultHttpHeaders("index")
.disableCaching();
}
//处理不带id参数的GET请求
//进入添加新图书。
publicStringeditNew()
{
//创建一个新图书
model=newBook();
return"editNew";
}
//处理不带id参数的POST请求
//保存新图书
publicHttpHeaderscreate()
{
//保存图书
bookService.saveOrUpdate(model);
addActionMessage("添加图书成功");
returnnewDefaultHttpHeaders("success")
.setLocationId(model.getId());
}
//处理带id参数的GET请求
//显示指定图书
publicHttpHeadersshow()
{
returnnewDefaultHttpHeaders("show");
}
//处理带id参数、且指定操作edit资源的GET请求
//进入编辑页面(book-edit.jsp)
publicStringedit()
{
return"edit";
}
//处理带id参数的PUT请求
//修改图书
publicStringupdate()
{
bookService.saveOrUpdate(model);
addActionMessage("图书编辑成功!");
return"success";
}
//处理带id参数,且指定操作deleteConfirm资源的方法
//进入删除页面(book-deleteConfirm.jsp)
publicStringdeleteConfirm()
{
return"deleteConfirm";
}
//处理带id参数的DELETE请求
//删除图书
publicStringdestroy()
{
bookService.remove(id);
addActionMessage("成功删除ID为"+id+"的图书!");
return"success";
}
//实现ModelDriven接口必须实现的getModel方法
publicObjectgetModel()
{
return(list!=null?list:model);
}
}
上面Action代码中粗体字代码定义了7个方法,这7个方法正是前面提到的标准方法。除此之外,该Action里还包含一个额外的deleteConfirm()方法,这个方法用于处理带id参数、且指定操作deleteConfirm资源的GET请求。也就是说,当用户请求/book/1/deleteConfirm时,该请求将由该方法负责处理。实际上,RestActionMapper不仅可以将对/book/1/edit的请求映射到Book控制器的edit()方法,而1将作为id请求参数。实际上,它可以将任意/book/1/xxx的请求映射到Book控制器的xxx()方法,而1是请求参数。上面Action类使用了@Results进行修饰,这表明当Action的任何方法返回“success”逻辑视图时,系统将重定向到book.action。
可能有读者会对index()、create()、show()三个方法的返回值感到疑惑:它们不再直接返回普通字符串作为逻辑视图名字,而是返回一个以字符串为参数的DefaultHttpHeaders对象?其实读者不必对DefaultHttpHeaders感到疑惑,其实DefaultHttpHeaders只是普通字符串的加强形式,用于REST对处理结果进行更多额外的控制。当Action类的处理方法返回字符串作为逻辑视图时,Struts2只能将其当成一个简单的视图名,仅能根据该视图名映射到实际视图资源,仅此而已。如果使用DefaultHttpHeaders作为逻辑视图,DefaultHttpHeaders除了可以包含普通字符串作为逻辑视图名之外,还可以额外增加更多的控制数据,从而可以增强对Response的控制。关于HttpHeaders和DefaultHttpHeaders的介绍请参考REST插件的API。
还有一点需要指出,上面的BookController控制器实现类的类名并不以Action结尾,而是以Controller结尾,因此我们可以在struts.xml文件中配置如下常量:
<!--指定控制器类的后缀为Controller-->
<constantname="struts.convention.action.suffix"
value="Controller"/>
本应用里的struts.xml文件如下:
程序清单:codes\12\12.6\BookShow\WEB-INF\src\struts.xml
<?xmlversion="1.0"encoding="GBK"?>
<!--指定Struts2配置文件的DTD信息-->
<!DOCTYPEstrutsPUBLIC
"-//ApacheSoftwareFoundation//DTDStrutsConfiguration2.1//EN"
"http://struts.apache.org/dtds/struts-2.1.dtd">
<!--指定Struts2配置文件的根元素-->
<struts>
<constantname="struts.i18n.encoding"value="GBK"/>
<!--指定控制器类的后缀为Controller-->
<constantname="struts.convention.action.suffix"
value="Controller"/>
<constantname="struts.convention.action.mapAllMatches"
value="true"/>
<!--指定Action所在包继承的父包-->
<constantname="struts.convention.default.parent.package"
value="rest-default"/>
</struts>
实现视图层
定义了上面Action之后,接下来应该为这些Action提供视图页面了,根据Convention插件的约定,所有视图页面都应该放在WEB-INF\content目录下,例如当用户向/book.action发送请求时,该请求将由BookController的index()方法进行处理,该方法处理结束后返回“index”字符串,也就是将会使用WEIN-INF\content\book-index.jsp页面作为视图资源。下面是book-index.jsp页面的代码:
<%@pagecontentType="text/html;charset=GBK"language="java"errorPage=""%>
<%@taglibprefix="s"uri="/struts-tags"%>
<!DOCTYPEhtmlPUBLIC"-//W3C//DTDXHTML1.0Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>图书展示系统</title>
<linkhref="<%=request.getContextPath()%>/css/demo.css"
rel="stylesheet"type="text/css"/>
</head>
<body>
<s:actionmessage/>
<table>
<tr>
<th>图书ID</th>
<th>书名</th>
<th>价格</th>
<th>操作</th>
</tr>
<s:iteratorvalue="model">
<tr>
<td><s:propertyvalue="id"/></td>
<td>${name}</td>
<td>${price}</td>
<td><ahref="book/${id}">查看</a>|
<ahref="book/${id}/edit">编辑</a>|
<ahref="book/${id}/deleteConfirm">删除</a></td>
</tr>
</s:iterator>
</table>
<ahref="<%=request.getContextPath()%>/book/new">创建新图书</a>
</body>
</html>
上面JSP页面非常简单,它负责迭代输出Action里包含的集合数据,向该应用book.action发送请求将看到如图1所示页面。
图1使用Struts2开发的REST服务
Struts2的REST插件支持一种资源具有多少表示形式,当浏览者向book.xml发送请求将可以看到如图2所示页面。
图2REST服务的XML形式
从图2可以看出,该页面正是Action所包含的全部数据,当使用XML显示时REST插件将会负责把这些数据转换成XML文档。
除此之外,REST插件还提供了JSON格式的显示方式,当开发者向book.json发送请求将看到如图3所示页面。
图3REST服务的JSON形式
Struts2的REST插件默认支持XHTML、XML和JSON三种形式的数据。
当浏览者单击页面右边的“编辑”链接,将会向book/idVal/edit发送请求,这是一个包含ID请求参数、且指定操作edit资源的请求,因此将由BookController的edit()方法负责处理,处理结束后进入book-edit.jsp页面。浏览器里将看到如图4所示页面。
图4编辑指定图书
该页面单击“修改”按钮时需要修改图书信息,也就是需要使用PUT操作,但由于HTML不支持PUT操作,因此需要为该表单页增加一个额外的请求参数:_method,该请求参数的值为put。该表单页的代码如下:
<%@pagecontentType="text/html;charset=GBK"language="java"errorPage=""%>
<%@taglibprefix="s"uri="/struts-tags"%>
<!DOCTYPEhtmlPUBLIC"-//W3C//DTDXHTML1.0Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>编辑ID为<s:propertyvalue="id"/>的图书</title>
<linkhref="<%=request.getContextPath()%>/css/demo.css"
rel="stylesheet"type="text/css"/>
</head>
<body>
<s:formmethod="post"
action="%{#request.contextPath}/book/%{id}">
<!--增加_method请求参数,参数值为put用于模拟PUT操作-->
<s:hiddenname="_method"value="put"/>
<table>
<s:textfieldname="id"label="图书ID"disabled="true"/>
<s:textfieldname="name"label="书名"/>
<s:textfieldname="price"label="价格"/>
<tr>
<tdcolspan="2">
<s:submitvalue="修改"/>
</td>
</table>
</s:form>
<ahref="<%=request.getContextPath()%>/book">返回首页</a>
</body>
</html>
该表单将提交给BookController的update()方法处理,update()方法将负责修改系统里指定ID对应的图书信息。
与之类似的是,当请求需要执行 DELETE 操作时,一样需要增加名为 _method 的请求参数,并将该请求参数值设置为 delete。