论坛首页 Java企业应用论坛

关于支持RESTful的思考

浏览 8766 次
该帖已经被评为良好帖
作者 正文
   发表时间:2008-12-04   最后修改:2008-12-08
现在基本上所有的MVC框架都叫喧着支持RESTful(http://zh.wikipedia.org/wiki/REST),
最近写的Struts(for)RCP(http://struts4rcp.googlecode.com)也来凑下热闹,
这里讲下基本思路,作个参考。
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浏览器。当然也可以是任何其他的格式。
下面一一论述以上四点的实现:
1. URI数据映射
鉴于RESTful要从URI上取值,并注入到相应属性,这就需要一种方式,来声明哪一截数据应该注入到哪个属性,
在参考了多个RESTful框架后,决定采用常见的"/catalogs/{categoryId}/books/{bookId}"风格声明URI数据提取方式,如:
@Path("/users/{id}")
public class UserManageAction extends ResourceAction<User> {
	
	// ......

}

2. 方法分派
关于方法的对应关系,有很多做法,如:
(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(http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html)的规定:
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.

要求除POST以外的方法都具有幂等性(idempotence),
也就是调用这些方法n次与调用1次的结果一致,
比如,你删除一个资源8次,和删除这个资源1次没什么区别,
还要求OPTIONS和TRACE方法无副作用(side effects),
也就是不修改任何状态(包括数据库数据与对象属性),
调用这些方法n次与调用0次结果一致,
按照RESTful的要求,GET方法也应该是无副作用的,
这些与契约式设计(Design By Contract)的“区分命令与查询”原则是一致的。
但框架在这方面没法做太多限制,只能在文档中声明并告知业务开发人员。
3. 客户端操作方式
要保证的语义是:客户端看起来是在操作资源表形本身。
(1) 客户端
首先,客户端需要一个代表资源的接口,它的所有函数都是操作该资源本身,如:
/**
 * 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();

(2) AJAX客户端
尽量保持与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();

(3) Web页面
<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>

4. 资源多重表述
也就是客户端,可以通过修改"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语义。
很多细节还没考虑清楚,先写到这了,等正式加入框架,再写个帮助文档。
   发表时间:2008-12-08   最后修改:2008-12-16
经过再三思考,决定使用以下接口形式:

客户端使用:
// 从工厂中获取资源目录引用 (包装代理类,不与服务器交互)
Resources<User> userResources = Client.getClient().getResources("/users");
// 统计资源列表 (发送"HEAD"请求)
long userCount = userResources.count();
long userCount = userResources.count(new User("xxx"));
// 获取资源列表 (发送"GET"请求)
Resource<User>[] users = userResources.list();
Resource<User>[] users = userResources.list(new User("xxx")); // 可以使用User的子类作为过滤条件
Resource<User> userResource = users[0];
// 创建资源 (发送"POST"请求)
Resource<User> userResource = userResources.create(new User("liangfei", "xxx@xxx.com"));
// 从工厂中获取资源引用 (包装代理类,不与服务器交互)
Resource<User> userResource = Client.getClient().getResource("/users/{0}", id); // 不定长参数,替换URI中的占位符   
// 读取资源 (发送"GET"请求)
User user = userResource.read();
// 更新资源 (发送"PUT"请求)
userResource.update(user);
// 删除资源 (发送"DELETE"请求)
userResource.delete();


服务器端处理:
@Path("/users//{id}") // 双斜杠用于分隔集合资源URI和单一资源URI
public class UserResourceAction extends ResourceAction<User> {

	// 统计资源列表 (接收集合资源的"HEAD"请求)
	protected long count(User user) throws Exception {
		if (user == null)
			return userService.countAllUser();
		return userService.countByUser(user);
	}

	// 获取资源列表 (接收集合资源的"GET"请求)
	protected User[] list(User user) throws Exception {
		if (user == null)
			return userService.findAllUser().toArray(new User[0]);
		return userService.findByUser(user).toArray(new User[0]);
	}

	// 创建资源 (接收集合资源的"POST"请求)
	protected void create(User user) throws Exception {
		userService.saveUser(user);
	}

	// 读取资源 (接收单一资源的"GET"请求)
	protected User read(User user) throws Exception {
		return userService.loadUser(user.getId());
	}

	// 修改资源 (接收单一资源的"PUT"请求)
	protected void update(User user) throws Exception {
		userService.updateUser(user);
	}

	// 删除资源 (接收单一资源的"DELETE"请求)
	protected void delete(User user) throws Exception {
		userService.removeUser(user.getId());
	}

}
0 请登录后投票
   发表时间:2008-12-18  
我个人认为用 标注 和 配置文件 灵活性更大些,  比如: get()、find()、read()等都可以标注成@Get, 感觉这样隔离了实现!
0 请登录后投票
   发表时间:2008-12-18   最后修改:2008-12-18
Beyon_javaeye 写道

我个人认为用 标注 和 配置文件 灵活性更大些,&nbsp; 比如: get()、find()、read()等都可以标注成@Get, 感觉这样隔离了实现!

是的,用标注是个好想法,JSR311就是全部用标注实现的,但我觉得REST与RPC的一个重要区别就在于它统一了“动词”,并以“名词”为中心,要的就是不“灵活”(零风格是最灵活的),虽然会对某些特殊功能实现带来麻烦,但对常见功能非常简化,而且我想实现的是MVC框架,而不是REST版的WebService(这是JSR311的目标),将REST作为控制器层实现,所以固定函数名,会比较合理些,并且可以保留对"Action"概念的支持,以处理特殊情况。
1 请登录后投票
   发表时间:2009-01-14  
目前HP的Symphony SDK和社区的Restlet和Jersey还算是比较好用,Struts和Axis2所宣称的Restful WS都很糟糕.
另外,我觉得Atom和JSON已经是Restful WS的既定事实的必须支持的两种representation了

引用
如:@Path({"/users/{id}","/users","/users/add","/users/{id}/edit"})
当然,可以用命名约定来减少路径的个数,问题在于add和edit已经超出(甚至违背了)RESTful风格,


如果是自增主键的话,add可以POST到resources上啊,这是Restful WS推荐的方式也是Atom协议要求的. Edit可以put到resources/{id}上



0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics