2008年10月10日星期五

一点点关于Restful Web Service的设计的思考

Restful Web Service 的五条原则:
* 为所有“事物”定义ID
* 将所有事物链接在一起
* 使用标准方法
* 资源多重表述
* 无状态通信

如果你要实现这样的Restful WS那么你可能会遇到和我遇到的一样的一些问题:

问题1:面向资源和面向消息动作的服务

Restful Web Service是面向资源的服务,不同于SOAP是面向消息和动作的服务,Rest WS应该用URI来表示资源.
这就有一个问题,对于资源来讲是没有业务语义的,比如一个缺陷跟踪软件如果需要暴露这样两个服务:
1) update一个bug,对于SOAP来讲,在WSDL里暴露一个update的operation,服务端代码更新数据库就好了
2) reopen一个bug,对于SOAP来讲,你需要WSDL暴露一个operation是reopen,然后服务端代码除了会操作数据库更改bug的状态之外,还会发送email通知owner,并启动一个处理bug的工作流。
这两个操作本质上都会update bug的字段,但是由于两个操作有业务语义,所以他们是不同的,后者会启动一个业务工作流,而前者不会。

对于Restful Web Service来讲,更改bug在数据库的状态很简单,但是如何区分语义呢?我们知道这个HTTP PUT(或POST)操作在Restful WS中本身只知道我要更新资源,它本身是没有业务语义的,因为Rest WS是基于资源的服务,没有任何业务逻辑。
这个问题确实比较头疼,在现实世界中,大多数复杂的应用是粗颗粒基于消息和动作的,比如RPC.如果你需要对资源操作,那么客户端就要负责业务逻辑和事务性等,这对客户端是一个很大的麻烦。比如一个SOAP里deletePersons( names[])对应到RestWS就要客户端循环发送DELETE /persons/name这个请求,对于更复杂的运算客户端不得不更多的了解服务端的内部数据关系,严重破坏了封装性。另外,这种情况下客户端也很难实现事务性。
但是反过来说,现实世界中暴露的服务,可能80%的操作还是最基本的CRUD(增读改删),这个比较适合Rest,清晰简单,可能20%的操作还是有很强的业务语义的操作,更适合SOAP,这个比例每个项目会不太一样,但是肯定是CRUD比较多。那么如果你要使用Restful WS如何平衡呢?我的想法是,做一些违反Restful规则的服务。比如deletePersons(names[])这个操作,你可以发一个请求 POST /persons?deletePersons,然后请求体包含一段XML或者JSON包含所有的name.事实上很多互联网网站提供的Restful API也是这种面向资源和动作的服务的混合体,正是那句话,没有银弹。

问题2,客户端说,该死,Restful不是规范所以更不会有WSDL,我怎么知道怎么调用?
另外Restful WS没有WSDL那样的规范,即便你用JSON或者ATOM协议,具体内容(比如atom的content tag)仍然是没有具体规定的,你可以纯文本或者POX(Plain Old XML)或者base64的内容放到atom的content tag里,没有限制,如果资源是一个树状结构的复杂数据实体,那么客户端怎么才能知道如何产生请求报文和解析响应呢?
我的想法:提供MDS(meta data service),暴露一个全局的服务,告诉客户端每个服务支持哪些格式,比如如果你的内容是POX那么这个MDS应该会是很多schema,如果内容也可以是JSON格式,因为JSON没有schema,但是可以用BNF范式,不过BNF读起来实在不是很方便,你可以用JSON-XML转换的规则来套用XML的Schema (http://www.ibm.com/developerworks/library/x-atom2json.html, 我更喜欢这个 http://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html) 。还有一种方式,就是把复杂的资源拆分为多个小的资源,保证所有资源背后的数据都是flat的key/value pair,这样你就可以通过POST/PUT做表单提交一样的更新数据。但是这个工作有些时候对调用者不太方便,事务性也很难保证。

Restful WS没有任何WS-Enumeration, WS-Policy, WS-Security, WS-Transaction这样的协议集合,所以你需要自己来实现,这是没有标准的。这么看来Restful WS确实比较单薄,但是或许正是因为Restful 比较简单所以才会使用越来越广泛。这也不是一个大问题,你可以尽量利用现有技术或者实现自己的方案,比如安全性你可以使用证书和HTTPS的保证验证和传输安全。

问题3,我该用PUT 还是 POST?
根据HTTP规范,PUT适用于做安全的幂等性操作(idempotent),换句话说,两次操作是安全的,不会导致不同的结果。
比如一条SQL:update gender='male' where name='daniel'执行两次不会造成不同的结果
类似的还有:createOrUpdate(user1)调用两次也没有问题
非幂等性的操作就不能保证这一点,比如,一条SQL:update counter=counter+1 where name='x'执行两次就会产生不同的结果,再比如create(user1)调用两次会创建两个一样的用户,这也会产生不同的结果。

这意味着PUT其实可以看作是createOrUpdate操作,比如
PUT /persons/daniel第一次会创建daniel这个人,第二次请求会更新daniel这个人,如果两个请求内容一样,那么更新操作其实不会对第一次创建的daniel做任何改动。

和PUT一样GET,HEAD,PUT,DELETE,OPTIONS和TRACE都有这种性质.
DELETE可以用于删除,GET可以用于读取。

比较特殊的是POST,他蕴含的意思是,嘿,客户端,调用我的话,我可不保证幂等性,我可能会启动一个工作流,也可能会插入一个消息到另一个系统,我什么都能做,但我就是不能保证你调用我两次同样的请求会出现什么后果。
上面我们提到过的Restful WS是没有面向消息和动作的服务的,如果我们要提供的话,其实POST是最好的选择,他可以做任何事情。而PUT更适合做面向资源的服务,用来创建或者更新一个数据。

参考HTTP规范http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html:
The fundamental difference between the POST and PUT requests is reflected in the different meaning of the Request-URI. The URI in a POST request identifies the resource that will handle the enclosed entity. That resource might be a data-accepting process, a gateway to some other protocol, or a separate entity that accepts annotations. In contrast, the URI in a PUT request identifies the entity enclosed with the request -- the user agent knows what URI is intended and the server MUST NOT attempt to apply the request to some other resource.

但是Atom Publishing Protocol这种经常被用作Restful WS的协议去不是这样的,她就是用PUT做更新,POST做创建。为什么呢?我觉得现实世界中,大多数情况下当你创建一个entry你是不知道他的主键的,这个主键90%的情况下都是服务器端生成的自增主键。如果你用PUT去创建一个entry,比如PUT /rest/person/daniel, 这隐含着daniel就是person的主键值,这里不能有两个人都叫daniel。而事实上当你创建一个新的person你通常是不指定主键值,而是服务器端返回一个社会福利号码或者身份证编号这样的主键,所以如果你还用PUT那么请求看起来应该是这样PUT /rest/person, 然后创建出来的person的URI /rest/person/12434320456。乍一看没问题,其实问题很严重,PUT URI之后,资源的URI不应该变化,这里你的URI从没有后面的数字ID变成了有数字ID, 这种对一个URL提交然后在服务端处理后在另外一个URL暴露你提交的数据,这不是POST在HTTP规范里定义的行为么?所以,呵呵,APP认为大多数情况下ID是服务端生成的,所以用POST更合适。

那到底我们应该用什么呢?PUT还是POST创建记录?
我觉得,如果你知道新纪录的主键,比如客户端可以生成GUID你就可以用PUT,90%的情况下都是自增主键,那你还是用POST。

没有评论: