01

Restlet is a great, lightweight REST framework for Java developers. The only problem, there are not that many examples on how to use this framework available.
Some examples are included after installing the package under the src/org.restlet.example directory. But these examples are simple starting points and do not fully describe how to create a full REST web service with create, update and delete functionality.
I am using the 1.1 M4 version of restlet. The API documentation is available here.
The example code works but does not execute any database lookup. To make this example more useful, it is recommended to replace the :TODO lines with your database lookup.

First, create a simple User class. Which contains a variable id and name.

import java.nio.Buffer;

import org.json.JSONObject;

public class User {

private String id = null;
private String name = null;

/**
 * @return Returns the id.
 */
public String getId() {
 return id;
}

/**
 * @param id
 *            The id to set.
 */
public void setId(String id) {
 this.id = id;
}

/**
 * @return Returns the name.
 */
public String getName() {
 return name;
}

/**
 * @param name
 *            The name to set.
 */
public void setName(String name) {
 this.name = name;
}

/**
 * Convert this object to a JSON object for representation
 */
public JSONObject toJSON() {
try{
 JSONObject jsonobj = new JSONObject();
 jsonobj.put("id", this.id);
 jsonobj.put("name", this.name);
 return jsonobj;
}catch(Exception e){
 return null;
}
}

/**
 * Convert this object to a string for representation
 */
public String toString() {
 StringBuffer sb = new StringBuffer();
 sb.append("id:");
 sb.append(this.id);
 sb.append(",name:");
 sb.append(this.name);
 return sb.toString();
}
}

We also need some kind of error object to represent an error message. This is a very simple example

import org.json.JSONObject;

public class ErrorMessage {

public JSONObject toJSON() {
 JSONObject jsonobj = new JSONObject();
 try {
  jsonobj.put("error", "An error occured");
  return jsonobj;
 } catch (Exception e) {
  return null;
 }
}
public String toString() {
 StringBuffer sb = new StringBuffer();
 sb.append("error:");
 sb.append("An error occured");
 return sb.toString();
}
}

Now, let's create our UserResource.

import org.restlet.Context;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;
import org.restlet.ext.json.JsonRepresentation;
import org.restlet.resource.Representation;
import org.restlet.resource.Resource;
import org.restlet.resource.ResourceException;
import org.restlet.resource.StringRepresentation;
import org.restlet.resource.Variant;

