有没有人知道遵循HATEOAS原则的RESTful客户端的示例?

时间:2009-11-21 15:32:09

标签: rest client web-services hateoas

所以到目前为止,我已经明白我们应该实现我们的RESTful服务,提供表示,使客户能够遵循HATEOAS原则。虽然理论上这一切都很有道理,但我一直在网上搜索一些严格遵循这个想法的客户代码的好例子。

我读的越多,我就越觉得这是一次学术讨论,因为没有人真正这样做!人们可以呻吟所有他们喜欢的WS- *堆栈的许多缺陷但至少很清楚如何编写客户端:你可以解析WSDL并生成代码。

现在我明白这对于一个好的RESTful服务来说不是必需的:你应该只需要了解所涉及的关系和表示,你应该能够动态地对这些做出反应。但即使如此,现在这个原则是不是应该被提炼出来并抽象成一些常见的库?提供有关您可能会收到的表示和关系的信息,并获得一些可以在您的应用程序中使用的更有用的更高级代码?

这些只是我的半生不熟的想法,但我只是担心,如果我现在潜入并编写一个正确的RESTful API,实际上没有人能够使用它!或者至少使用它会在背后造成这样的痛苦,因为人们将不得不编写胶水代码来解释我提供的关系和表示。

任何人都可以从客户的角度阐明这一点吗?有人可以展示一个正确的动态/反应式RESTful客户端代码示例,这样我就可以了解我实际上正在为之写作的受众吗? (更好的是一个提供一些抽象的客户端API的例子)否则它都非常理论化......

[编辑:请注意,我发现了一个类似的问题here,我认为这个问题没有真正得到解答,作者被一个维基百科的存根所取代!]

6 个答案:

答案 0 :(得分:17)

我们目前的项目已经完成了一半。我们返回的表示是从域对象生成的,客户端可以使用XML,JSON或XHTML来请求它们。如果它是像Firefox这样的XHTML客户端,那么一个人可以从众所周知的根资源中看到一组出站链接,并可以浏览所有其他资源。到目前为止,纯HATEOAS,以及开发人员的一个很棒的工具。

但是当客户端是程序而不是使用浏览器的人时,我们会关注性能。对于我们的XML和JSON表示,我们目前已经抑制了相关链接的生成,因为它们使表示大小增加了三倍,从而大大影响了序列化/反序列化,内存使用和带宽。我们的另一个效率问题是,对于纯HATEOAS,客户端程序在从众所周知的链接浏览到所需信息时会产生数倍的HTTP请求。因此,从效率的角度来看,如果客户知道其中编码的链接,那么这似乎是最好的。

但这样做意味着客户端必须执行大量字符串连接才能形成URI, 这容易出错并且很难重新排列资源名称空间。因此,我们使用模板系统,其中客户端代码选择模板并要求它从参数对象扩展自己。这是一种填表形式。

我真的很想看到别人在这方面遇到了什么。除了性能方面,HATEOAS似乎是一个好主意。

编辑:我们的模板是我们在Restlet框架之上编写的Java客户端库的一部分。客户端库处理HTTP请求/响应,HTTP标头,反序列化/序列化,GZIP编码等的所有细节。这使得实际的客户端代码非常简洁,并有助于将其与服务器端的一些更改隔离开来。

Roy Fielding的blog entry about HATEOAS之后有一个非常相关且有趣的讨论。

答案 1 :(得分:9)

到目前为止,我已经构建了两个访问REST服务的客户端。两者都只使用HATEOAS。我已经取得了巨大的成功,无需更新客户端即可更新服务器功能。

我使用xml:base来启用相对URL来减少xml文档中的噪音。除了加载图像和其他静态数据之外,我通常只关注用户请求的链接,因此链接的性能开销对我来说并不重要。

在客户端上,我认为需要创建的唯一常见功能是围绕我的媒体类型和管理链接的类的包装。


<强>更新

从客户端的角度来看,似乎有两种不同的方式来处理REST接口。第一个是客户端知道它想要获取什么信息的地方,并且知道它需要遍历以获取该信息的链接。当客户端应用程序的人类用户控制要遵循哪些链接并且客户端可能事先不知道将从服务器返回什么媒体类型时,第二种方法是有用的。对于娱乐价值,我分别称这两种类型的客户端,数据挖掘者和调度员。

