49

所以现在我明白了,我们都应该实现我们的 RESTful 服务,提供使客户能够遵循HATEOAS原则的表示。虽然理论上这一切都说得通,但我一直在网上搜索,以找到一个严格遵循这个想法的客户端代码的一个很好的例子。

我读得越多,我就越觉得这是一场学术讨论,因为实际上没有人这样做!人们可以尽情地抱怨 WS-* 堆栈的许多缺陷,但至少很清楚如何编写客户端:您可以解析 WSDL 并生成代码。

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

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

任何人都可以从客户的角度对此有所了解吗?有人可以展示一个正确动态/反应式 RESTful 客户端代码的示例,以便我了解我实际为之写作的受众吗?(最好还是一个提供一些抽象的客户端 API 的例子)否则它都是非常理论的......

[编辑:注意,我在这里发现了一个类似的问题,我认为没有得到真正的回答,作者被维基百科存根骗了!]

4

6 回答 6

18

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

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

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

我真的很想看看其他人在这方面的经历。除了性能方面,HATEOAS 似乎是一个好主意。

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

Roy Fielding 的关于 HATEOAS 的博客文章随后进行了非常相关且有趣的讨论。

于 2009-11-21T18:09:00.510 回答
10

到目前为止,我已经构建了两个访问 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 的部分。当我浏览了许多细节时,请随时要求我澄清任何要点。

于 2009-11-22T00:13:26.430 回答
3

诺基亚的Places API(Web 存档快照)是 RESTful 并始终使用超媒体。在其文档中也很明确地不鼓励使用 URI 模板/硬编码:

超媒体链接的使用

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

于 2012-10-31T19:15:21.947 回答
1

Jim,我也对缺乏遵循 HATEOAS 的 RESTful 客户端的示例感到有些沮丧,所以我写了一篇博客文章,展示了一个用于创建和下订单的适当 HATEOAS 示例。令人惊讶的是,通过 API 执行此操作的示例很少,我发现它有点令人困惑,但这里是链接: 使用 Rest 的 API 示例。让我知道你的想法和你认为我做错了什么。

于 2012-05-16T20:57:36.743 回答
0

我已经编写了两个 HATEOAS 客户端,一次用 Java,一次用 Ruby,我和你一样感到沮丧。在这两种情况下,我所做的工作都完全缺乏工具支持。例如,我使用的 REST API 会告诉我为每个超文本控件使用什么 HTTP 方法,但是HttpClient不允许您传入该方法,所以我最终得到了以下丑陋的代码(顺便说一句,所有代码都在一个自定义的 Ant 任务,因此是BuildExceptions):

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;
}

使用这一点代码,我可以相当轻松地编写将遍历返回文档中的超媒体控件的客户端。缺少的主要部分是对表单参数的支持。幸运的是,我使用的所有控件都是无参数的,除了一个(我在重构方面遵循三规则)。为了完整起见,代码片段如下所示:

    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以找出需要传递哪些参数,但这必须等到我有两个需要遵循的参数化形式

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

于 2012-10-31T20:27:42.717 回答
-6

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

这个问题真的没有意义。

于 2009-11-23T21:21:39.757 回答