public class UserResource extends Resource {

private User user = null;

public UserResource(Context context, Request request, Response response) {
 super(context, request, response);
 String userid = null;
 userid = (String) getRequest().getAttributes().get("id");
 this.user = findUser(userid);
 getVariants().add(new Variant(MediaType.TEXT_PLAIN));
 getVariants().add(new Variant(MediaType.APPLICATION_JSON));
}

/**
 * Allow a PUT http request
 *
 * @return
 */
public boolean allowPut() {
 return true;
}

/**
 * Allow a POST http request
 *
 * @return
 */
public boolean allowPost() {
 return true;
}

/**
 * Allow a DELETE http request
 *
 * @return
 */
public boolean allowDelete() {
 return true;
}

/**
 * Allow the resource to be modified
 *
 * @return
 */
public boolean setModifiable() {
 return true;
}

/**
 * Allow the resource to be read
 *
 * @return
 */
public boolean setReadable() {
 return true;
}

/**
 * Find the requested user object
 *
 * @param userid
 * @return
 */
private User findUser(String userid) {
 try {
  if (null == userid)
   return null;
  // :TODO {do some database lookup here }
  // user = result of lookup
  // This part should be replaced by a lookup
  User u = new User();
  u.setId("1");
  u.setName("name");
  // end replace
  return u;
 } catch (Exception e) {
  return null;
 }
}

/**
 * Represent the user object in the requested format.
 *
 * @param variant
 * @return
 * @throws ResourceException
 */
public Representation represent(Variant variant) throws ResourceException {
 Representation result = null;
 if (null == this.user) {
  ErrorMessage em = new ErrorMessage();
  return representError(variant, em);
 } else {
  if (variant.getMediaType().equals(MediaType.APPLICATION_JSON)) {
   result = new JsonRepresentation(this.user.toJSON());
  } else {
   result = new StringRepresentation(this.user.toString());
  }
 }
 return result;
}

/**
 * Handle a POST Http request. Create a new user
 *
 * @param entity
 * @throws ResourceException
 */
public void acceptRepresentation(Representation entity)
  throws ResourceException {
 // We handle only a form request in this example. Other types could be
 // JSON or XML.
 try {
  if (entity.getMediaType().equals(MediaType.APPLICATION_WWW_FORM,
    true)) {
   Form form = new Form(entity);
   User u = new User();
   u.setName(form.getFirstValue("user[name]"));
   // :TODO {save the new user to the database}
   getResponse().setStatus(Status.SUCCESS_OK);
   // We are setting the representation in the example always to
   // JSON.
   // You could support multiple representation by using a
   // parameter
   // in the request like "?response_format=xml"
   Representation rep = new JsonRepresentation(u.toJSON());
   getResponse().setEntity(rep);
  } else {
   getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
  }
 } catch (Exception e) {
  getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
 }
}

/**
 * Handle a PUT Http request. Update an existing user
 *
 * @param entity
 * @throws ResourceException
 */
public void storeRepresentation(Representation entity)
  throws ResourceException {
 try {
  if (null == this.user) {
   ErrorMessage em = new ErrorMessage();
   Representation rep = representError(entity.getMediaType(), em);
   getResponse().setEntity(rep);
   getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
   return;
  }
  if (entity.getMediaType().equals(MediaType.APPLICATION_WWW_FORM,
    true)) {
   Form form = new Form(entity);
   this.user.setName(form.getFirstValue("user[name]"));
   // :TODO {update the new user in the database}
   getResponse().setStatus(Status.SUCCESS_OK);
   // We are setting the representation in this example always to
   // JSON.
   // You could support multiple representation by using a
   // parameter
   // in the request like "?response_format=xml"
   Representation rep = new JsonRepresentation(this.user.toJSON());
   getResponse().setEntity(rep);
  } else {
   getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
  }
 } catch (Exception e) {
  getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
 }
}

/**
 * Handle a DELETE Http Request. Delete an existing user
 *
 * @param entity
 * @throws ResourceException
 */
public void removeRepresentations()
  throws ResourceException {
 try {
  if (null == this.user) {
   ErrorMessage em = new ErrorMessage();
   Representation rep = representError(MediaType.APPLICATION_JSON, em);
   getResponse().setEntity(rep);
   getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
   return;
  }
  // :TODO {delete the user from the database}
  getResponse().setStatus(Status.SUCCESS_OK);
 } catch (Exception e) {
  getResponse().setStatus(Status.SERVER_ERROR_INTERNAL);
 }
}

/**
 * Represent an error message in the requested format.
 *
 * @param variant
 * @param em
 * @return
 * @throws ResourceException
 */
private Representation representError(Variant variant, ErrorMessage em)
  throws ResourceException {
 Representation result = null;
 if (variant.getMediaType().equals(MediaType.APPLICATION_JSON)) {
  result = new JsonRepresentation(em.toJSON());
 } else {
  result = new StringRepresentation(em.toString());
 }
 return result;
}

protected Representation representError(MediaType type, ErrorMessage em)
  throws ResourceException {
 Representation result = null;
 if (type.equals(MediaType.APPLICATION_JSON)) {
  result = new JsonRepresentation(em.toJSON());
 } else {
  result = new StringRepresentation(em.toString());
 }
 return result;
}
}

And implement a restlet server, listening on port 8100.

import org.restlet.Application;
import org.restlet.Component;
import org.restlet.Context;
import org.restlet.Restlet;
import org.restlet.Router;
import org.restlet.data.MediaType;
import org.restlet.data.Protocol;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.resource.StringRepresentation;

public class WebServiceApplication extends Application {