数据挖掘者

例如,想象一下,Twitter API实际上是RESTful,我想编写一个客户端,可以检索特定Twitter用户的最新关注者的最新状态消息。

假设我使用了很棒的新的Microsoft.Http.HttpClient库,并且我编写了一些“ReadAs”扩展方法来解析来自twitter API的XML,我想它会是这样的:

var twitterService = HttpClient.Get("http://api.twitter.com").Content.ReadAsTwitterService();

var userLink = twitterService.GetUserLink("DarrelMiller");
var userPage = HttpClient.Get(userLink).Content.ReadAsTwitterUserPage();

var followersLink = userPage.GetFollowersLink();
var followersPage = HttpClient.Get(followersLink).Content.ReadAsFollowersPage();
var followerUserName = followersPage.FirstFollower.UserName;

var followerUserLink = twitterService.GetUserLink(followerUserName);
var followerUserPage = HttpClient.Get(followerUserLink).Content.ReadAsTwitterUserPage();

var followerStatuses = HttpClient.Get(followerUserPage.GetStatusesLink()).Content.ReadAsTwitterUserPage();

var statusMessage = followerStatuses.LastMessage; 

调度员

为了更好地说明这个例子,假设您正在实现一个提供家谱信息的客户端。客户端需要能够显示树,深入了解特定人员的信息并查看相关图像。请考虑以下代码段:

 void ProcessResponse(HttpResponseMessage response) {
            IResponseController controller;

            switch(response.Content.ContentType) {
                case "vnd.MyCompany.FamilyTree+xml":
                    controller = new FamilyTreeController(response);
                    controller.Execute();
                    break;
                case "vnd.MyCompany.PersonProfile+xml":
                    controller = new PersonProfileController(response);
                    controller.Execute();
                    break;
                case "image/jpeg":
                    controller = new ImageController(response);
                    controller.Execute();
                    break;
            }

        }

客户端应用程序可以使用完全通用的机制来跟踪链接并将响应传递给此调度方法。从这里开始,switch语句将控制权传递给特定的控制器类,该控制器类知道如何根据媒体类型解释和呈现信息。

显然,客户端应用程序还有很多部分,但这些部分与HATEOAS相对应。随着我浏览了许多细节,请随意让我澄清任何要点。

答案 2 :(得分:3)

诺基亚的Places API(网络存档snapshot)是RESTful并且始终使用超媒体。在其文档中也清楚地表明不鼓励使用URI模板/硬编码:

  

超媒体链接的使用

     

您的应用程序必须使用JSON中公开的超媒体链接   对任务流中的后续请求的响应,而不是   尝试通过URI模板为后续步骤构建URI。通过   使用URI模板,您的请求将不包括关键   为下一个创建响应所需的信息   请求。

答案 3 :(得分:0)

Jim,我对HATEOAS之后的RESTful客户端缺乏示例感到有点沮丧,所以我写了博客文章,显示了创建和下订单的正确HATEOAS示例。令人惊讶的是,很少有通过API执行此操作的示例,我发现它令人困惑,但这里是链接: API Example Using Rest。让我知道你的想法以及你认为我做错了什么。

答案 4 :(得分:0)

我写了两个HATEOAS客户端,一次用Java,一次用Ruby,我分享你的挫败感。在这两种情况下,完全没有缺乏对我所做工作的工具支持。例如,我使用的REST API会告诉我每个超文本控件使用什么HTTP方法,但是HttpClient不允许你传递方法,所以我最终得到了以下丑陋的代码(BTW all代码存在于自定义Ant任务中,因此BuildException s):

