关于支持RESTful的思考

现在基本上所有的MVC框架都叫喧着支持RESTful([url]http://zh.wikipedia.org/wiki/REST[/url]),
最近写的Struts(for)RCP([url]http://struts4rcp.googlecode.com[/url])也来凑下热闹,
这里讲下基本思路,作个参考。
REST的一些要求,如:
1. 客户端和服务器结构
2. 连接协议具有无状态性
3. 能够利用Cache机制增进性能
4. 层次化的系统
5. Code On Demand - Javascript
通过RCP/RIA和HTTP协议本身就可以达到,就不多说了,
主要关注REST的设计风格,如:
1. 资源是由URI来指定。
2. 对资源的操作包括获取、创建、修改和删除资源,这些操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法。
3. 通过操作资源的表形来操作资源。
4. 资源的表现形式则是XML或者HTML,取决于是读者是机器还是人,消费web服务的客户软件还是web浏览器。当然也可以是任何其他的格式。
下面一一论述以上四点的实现:
[b]1. URI数据映射[/b]
鉴于RESTful要从URI上取值,并注入到相应属性,这就需要一种方式,来声明哪一截数据应该注入到哪个属性,
在参考了多个RESTful框架后,决定采用常见的"/catalogs/{categoryId}/books/{bookId}"风格声明URI数据提取方式,如:

@Path("/users/{id}")
public class UserManageAction extends ResourceAction<User> {

// ......

}

[b]2. 方法分派[/b]
关于方法的对应关系,有很多做法,如:
(1) 函数名与请求类型保持一致,如:

@Path("/users/{id}")
public class UserManageAction extends ResourceAction<User> {

@Override
public void post(User user) throws Exception {
// 除了ID以外的值,通过表单传入,表单中有ID值无效(会被覆盖掉)
userService.create(user);
}

@Override
public void put(User user) throws Exception {
// 除了ID以外的值,通过表单传入,表单中有ID值无效(会被覆盖掉)
userService.update(user);
}

@Override
public void delete(User user) throws Exception {
userService.delete(user.getId());
}

@Override
public User get(User user) throws Exception {
return userService.get(user.getId());
}

}

但感觉这样,函数名对业务开发人员不友好。
(2) 取一个更直观的名称与之对应,如:

@Path("/users/{id}")
public class UserManageAction extends ResourceAction<User> {

@Override
public void create(User user) throws Exception {
// 除了ID以外的值,通过表单传入,表单中有ID值无效(会被覆盖掉)
userService.create(user);
}

@Override
public void update(User user) throws Exception {
// 除了ID以外的值,通过表单传入,表单中有ID值无效(会被覆盖掉)
userService.update(user);
}

@Override
public void delete(User user) throws Exception {
userService.delete(user.getId());
}

@Override
public User get(User user) throws Exception {
return userService.get(user.getId());
}

}

上面四个方法分别对应POST,PUT,DELETE,GET四个HTTP请求方法。
(3) 不限制名称,用标注在函数上进行标识,如:

@Path("/users/{id}")
public class UserManageAction extends ResourceAction<User> {

@Post
public void createUser(User user) throws Exception {
// 除了ID以外的值,通过表单传入,表单中有ID值无效(会被覆盖掉)
userService.create(user);
}

@Put
public void updateUser(User user) throws Exception {
// 除了ID以外的值,通过表单传入,表单中有ID值无效(会被覆盖掉)
userService.update(user);
}

@Delete
public void deleteUser(User user) throws Exception {
userService.delete(user.getId());
}

@Get
public User getUser(User user) throws Exception {
return userService.get(user.getId());
}

}

(4) 配置文件声明,如:

@Path("/users/{id}")
public class UserManageAction extends ResourceAction<User> {

public void createUser(User user) throws Exception {
// 除了ID以外的值,通过表单传入,表单中有ID值无效(会被覆盖掉)
userService.create(user);
}

public void updateUser(User user) throws Exception {
// 除了ID以外的值,通过表单传入,表单中有ID值无效(会被覆盖掉)
userService.update(user);
}

public void deleteUser(User user) throws Exception {
userService.delete(user.getId());
}

public User getUser(User user) throws Exception {
return userService.get(user.getId());
}

}


<bean id="userManageAction" class="com.xxx.UserManageAction">
<property name="postMethodName" value="createUser">
<property name="putMethodName" value="updateUser">
<property name="deleteMethodName" value="deleteUser">
<property name="getMethodName" value="getUser">
</bean>

最终觉得第2种方案比较简单友好, 并且客户端便于创建对等接口, 标注和配置也可以考虑支持。
还有一个问题是,ResourceAction应不应该提供list, find等函数,用于列表和查找,如:

@Path("/users/{id}")
public class UserManageAction extends ResourceAction<User> {

// create(), update(), delete(), get()

@Override
public Collection<User> list() throws Exception {
return userService.findAll();
}

@Override
public Collection<User> find(User user) throws Exception {
return userService.find(user);
}
}

这些已经不是同一个资源了,而是同一类型的资源,如果分开应该是:

@Path("/users")
public class UserListAction extends ResourceAction<Collection<User>> {

@Override
public Collection<User> get(User user) throws Exception {
return userService.find(user); // 如果条件为空即为全部
}

}

如果定义在一起,@Path就需要声明多个资源路径,
如:@Path({"/users/{id}", "/users"})
另一个相似的问题是,ResourceAction应不应该提供add, edit方法,用于跳转到新增页面和编辑页面,如:

@Path("/users/{id}")
public class UserManageAction extends ResourceAction<User> {

// create(), update(), delete(), get()

@Override
public User add() throws Exception {
return new User();
}

@Override
public User edit(User user) throws Exception {
return userService.get(user.getId());
}
}

如:@Path({"/users/{id}","/users","/users/add","/users/{id}/edit"})
当然,可以用命名约定来减少路径的个数,问题在于add和edit已经超出(甚至违背了)RESTful风格,
因为RESTful要求客户端Code On Demand,要求在客户端缓存和管理状态,
像新增页面应该在客户端直接处理,而编辑页面实际上是调用get方法取值填充,只是非RIA的Web应用需要这种间接页面,
如果用RESTful风格第4点“资源多重表述”来解决,可能更合理,
edit只是资源"/users/{id}"的一种GET展示形式,也就是客户端设置"Accept"信息头为"edit",
但这样存在一个问题,浏览器不能友好的输入请求头信息。
另外,add是否需要作为特殊值,以避免冲突,因为可能某个用户的ID就是add,当然,如果ID都是数字就没问题。
再者,根据RFC2616([url]http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html[/url])的规定:
[quote="RFC2616"]
9.1.2 Idempotent Methods
Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request. The methods GET, HEAD, PUT and DELETE share this property. Also, the methods OPTIONS and TRACE SHOULD NOT have side effects, and so are inherently idempotent.
[/quote]
要求除POST以外的方法都具有幂等性(idempotence),
也就是调用这些方法n次与调用1次的结果一致,
比如,你删除一个资源8次,和删除这个资源1次没什么区别,
还要求OPTIONS和TRACE方法无副作用(side effects),
也就是不修改任何状态(包括数据库数据与对象属性),
调用这些方法n次与调用0次结果一致,
按照RESTful的要求,GET方法也应该是无副作用的,
这些与契约式设计(Design By Contract)的“区分命令与查询”原则是一致的。
但框架在这方面没法做太多限制,只能在文档中声明并告知业务开发人员。
[b]3. 客户端操作方式[/b]
要保证的语义是:客户端看起来是在操作资源表形本身。
[b](1) 客户端[/b]
首先,客户端需要一个代表资源的接口,它的所有函数都是操作该资源本身,如:

/**
* RESTful远程资源接口
* @param <R> 资源类型
*/
public interface Resource<R extends Serializable> {

/**
* 创建资源
* @param resource 资源信息(注:标识性属性值(如:ID值)无效,在服务器端接收时,将被替换为资源URI所指定的值)
* @throws Exception 创建失败或网络连接出错时抛出
*/
void create(R resource) throws Exception;

/**
* 更新资源
* @param resource 资源信息(注:标识性属性值(如:ID值)无效,在服务器端接收时,将被替换为资源URI所指定的值)
* @throws Exception 更新失败或网络连接出错时抛出
*/
void update(R resource) throws Exception;

/**
* 删除资源
* @throws Exception 删除失败或网络连接出错时抛出
*/
void delete() throws Exception;

/**
* 获取资源
* @return 资源
* @throws Exception 获取失败或网络连接出错时抛出
*/
R get() throws Exception;

}

然后,通过工厂获取此接口,如:

Resource<User> userResource = Resources.getResource("/users/4271"); // URI带上ID
// 或者:Resources.getResource("/users/{0}", id); // 不定长参数,替换URI中的占位符
User user = new User("liangfei", "xxx@xxx.com");
userResource.create(user);
user = userResource.get();
userResource.update(user);
userResource.delete();

另外,可以考虑增加一个批量资源接口,如:

/**
* 批量资源接口
* @param <R> 资源类型
*/
public interface BatchResource<R extends Serializable> extends Resource<R[]> {

// 将从父接口中继承:
// void create(R[] resources) throws Exception; // 批量创建
// void update(R[] resources) throws Exception; // 批量更新
// void delete() throws Exception; // 删除全部资源
// R[] get() throws Exception; // 获取全部资源

/**
* 删除匹配的资源
* @param resource 匹配条件(如果条件复杂,可以传入资源类型的子类作为条件)
* @throws Exception 删除失败或网络连接出错时抛出
*/
void delete(R resource) throws Exception;

/**
* 获取匹配的资源
* @param resource 匹配条件(如果条件复杂,可以传入资源类型的子类作为条件)
* @return 资源
* @throws Exception 获取失败或网络连接出错时抛出
*/
R[] get(R resource) throws Exception;

}

同样,可以使用工厂获取此接口,如:

BatchResource<User> userBatchResource = Resources.getBatchResource("/users"); // URI不带ID
User[] users = userBatchResource.get();

[b](2) AJAX客户端[/b]
尽量保持与RCP客户端一致,如:

var userResource = Resources.getResource("/users/4271");
var user = {name:"liangfei", emial: "xxx@xxx.com"};
userResource.create(user);
user = userResource.get();
userResource.update(user);
userResource.delete();

[b](3) Web页面[/b]

<a href="/users/1">view</a>
<form action="/users/1" method="post" accept="text/html">
<input type="text" name="name" />
<input type="text" name="email" />
<input type="submit" value="create"/>
</form>

[b]4. 资源多重表述[/b]
也就是客户端,可以通过修改"Accept"请求头信息,来要求服务器端返回不同类型的数据结果,如:
Accept: text/json 返回JSON数据
Accept: text/xml 返回XML数据
Accept: text/html 返回HTML页面
Accept: text/wml 返回WML页面
这个可以通过读取根据请求头信息来切换序列化器实现,首先需要留有策略接口,如:

/**
* Action接收映射接口
*/
public interface ActionMapper {

/**
* 获取序列化器
* @param request 请求信息
* @return 序列化器
*/
Serializer getSerializer(HttpServletRequest request);

// 当然,此接口可能还有相关的其它映射函数,如:getActionName(request)等

}

然后写一个Restful的实现,如:

public class RestfulActionMapper implements ActionMapper {

private final Map<String, Serializer> serializers = new HashMap<String, Serializer>();

public RestfulActionMapper() {
// 搜索或注册序列化器...
serializers.put("text/json", new JsonSerializer());
serializers.put("text/xml", new XmlSerializer());
serializers.put("text/html", new JspHtmlSerializer());
serializers.put("text/wml", new JspWmlSerializer());
// serializers.put("text/html", new VelocityHtmlSerializer());
// serializers.put("text/wml", new VelocityWmlSerializer());
}

@Override
public Serializer getSerializer(HttpServletRequest request) {
String type = request.getHeader("Accept");
return serializers.get(type);
}

}

上面提到过结果类型放在"Accept"信息头中有个问题,就是不便输入,对于B/S应用的浏览器来说,不太友好,所以Struts2等通过URL扩展名来识别,如:"/user/1.html","/user/1.wml",这样做超接会方便些,但却违反了RESTful语义。
很多细节还没考虑清楚,先写到这了,等正式加入框架,再写个帮助文档。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值