    public static void main(String[] args) throws Exception {

        // Create a component
        Component component = new Component();
        component.getServers().add(Protocol.HTTP, 8100);

        WebServiceApplication application = new WebServiceApplication(
                component.getContext());

        // Attach the application to the component and start it
        component.getDefaultHost().attach(application);
        component.start();
    }

    public WebServiceApplication() {
        super();
    }

    public WebServiceApplication(Context context) {
        super(context);
    }

    @Override
    public Restlet createRoot() {

        Router router = new Router(getContext());
        router.attach("/users", UserResource.class);
        router.attach("/users/{id}", UserResource.class);

        Restlet mainpage = new Restlet() {
            @Override
            public void handle(Request request, Response response) {
                StringBuilder stringBuilder = new StringBuilder();

                stringBuilder.append("<html>");
                stringBuilder
                        .append("<head><title>Sample Application Servlet Page</title></head>");
                stringBuilder.append("<body bgcolor=white>");

                stringBuilder.append("<table border=\"0\">");
                stringBuilder.append("<tr>");
                stringBuilder.append("<td>");
                stringBuilder.append("<h1>example - REST</h1>");
                stringBuilder.append("</td>");
                stringBuilder.append("</tr>");
                stringBuilder.append("</table>");
                stringBuilder.append("</body>");
                stringBuilder.append("</html>");

                response.setEntity(new StringRepresentation(stringBuilder
                        .toString(), MediaType.TEXT_HTML));

            }
        };
        router.attach("", mainpage);
        return router;
    }
}

You can test the restlet web service with any internet browser except for the PUT and DELETE requests. I prefer curl to test a Restful web service.

GET REQUEST – Show the information of a user:

curl http://localhost:8100/users/1

POST REQUEST – Create a new user:

curl -d “user[name]=John” http://localhost:8100/users

PUT REQUEST – Update an existing user:

curl -X PUT -d “user[name]=Jane” http://localhost:8100/users/1

DELETE REQUEST – Delete an existing user:

curl -X DELETE http://localhost:8100/users/1k

Tagged with:
16

作者 Stefan Tilkov 译者 徐涵 发布于 2008年8月4日 上午7时25分

人们在试验REST时,通常会四处寻找样例——而他们往往不仅能找到一大堆自称“符合REST”或标榜为“REST API”的样例,还会发现许多关于某个自称符合REST的特定服务名不副实的讨论。

为什么会这样?HTTP虽不是什么新事物,但人们使用它的方式却五花八门。其中有些做法符合Web设计者的初衷,但许多并非如此。要为你的HTTP 应用(无论是面向人类、还是计算机、或同时面向这两者使用的)应用REST原则,意味着你要恰好反过来:尽量“正确地”使用Web,或者说按符合REST 的方式使用Web(倘若你不喜欢用对或错来评判的话)。对许多人来说,这的确是一种崭新的方式方法。

我经常在文章里作同样的声明:REST、Web和HTTP是不同的事物;REST可以用多种不同技术来实现,而HTTP只是一种恰好符合REST架 构风格的具体架构。所以,其实我应该小心区分“REST”与“REST式HTTP”这两个概念的。但我没有这么做,在本文剩余部分,我们姑且认为它们是相 同的事物。

跟任何新的方式方法一样,发掘一些共同的模式是有益的。在本系列的第一第二篇 文章中,我已经讲述了一些基础——比如集合资源的概念、将计算结果转换为资源本身、以及用聚合(syndication)来模仿事件。后续文章将进一步讲 述这些及其他模式。不过在本文中,我想主要说说反模式(anti-patterns)——即那些力求符合REST式HTTP、但未能成功而造成问题的典型 做法。

首先我们来看看我发掘了哪些反模式:

  1. 全部采用GET
  2. 全部采用POST
  3. 忽视缓存
  4. 忽视响应代码
  5. 误用cookies
  6. 忘记超媒体
  7. 忽视MIME类型
  8. 破坏自描述性

下面我们来逐个详细说明。

全部采用GET