private HttpMethod getHypermediaControl(Node href, Node method,
        NodeList children) {
    if (href == null) {
        return null;
    }
    HttpMethod control;
    if (method == null || method.getNodeValue().equals("")
            || method.getNodeValue().equalsIgnoreCase("GET")) {
        control = new GetMethod(href.getNodeValue());
    } else if (method.getNodeValue().equalsIgnoreCase("POST")) {
        control = new PostMethod(href.getNodeValue());
    } else if (method.getNodeValue().equalsIgnoreCase("PUT")) {
        control = new PutMethod(href.getNodeValue());
    } else if (method.getNodeValue().equalsIgnoreCase("DELETE")) {
        control = new DeleteMethod(href.getNodeValue());
    } else {
        throw new BuildException("Unknown/Unimplemented method "
                + method.getNodeValue());
    }
    control.addRequestHeader(accept);
    return control;
}

这最终成为我使用的REST客户端实用程序方法的基础。

private HttpMethod getHypermediaControl(String path, Document source)
        throws TransformerException, IOException {

    Node node = XPathAPI.selectSingleNode(source, path);
    return getHypermediaControl(node);
}

private HttpMethod getHypermediaControl(Node node) {
    if (node == null) {
        return null;
    }
    NamedNodeMap attributes = node.getAttributes();
    if (attributes == null) {
        return null;
    }
    Node href = attributes.getNamedItem("href");
    Node method = attributes.getNamedItem("method");
    HttpMethod control = getHypermediaControl(href, method,
            node.getChildNodes());
    return control;
}

private Document invokeHypermediaControl(HttpClient client, Document node,
        final String path) throws TransformerException, IOException,
        HttpException, URIException, SAXException,
        ParserConfigurationException, FactoryConfigurationError {
    HttpMethod method = getHypermediaControl(path, node);
    if (method == null) {
        throw new BuildException("Unable to find hypermedia controls for "
                + path);
    }
    int status = client.executeMethod(method);

    if (status != HttpStatus.SC_OK) {
        log(method.getStatusLine().toString(), Project.MSG_ERR);
        log(method.getResponseBodyAsString(), Project.MSG_ERR);
        throw new BuildException("Unexpected status code ("
                + method.getStatusCode() + ") from " + method.getURI());
    }
    String strResp = method.getResponseBodyAsString();
    StringReader reader = new StringReader(strResp);
    Document resp = getBuilder().parse(new InputSource(reader));
    Node rval = XPathAPI.selectSingleNode(resp, "/");
    if (rval == null) {
        log(method.getStatusLine().toString(), Project.MSG_ERR);
        log(method.getResponseBodyAsString(), Project.MSG_ERR);
        throw new BuildException("Could not handle response");
    }
    method.releaseConnection();
    return resp;
}

使用这些代码,我可以非常轻松地编写将遍历返回的文档中的超媒体控件的客户端。缺少的主要部分是对表单参数的支持。幸运的是,我使用的所有控件都是无参数的,除了一个(我遵循rule of three in regards to refactoring)。为了完整起见,这是代码片段的样子:

    HttpMethod licenseUpdateMethod = getHypermediaControl(
            "/license/update", licenseNode);
    if (licenseUpdateMethod == null) {
        log(getStringFromDoc(licenseNode), Project.MSG_ERR);
        throw new BuildException(
                "Unable to find hypermedia controls to get the test suites or install the license");
    } else if (license != null) {
        EntityEnclosingMethod eem = (EntityEnclosingMethod) licenseUpdateMethod;
        Part[] parts = { new StringPart("license", this.license) };
        eem.setRequestEntity(new MultipartRequestEntity(parts, eem
                .getParams()));
        int status2 = client.executeMethod(eem);
        if (status2 != HttpStatus.SC_OK) {
            log(eem.getStatusLine().toString(), Project.MSG_ERR);
            log(eem.getResponseBodyAsString(), Project.MSG_ERR);
            throw new BuildException("Unexpected status code ("
                    + eem.getStatusCode() + ") from " + eem.getURI());
        }
        eem.releaseConnection();
    }

现在,应该做的是查看/license/update的孩子,找出需要传递的参数,但是that will have to wait until I have two more parameterised form that I need to follow

顺便说一下,经过所有的努力,它非常令人满意并且很容易修改服务器而不会影响客户端。感觉非常好,我很惊讶它在某些州没有被禁止。

答案 5 :(得分:-4)

您选择的网络浏览器是整个WWW的“纯HATEOAS”客户端。

这个问题真的没有意义imo。