利用Struts 2开拓RESTful处事
副标题#e#
REST 简介
REST是英文Representational State Transfer的缩写,这个术语由Roy Thomas Fielding博士在他的论文《Architectural Styles and the Design of Network-based Software Architectures》中提出。从这篇论文的标题可以看出:REST是一种基于网络的软件架构气势气魄。
提示:海内许多网络资料将REST翻译为“表述性状态转移”,不外笔者对这个翻译不太认同。因为这个专业术语无法转达REST的寄义,读者可以先不要剖析REST到底该如何翻译,只管先去领略REST是什么?有什么用?然后再来看这个术语的翻译。关于Roy Thomas Fielding博士的原文拜见如下地点:http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm。
REST 架构是针对传统Web应用提出的一种改造,是一种新型的漫衍式软件设计架构。对付异构系统如何举办整合的问题,今朝主流做法都会合在利用 SOAP、WSDL 和 WS-* 类型的Web Services。而REST架构实际上也是办理异构系统整合问题的一种新思路。
假如开拓者在开拓进程中能僵持REST原则,将可以获得一个利用了优质Web架构的系统,从而为系统提供更好的可伸缩性,并低落开举事度。关于REST架构的主要原则如下:
网络上的所有事物都可被抽象为资源(Resource)。
每个资源都有一个独一的资源标识符(Resource Identifier)。
同一资源具有多种表示形式。
利用尺度要领操纵资源。
通过缓存来提高机能。
对资源的各类操纵不会改变资源标识符。
所有的操纵都是无状态的(Stateless)。
仅从上面几条原则来看REST架构,其实依然较量难以领略,下面笔者将从如下二个方面来先容REST。
资源和标识符
此刻的Web应用上包括了大量信息,但这些信息都被埋没在 HTML、CSS 和 JavaScript 代码中,对付普通欣赏者而言,他们进入这个系统时无法知道该系统包括哪些页面;对付一个需要会见该系统资源的第三方系统而言,同样无法大白这个系统包括几多成果和信息。
URI 和 URL
与 URI 相关的观念尚有 URL,URL 是 Uniform Resource Locator,也就是统一资源定位符的意思。个中 http://www.crazyit.org 就是一个统一资源定位符,URL 是 URI 的子集。简而言之:每个 URL 都是 URI,但不是每个 URI 都是 URL。
从REST架构的角度来看,该系统里包括的所有成果和信息,都可被称为资源(Resource),REST 架构中的资源包括静态页面、JSP 和 Servlet 等,该应用袒露在网络上的所有成果和信息都可被称为资源。
除此之外,REST 架构类型了应用资源的定名方法,REST 划定对应用资源利用统一的定名方法:REST 系统中的资源必需统一定名和筹划,REST 系统由利用 URI(Uniform Resource Identifier,即统一资源标识符)定名的资源构成。由于REST对资源利用了基于 URI 的统一定名,因此这些信息就自然地袒暴露来了,从而制止 “信息地窖”的不良效果。
对付当今最常见的网络应用来说,资源标识符就是 URI,资源的利用者则按照 URI 来操纵应用资源。当 URI 产生改变时,表白客户机所利用的资源产生了改变。
从资源的角度来看,当客户机操纵差异的资源时,资源地址的Web页(将Web页当成虚拟的状态机来看)的状态就会产生改变、迁移(Transfer),这就是REST术语中 ST(State Tranfer)的由来了。
客户机为了操纵差异状态的资源,则需要发送一些 Representational 的数据,这些数据包括须要的交互数据,以及描写这些数据的元数据。这就是REST术语中 RE(Representational)的由来了。领略了这个条理之后,至于REST如何翻译、或是否真正给它一其中文术语,读者可自行抉择。
#p#副标题#e#
操纵资源的方法
对付REST架构的处事器端而言,它提供的是资源,但同一资源具有多种表示形式(可通过在 HTTP Content-type 头中包括关于数据范例的元数据)。假如客户措施完全支持 HTTP 应用协议,并能正确处理惩罚REST架构的尺度数据名目,那么它就可以与世界上任意一个REST气势气魄的用户交互。
上面的环境不只合用于从处事器端到客户端的数据,反之亦然——倘若从客户端传来的数据切合REST架构的尺度数据名目,那么处事器端就可以正确处理惩罚数据,而不去体贴客户端的范例。
典范环境下,REST 气势气魄的资源能以 XHTML、XML 和 JSON 三种形式存在,个中 XML 名目标数据是 WebServices 技能的数据互换名目,而 JSON 则是另一种轻量级的数据互换名目;至于 XHTML 名目则主要由欣赏器认真泛起。
当处事器为所有资源提供多种表示形式之后,这些资源不只可以被尺度Web欣赏器所利用,还可以由 JavaScript 通过 Ajax 技能挪用,可能以 RPC(Remote Procedure Call)气势气魄挪用,从而酿成REST气势气魄的 WebServices。
#p#分页标题#e#
REST 架构除了划定处事器提供资源的方法之外,还推荐客户端利用 HTTP 作为 Generic Connector Interface(也就是通用毗连器接口),而 HTTP 则把对一个 URI 的操纵限制在了 4 个之内:GET、POST、PUT 和 DELETE。通过利用通用毗连器接口对资源举办操纵的长处是担保系统提供的处事都是高度解耦的,从而简化了系统开拓,改进了系统的交互性和可重用性。
REST 架构要求客户端的所有的操纵在本质上是无状态的,即从客户随处事器的每个 Request 都必需包括领略该 Request 的所有必须信息。这种无状态性的类型提供了如下几点长处:
无状态性使得客户端和处事器端不必生存对方的具体信息,处事器只需要处理惩罚当前 Request,而不必相识前面 Request 的汗青。
无状态性淘汰了处事器从局部错误中规复的任务量,可以很是利便地实现 Fail Over 技能,从而很容易地将处事器组件陈设在集群内。
无状态性使得处事器端不必在多个 Request 中生存状态,从而可以更容易地释放资源。
无状态性无需处事组件生存 Request 状态,因此可让处事器充实操作 Pool 技能来提高不变性和机能。
虽然,无状态性会使得处事器不再生存 Request 的状态数据,因此需要在一系列 Request 中发送反复数据的,从而提高了系统的通信本钱。为了改进无状态性带来的机能下降,REST 架构填加了缓存约束。缓存约束答允隐式或显式地标志一个 Response 中的数据,这样就赋予了客户端缓存 Response 数据的成果,这样就可觉得今后的 Request 共用缓存的数据,部门或全部的消除一些交互,增加了网络的效率。可是用于客户端缓存了信息,也就同时增加了客户端与处事器数据纷歧致的大概,从而低落了靠得住性。
Struts 2 的REST支持
约定优于设置
Convention 这个单词的翻译过来就是“约定”的意思。有 Ruby On Rails 开拓履历的读者知道 Rails 有一条重要原则:约定优于设置。Rails 开拓者只需要按约定开拓 ActiveRecord、ActiveController 即可,无需举办设置。很明明,Struts 2 的 Convention 插件警惕了 Rails 的创意,甚至连插件的名称都警惕了“约定优于设置”原则。
从Struts 2.1 开始,Struts 2 改为利用 Convention
RestActionMapper 简介
从本质上来看,Struts 2 依然是一个 MVC 框架,最初设计Struts 2时并没有按REST架构举办设计,因此Struts 2本质上并不是一个REST框架。
由于Struts 2提供了精采的可扩展性,因此答允通过REST插件将其扩展成支持REST的框架。REST 插件的焦点是 RestActionMapper,它认真将 Rails 气势气魄的 URL 转换为传统请求的 URL。
用 WinRAR 打开 struts2-rest-plugin-2.1.6 文件,看到该文件里包括一个 struts-plugin.xml 文件,该文件中包括如下一行:
<!– 界说支持REST的 ActionMapper –>
<bean type="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 中即可。表 12.3 显示了 RestActionMapper 对差异 HTTP 请求的处理惩罚功效。
表 12.3 RestActionMapper 对 HTTP 请求的处理惩罚
#p#分页标题#e#
HTTP 要领 | URI | 挪用 Action 的要领 | 请求参数 |
GET | /book | index | |
POST | /book | create | |
PUT | /book/2 | update | id=2 |
DELETE | /book/2 | destroy | id=2 |
GET | /book/2 | show | id=2 |
GET | /book/2/edit | edit | id=2 |
GET | /book/new | editNew |
不幸地是,尺度 HTML 语言今朝基础不支持 PUT 和 DELETE 两个操纵,为了补充这种不敷,REST 插件答允开拓者提交请求时特别增加一个 _method 请求参数,该参数值可觉得 PUT 或 DELETE,用于模仿 HTTP 协议的 PUT 和 DELETE 操纵。
为Struts 2应用安装REST插件
安装REST插件很是简朴,只需按如下步调举办即可:
(1)将Struts 2项目下 struts2-convention-plugin-2.1.6.jar、struts2-rest-plugin-2.1.6.jar 两个 JAR 包复制到Web应用的Web-INF\lib 路径下。
(2)由于Struts 2的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 路径下。
(3)通过 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 类很是简朴,代码如下:
public class Book
{
private Integer id;
private String name;
private double price;
// 无参数的结构器
public Book(){}
//id 属性的 setter 和 getter 要领
public void setId(Integer id)
{
this.id = id;
}
public Integer getId()
{
return this.id;
}
// 省略 name 和 price 的 setter 和 getter 要领
...
}
除了提供上面的Book类之外,我们还为该 Book 类提供一个业务逻辑组件:BookService。为了简朴起见,BookService 类不再依赖 DAO 组件会见数据库,而是直接操纵内存中的 Book 数组——简朴地说,本系统中状态是瞬态的,没有耐久化生存,应用运行进程中这些状态一直存在,但一旦重启该应用,则系统状态丢失。下面是 BookService 类的代码:
public class BookService
{
private static Map<Integer , Book> books
= new HashMap<Integer , Book>();
// 保存下本图书的 ID
private static int nextId = 5;
// 以内存中的数据模仿数据库的耐久存储
static {
books.put(1 , new Book(1
, "猖獗 Java 教材" , 99));
books.put(2 , new Book(2
, "轻量级 Java EE 企业应用实战" , 89));
books.put(3 , new Book(3
, "猖獗 Ajax 教材", 78));
books.put(4 , new Book(4
, "Struts 2 权威指南" , 79));
}
// 按照 ID 获取
public Book get(int id)
{
return books.get(id);
}
// 获取系统中全部图书
public List<Book> getAll()
{
return new ArrayList<Book>(books.values());
}
// 更新已有的图书或生存新图书
public void saveOrUpdate(Book book)
{
// 假如试图生存的图书的 ID 为 null,表白是生存新的图书
if (book.getId() == null)
{
// 为新的图书分派 ID。
book.setId(nextId++);
}
// 将生存 book
books.put(book.getId() , book);
}
// 删除图书
public void remove(int id)
{
books.remove(id);
}
}
从上面粗体字代码可以看出,BookService 提供了 4 个要领,用于实现对 Book 工具的 CRUD 操纵。
#p#分页标题#e#
下面开始界说支持REST的 Action 类了,这个 Action 类与前面先容Struts 2的普通 Action 存在一些差别——因为该 Action 不再用 execute() 要领来处理惩罚用户请求,而是利用前面先容的 7 个尺度要领来处理惩罚用户请求。除此之外,该 Action 老是需要处理惩罚 id 请求参数,因此必需提供 id 请求参数,并为之提供对应的 setter 和 getter 要领。
因为本系统已经提供了 Book Model 类,而且为了更好的模仿 Rails 中 ActiveController(Controller)直接会见 ActiveRecord(Model)的方法,本系统回收了 ModelDriven 的开拓方法,下面是本系统中支持REST的 Action 类的代码。
// 界说返回 success 时重定向到 book Action
@Results(@Result(name="success"
, type="redirectAction"
, params = {"actionName" , "book"}))
public class BookController extends ActionSupport
implements ModelDriven<Object>
{
// 封装 id 请求参数的属性
private int id;
private Book model = new Book();
private List<Book> list;
// 界说业务逻辑组件
private BookService bookService = new BookService();
// 获取 id 请求参数的要领
public void setId(int id)
{
this.id = id;
// 取得要领时顺带初始化 model 工具
if (id > 0)
{
this.model = bookService.get(id);
}
}
public int getId()
{
return this.id;
}
// 处理惩罚不带 id 参数的 GET 请求
// 进入首页
public HttpHeaders index()
{
list = bookService.getAll();
return new DefaultHttpHeaders("index")
.disableCaching();
}
// 处理惩罚不带 id 参数的 GET 请求
// 进入添加新图书。
public String editNew()
{
// 建设一个新图书
model = new Book();
return "editNew";
}
// 处理惩罚不带 id 参数的 POST 请求
// 生存新图书
public HttpHeaders create()
{
// 生存图书
bookService.saveOrUpdate(model);
addActionMessage("添加图书乐成");
return new DefaultHttpHeaders("success")
.setLocationId(model.getId());
}
// 处理惩罚带 id 参数的 GET 请求
// 显示指定图书
public HttpHeaders show()
{
return new DefaultHttpHeaders("show");
}
// 处理惩罚带 id 参数、且指定操纵 edit 资源的 GET 请求
// 进入编辑页面 (book-edit.jsp)
public String edit()
{
return "edit";
}
// 处理惩罚带 id 参数的 PUT 请求
// 修改图书
public String update()
{
bookService.saveOrUpdate(model);
addActionMessage("图书编辑乐成!");
return "success";
}
// 处理惩罚带 id 参数,且指定操纵 deleteConfirm 资源的要领
// 进入删除页面 (book-deleteConfirm.jsp)
public String deleteConfirm()
{
return "deleteConfirm";
}
// 处理惩罚带 id 参数的 DELETE 请求
// 删除图书
public String destroy()
{
bookService.remove(id);
addActionMessage("乐成删除 ID 为" + id + "的图书!");
return "success";
}
// 实现 ModelDriven 接口必需实现的 getModel 要领
public Object getModel()
{
return (list != null ? list : model);
}
}
#p#分页标题#e#
上面 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 类的处理惩罚要领返回字符串作为逻辑视图时,Struts 2 只能将其当成一个简朴的视图名,仅能按照该视图名映射到实际视图资源,仅此罢了。假如利用 DefaultHttpHeaders 作为逻辑视图,DefaultHttpHeaders 除了可以包括普通字符串作为逻辑视图名之外,还可以特别增加更多的节制数据,从而可以加强对 Response 的节制。关于 HttpHeaders 和 DefaultHttpHeaders 的先容请参考REST插件的 API。
尚有一点需要指出,上面的 BookController 节制器实现类的类名并不以 Action 末了,而是以 Controller 末了,因此我们可以在 struts.xml 文件中设置如下常量:
<!-- 指定节制器类的后缀为 Controller -->
<constant name="struts.convention.action.suffix"
value="Controller"/>
本应用里的 struts.xml 文件如下:
措施清单:codes\12\12.6\BookShow\WEB-INF\src\struts.xml
<?xml version="1.0" encoding="GBK" ?>
<!-- 指定 Struts 2 设置文件的 DTD 信息 -->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.1//EN"
"http://struts.apache.org/dtds/struts-2.1.dtd">
<!-- 指定 Struts 2 设置文件的根元素 -->
<struts>
<constant name="struts.i18n.encoding" value="GBK"/>
<!-- 指定节制器类的后缀为 Controller -->
<constant name="struts.convention.action.suffix"
value="Controller"/>
<constant name="struts.convention.action.mapAllMatches"
value="true"/>
<!-- 指定 Action 地址包担任的父包 -->
<constant name="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 页面的代码:
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title> 图书展示系统 </title>
<link href="<%=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:iterator value="model">
<tr>
<td><s:property value="id"/></td>
<td>${name}</td>
<td>${price}</td>
<td><a href="book/${id}"> 查察 </a> |
<a href="book/${id}/edit"> 编辑 </a> |
<a href="book/${id}/deleteConfirm"> 删除 </a></td>
</tr>
</s:iterator>
</table>
<a href="<%=request.getContextPath() %>/book/new"> 建设新图书 </a>
</body>
</html>
上面 JSP 页面很是简朴,它认真迭代输出 Action 里包括的荟萃数据,向该应用 book.action 发送请求将看到如图 1 所示页面。
图 1 利用Struts 2开拓的REST处事
Struts 2 的REST插件支持一种资源具有几多暗示形式,当欣赏者向 book.xml 发送请求将可以看到如图 2 所示页面。
图 2REST处事的 XML 形式
从图 2 可以看出,该页面正是 Action 所包括的全部数据,当利用 XML 显示时REST插件将会认真把这些数据转换成 XML 文档。
除此之外,REST 插件还提供了 JSON 名目标显示方法,当开拓者向 book.json 发送请求将看到如图 3 所示页面。
图 3REST处事的 JSON 形式
Struts 2 的REST插件默认支持 XHTML、XML 和 JSON 三种形式的数据。
#p#分页标题#e#
当欣赏者单击页面右边的“编辑”链接,将会向 book/idVal/edit 发送请求,这是一个包括 ID 请求参数、且指定操纵 edit 资源的请求,因此将由 BookController 的 edit() 要领认真处理惩罚,处理惩罚竣事后进入 book-edit.jsp 页面。欣赏器里将看到如图 4 所示页面。
图 4 编辑指定图书
该页面单击“修改”按钮时需要修改图书信息,也就是需要利用 PUT 操纵,但由于 HTML 不支持 PUT 操纵,因此需要为该表单页增加一个特另外请求参数:_method,该请求参数的值为 put。该表单页的代码如下:
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title> 编辑 ID 为 <s:property value="id"/> 的图书 </title>
<link href="<%=request.getContextPath() %>/css/demo.css"
rel="stylesheet" type="text/css" />
</head>
<body>
<s:form method="post"
action="%{#request.contextPath}/book/%{id}">
<!-- 增加 _method 请求参数,参数值为 put 用于模仿 PUT 操纵 -->
<s:hidden name="_method" value="put" />
<table>
<s:textfield name="id" label="图书 ID" disabled="true"/>
<s:textfield name="name" label="书名"/>
<s:textfield name="price" label="价值" />
<tr>
<td colspan="2">
<s:submit value="修改"/>
</td>
</table>
</s:form>
<a href="<%=request.getContextPath() %>/book"> 返回顾页 </a>
</body>
</html>
该表单将提交给 BookController 的 update() 要领处理惩罚,update() 要领将认真修改系统里指定 ID 对应的图书信息。
与之雷同的是,当请求需要执行 DELETE 操纵时,一样需要增加名为 _method 的请求参数,并将该请求参数值配置为 delete。