在许多人看来,REST仅仅意味着用HTTP暴露一些应用功能。HTTP GET是最重要的基本操作(operation )(严格地讲,用“动词(verb)”或“方法(method)”这样的术语比较好)。GET方法应当用于获取由URI标识的资源的一个表示 (representation),而许多(即便谈不上所有)现有的HTTP库和服务器编程API不是将URI视为一种资源标识符(resource identifier),而是将之视为一种传递参数的便利手段。这导致了以下这种URIs的出现:

http://example.com/some-api?method=deleteCustomer&id=1234

实际上,你无法根据构成URI的字符获知关于给定系统的“REST性(RESTfulness)”的任何信息,不过对于上面那个URI,我们可以判 断该 GET操作不是“安全的(safe)”——也就是说,调用者很可能要为结果(删除一个客户)负责,尽管规范里说在这种情况下使用GET方法是错误的。

这种做法唯一有利的方面在于它编程起来容易,而且在浏览器中调试也简单——你只要把URI粘贴到浏览器地址栏里、然后调整一些“参数”就行了。这种反模式主要存在以下问题:

  1. URI没有被用作资源标识符,而是被用于传递操作及其参数了。
  2. HTTP方法(HTTP method)不一定跟语义相符。
  3. 这种链接一般不可加入书签。
  4. 有“爬虫”造成非预期副作用的风险。

注意:符合这一反模式的APIs没准最终碰巧符合REST原则。这里有个例子:

http://example.com/some-api?method=findCustomer&id=1234

这个URI是标识操作及其参数呢,还是标识一个资源呢?两种情况都有可能:它可以是一个完全合法的、可加入书签的URI;对它做GET操作也许是 “安全的 ”;它也许会根据Accept报头返回不同的格式,并支持复杂的缓存机制。在很多情况下,这将是偶然的。API经常在刚开始时采用这种方式来暴露一个“读 ”接口,但当开发者要增添“写”功能时就有问题了(因为你无法通过对上述URI做PUT操作来更新一个客户——开发者得构造另一个URI)。

全部采用POST

这一反模式跟前一个颇为相似,只不过这里用的是POST方法而已。POST除了携带一个URI,还携带一个实体主体(entity body)。一个典型的场景是:将单个URI作为POST请求的目标、通过发送不同的消息来表达不同的意图。实际上,SOAP 1.1 Web服务就是这样做的,它把HTTP当作一种“传输协议”来用。服务器根据SOAP消息(可能还包括一些WS-Addressing SOAP报头)决定做什么。

可能有人认为“全部采用POST”跟“全部采用GET”存在的问题完全一样,只是它更难用一些,而且不能利用缓存(甚至连偶尔的机会都没有),且无法支持书签。事实上,它并不是违反了哪条REST原则,而是根本忽视了REST原则。

忽视缓存

即使你按各个动词的原本意图来使用它们,你仍可以轻易禁止缓存机制。最简单的做法就是在你的HTTP响应里增加这样一个报头:

Cache-control: no-cache

这样可以禁止缓存机制发挥作用。当然,这也许正是你想要做的,然而通常这只是你的Web框架规定的一个缺省设置。不过,对高效的缓存与再验证 (caching and re-validation)的支持,是采用REST式HTTP的诸多关键优点之一。Sam Ruby表示,在判断是否符合REST原则时的一个关键问题就是“你支持ETag吗”? (ETag是HTTP 1.1里引入的一种机制,它允许客户端通过加密的校验和来验证一个被缓存的表示是否仍然有效)。要生成正确的报头,最简单的做法就是把这个任务交给一个“ 知道”怎样做的基础设施——例如通过在Web服务器(比如Apache HTTPD)的目录里生成一个文件。

当然,这也要涉及到客户端一方:你在为一个REST式服务实现程序客户端时,你应充分利用现有的缓存机制,以免每次都重新获取表示。例如,服务器也 许已经发出信息:初次返回的表示在600秒内都可被认为是“新的”(比方说因为后端系统每30分钟才轮询一次)。这样的话,短时间内重复请求同一信息就完 全没必要了。在客户端设置一个代理缓存(比如Squid)也许比自行构建相应逻辑更好。

HTTP的缓存机制强大而复杂;Mark Nottingham的《缓存指南(Cache Tutorial)》是一个很好的指南。

