`
javatar
  • 浏览: 1704606 次
  • 性别: Icon_minigender_1
  • 来自: 杭州699号
社区版块
存档分类
最新评论

关于支持RESTful的思考

阅读更多
现在基本上所有的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语义。
很多细节还没考虑清楚,先写到这了,等正式加入框架,再写个帮助文档。
分享到:
评论
5 楼 DanielYWoo 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}上



4 楼 Jekey 2008-12-19  
标记一下,最近比较忙,有空研究一下。
3 楼 javatar 2008-12-18  
Beyon_javaeye 写道

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

是的,用标注是个好想法,JSR311就是全部用标注实现的,但我觉得REST与RPC的一个重要区别就在于它统一了“动词”,并以“名词”为中心,要的就是不“灵活”(零风格是最灵活的),虽然会对某些特殊功能实现带来麻烦,但对常见功能非常简化,而且我想实现的是MVC框架,而不是REST版的WebService(这是JSR311的目标),将REST作为控制器层实现,所以固定函数名,会比较合理些,并且可以保留对"Action"概念的支持,以处理特殊情况。
2 楼 Beyon_javaeye 2008-12-18  
我个人认为用 标注 和 配置文件 灵活性更大些,  比如: get()、find()、read()等都可以标注成@Get, 感觉这样隔离了实现!
1 楼 javatar 2008-12-08  
经过再三思考,决定使用以下接口形式:

客户端使用:
// 从工厂中获取资源目录引用 (包装代理类,不与服务器交互)
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());
	}

}

相关推荐

    RESTful Web Service 课件下载.pdf

    RESTful Web服务不仅是一种架构模式,更是一种思考分布式应用和服务的方式。它通过简单、标准的HTTP协议实现了资源的管理和交互,从而提高了应用的灵活性和可扩展性。随着技术的发展,REST已成为构建现代Web应用和...

    Python 图像相似度匹配 + Restful API,本科毕业设计程序.zip

    导师指导: 学生在毕业设计过程中通常由一名指导老师或导师团队提供指导和支持。导师负责引导学生确定研究方向、制定计划、提供建议,并在整个过程中监督进展。 学术规范: 毕业设计要求学生按照学术规范完成研究,...

    Java编程语言项目实战案例详解:控制台程序到在线教育平台

    内容概要:本文详细介绍了三个不同复杂度级别的Java项目,包括简单的基于API调用的控制台天气预报、中等复杂度采用Swing构建的图书管理系统,及其高级别的利用Spring Boot搭建支持RESTful接口的在线教育平台。...

    微服务的实践与思考-左文建

    在技术工具方面,文章中介绍了几个相关的开源项目,这些项目能够支持微服务架构的实施,包括rigger-ng(多环境与资源管理工具),pylon-ng(支持RESTful的PHP框架)以及SSDB(可持久化的NoSQL数据库,用于替代redis...

    设计思考07031

    设计思考07031主要涉及的是在IT行业中,特别是针对数据中心管理和三维展示的一个系统设计,其中重点讨论了如何构建一个Restful API接口以支持系统的功能需求。Restful API是一种广泛采用的Web服务设计风格,它强调...

    业务安全应急响应新思考-韩晋.pdf

    对象存储服务通常通过RESTful API进行访问,支持水平扩展,可以有效应对大数据存储需求。 综上所述,业务安全应急响应是一个涉及广泛领域的课题,它不仅需要紧密跟进最新的安全技术,还需要结合企业的具体业务场景...

    Graphql构建Api网关思考

    不过,传统上用Nginx作为API网关存在一些劣势,比如它主要是一个Web服务器和反向代理,对于安全认证、IP限制、限流、路由转发和日志记录等功能虽然支持但不够灵活,且难以实现复杂的业务逻辑。 为了适应不同的设备...

    API 设计思考_python_

    Python的Flask和Django框架都支持构建RESTful API。 5. **安全性**: 在设计API时,需要考虑安全因素,如身份验证、授权和数据加密。Python的requests库可以用于发送安全的HTTP请求,而Flask-Security或Django-OAuth...

    业务安全应急响应新思考.pdf

    态势感知技术能帮助企业实时了解网络状况,及时发现潜在威胁,为应急响应提供决策支持。 2. **用户和行为分析(UBA)** UBA技术通过学习和分析用户的正常行为模式,可以检测出异常行为,如异常登录、不寻常的数据...

    ThinkPHP,PHP开发框架

    11. **RESTful API支持**:随着移动互联网的发展,ThinkPHP也支持RESTful风格的API开发,方便前后端分离和移动端应用的开发。 12. **版本迭代**:ThinkPHP不断进行版本迭代和优化,以适应最新的PHP技术和行业趋势,...

    DubboCloudNative之路的实践与思考.ppt

    - **Dubbo架构体系**:专注于服务治理,提供高效、稳定的服务调用,支持多种协议和服务发现方式。 Dubbo在走向Cloud Native的过程中,也做了相应的调整和优化: - **注解驱动(Annotation-driven)**:借鉴Spring ...

    阿里搜索中台devOps&AIOps;的思考与实践

    阿里搜索中台的DevOps与AIOps的思考和实践是一篇深入探讨阿里搜索中台如何通过DevOps与AIOps提升系统效率、稳定性和智能化水平的文章。从中我们可以了解到一系列相关知识点,包括中台的概念、DevOps与AIOps在实践中...

    SilverlightQQ项目实践与架构思考

    【SilverlightQQ项目实践与架构思考】 SilverlightQQ是一个基于微软的Silverlight技术开发的即时通讯应用程序,它展示了Silverlight在构建富互联网应用(RIA)方面的潜力。Silverlight,作为.NET Framework的一部分...

    apache-jmeter-5.6.2.zip

    支持RESTful和SOAP等API测试,可以通过HTTP(S) Test Script Recorder或手动配置来创建API测试。 13. **性能指标**: 可以监控和记录响应时间、吞吐量、错误率等关键性能指标,为优化应用提供数据支持。 14. **...

    基于PHP的ScarecrowBlog PHP个人博客系统.zip

    6. RESTful API:提供RESTful接口,方便与其他应用或服务进行数据交换。 三、安装与配置 1. 环境要求:确保服务器或本地环境已安装PHP、MySQL并开启相应的服务。 2. 下载源码:从官方或可靠的第三方源获取...

    restlet in actiin

    1. **开始部分**:主要介绍如何重新思考Web开发方式,设计RESTful Web API,并开始构建和部署Restlet应用程序。 2. **准备部署**:深入探讨如何生产和消费Restlet表示、保障Restlet应用的安全性、文档化和版本控制...

    spring4源码

    3. **Spring MVC**:Spring4 在 MVC 模块中增强了对 RESTful 风格的支持,通过 @RequestMapping 注解可以轻松处理HTTP请求。此外,ModelAndView 类也得到了优化,使得视图和模型数据的管理更加灵活。 4. **Spring ...

    快速web开发中的前后端框架选型最佳实践 (2).docx

    本文主要探讨了在实际项目中的一些最佳实践,并分享了作者在选型过程中的经验和思考。 前端框架方面,作者选择了requirejs作为模块化工具,用于管理和加载JavaScript模块。requirejs能够有效地组织代码,提高代码...

    The rails way

    这使得Rails应用能够很好地支持RESTful API,从而与前后端分离的现代Web架构相契合。Rails还内置了对Ruby Gem包的管理机制,开发者可以轻松地在项目中引入第三方库或框架。 本书《The Rails Way》作为Ruby on Rails...

Global site tag (gtag.js) - Google Analytics