忽视响应代码

HTTP提供了一组丰富的应用级状态代码, 它们可用于应付不同场合,不过许多Web开发者对此并不知晓。大部分人对200(“OK”)、404(“Not found”)和500(“Internal server error”)这些状态代码是比较熟悉的。但除此以外还有很多其他状态代码,正确使用这些状态代码意味着客户端与服务器可以在一个具备较丰富语义的层次上 进行沟通。

例如,201(“Created”)响应代码表明已经创建了一个新的资源,其URI在Location响应报头里。409(“Conflict”) 告诉客户端存在冲突,比如随PUT请求发送的是基于老版本资源的数据。再如,412(“Precondition Failed”)表明服务器不能满足客户端的预期。

正确使用状态代码的另一方面涉及客户端:应该根据一种统一的总体方法对不同类别的状态代码(例如所有2xx段代码、所有5xx段代码)作不同处理——例如,即便客户端不具备处理特定代码的逻辑,但至少应把所有2xx段代码视为成功信号。

许多声称符合REST的应用仅仅返回200或500,甚至只返回200(并在响应实体主体里给出错误文本——SOAP就是这样的)。你要是愿意,可 以称之为“通过状态代码200传达错误”,但无论你觉得采用哪个术语好,假如你不利用HTTP状态代码丰富的应用语义,那么你将错失提高重用性、增强互操 作性和提升松耦合性的机会。

误用cookies

利用cookies来传播某个服务端会话状态的键(key)是另一种REST反模式。

Cookies表明肯定哪个地方不符合REST了。是这样吗?不;不一定。REST的关键思想之一是无状态性(statelessness)——不 是说一个服务器不能保存任何数据:倘若是资源状态(resource state)或客户端状态(client state),那是可以的。服务器不能保存的是会话状态(session state),因为那会造成可伸缩性、可靠性及耦合方面的问题。Cookies的最典型的用法是:保存一个跟“某个保存在服务端内存里的数据结构”相关联 的键(key)。这意味着,浏览器随各次请求发出去的cookie是被用于构建会话状态的。

如果一个cookie被用于保存一些“服务器不依赖于会话状态即可验证”的信息(比如认证令牌),那么这样的cookies是完全符合REST原则 的—— 不过有一点需要注意:如果有其他更为标准的方式来传递一则信息(比如放在URI里、放在某个标准报头里、或较少见地放在消息主体里),那就不应将之放在 cookie里。例如,按REST式HTTP的观点来使用HTTP认证就比较好。

忘记超媒体

最不易接受的REST思想就是标准的方法集合。REST理论并没有规定标准集合由哪些方法组成,它只是规定必须有一组适用于所有资源的方法集合。对 于 HTTP来说,这组集合是GET、PUT、POST和DELETE(至少起初是这样),你需要一定适应时间才能掌握如何将所有应用语义投射到这四个动词 上。但你一旦适应了,就可以开始运用这个REST的子集——一种基于Web的CRUD(Create、Read、Update、 Delete)架构——了。暴露这种反模式的应用不是真正的“非REST式”应用(假如存在这种事物的话),它们只是未能利用一个REST核心概念——“ 超媒体即应用状态引擎(hypermedia as the engine of application state)”。

超媒体(hypermedia)是一个把事物链接起来的概念,正是它造就了Web这个网——一个互联的资源集合,应用通过跟随链接从一个状态进入另一个状态。这听上去也许有点深奥,不过其实遵从这一原则是有正当理由的。

“忘记超媒体”反模式的首要表现就是:表示(representation)里缺少链接。尽管通常客户端可以根据一定的规则来构造URI,但是因为 服务器没有发送任何链接,所以客户端将无法跟随链接。一种较好的做法是:即支持构造URI,又支持跟随链接——这里的链接通常反映了下层数据模型中的关 系。但最好的情况是:客户端应该只需知道一个URI;其他URI(各个URI及其构造模式,如:各种查询字符串)应该通过超媒体(作为资源表示里的链接) 来传达。 Atom发布协议(Atom Publishing Protocol)就是一个好例子,它有一个服务文档(service documents)的概念,服务文档为它所描述的域内的各个集合提供具名元素(named elements)。最后,应用可能经历的状态迁移应该是动态传播的,客户端应该可以不用掌握多少知识就可以跟随它们。HTML就是一个好榜样,它包含足 够的信息,以便浏览器可以向用户提供一个完全动态的接口。

我本想增加一个“人类可读的URI”反模式的。但我没那么做,因为我跟其他人一样也喜欢可读的、好“篡改”的URI。但是当人们采用REST时,他 们经常浪费许多时间来讨论“正确的”URI设计,而忘记了超媒体方面。所以,我建议你不要花太多时间来寻找正确的URI设计(毕竟,它们只是字符串而 已),而是多花一些精力在表示里寻找提供链接的正确地方。

忽视MIME类型

HTTP有个内容协商(content negotiation)的概念,它允许客户端根据需要获取资源的不同表示(representations)。例如,一个资源也许有不同格式的表示(如 XML、JSON或YAML等)以便于用各种不同语言(如Java、JavaScript及Ruby)实现的消费者所使用。再如,一个资源可能即有面向人 类的PDF或JPEG版表示,又有“机器可读的”XML版表示。还有,一个资源可能同时支持v1.1版和v1.2版的自定义表示格式。不管怎样,也许可以 为“只有一个表示格式”找到理由,但这常常意味着丢掉某种机会。

显然,若一个服务能为更多未预见到的客户端所用(或重用)那更好。因此,依靠现有、预定义、广为人知的格式,要好过发明私有格式——这会导致本文讲述的最后一个反模式。

破坏自描述性

这种反模式是如此普遍,以至于几乎在每个、甚至那些由所谓的“REST狂热者们”(包括我在内)创建的REST应用里都可以看到:违反自描述性约束 (这一努力目标并不像人们最初想象的那样跟人工智能科幻小说有多大牵连)。理想情况下,一个消息(HTTP 请求或HTTP响应,包括报头与主体)应该包含足够信息,以便任何通用客户端、服务器或媒介(intermediary)能够处理它。例如,当你的浏览器 获取某个受保护资源的PDF表示(representation)时,你可以看到由标准达成的协定是如何起作用的:有些HTTP认证交换发生,可能会发生 一些缓存(caching)和/或再验证(revalidation),服务器发送的content-type报头(application/pdf)触发了你系统里注册的PDF阅读器,最后你得以在自己的屏幕上阅读该PDF。所有用户都可以用他们自己的基础设施来执行同样的请求。若服务器开发者另外增加一种内容类型,那么服务器的客户端(或服务的消费者)只需确保他们安装了正确的阅读器即可。

你要是发明自己的报头、格式或协议,那就一定程度上破坏了自描述性约束。极端地讲,所有没有被某个标准化组织官方标准化的东西都违反此约束,因而可 被认为符合本反模式。在实践中,你应努力做到尽可能遵循标准,并懂得“某些协定可能只在一个较小的领域(比方说,你的服务和客户端是专门针对它开发的)中 适用” 的道理。

总结

自从“四人组(Gang of Four)”出版了书籍、 掀起模式运动的开端以来,许多人误解了它,并试图在尽可能多的场合下应用模式——这已被其他人所取笑。模式应当仅在符合上下文时才被应用。同样地,可能有 人会不遗余力地在所有场合下虔诚地努力避免所有反模式。许多时候,你有充分理由违反某一规则,或者按REST的术语放松某一约束。这么做是没问题的——但 了解实际情况、作出知情决策是有益的。

但愿本文能有助于你在开始首个REST项目时避免落入这些常见的陷阱。

非常感谢Javier Botana和Burkhard Neppert对本文初稿的反馈。

Stefan Tilkov是InfoQ SOA社区的首席编辑,以及位于德国/瑞士的innoQ公司的合伙人、首席顾问和主要的REST狂热主义者。

查看英文原文:REST Anti-Patterns

Tagged with:
preload preload preload