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

发布于 2024-08-12 18:55:31 字数 1539 浏览 2 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

余生再见 2024-08-19 18:55:31

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

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

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

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

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

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

We've kind of half-done this on our current project. The representations we return are generated from domain objects, and the client can ask for them either in XML, JSON, or XHTML. If it's an XHTML client like Firefox, then a person sees a set of outbound links from the well-known root resource and can browse around to all the other resources. So far, pure HATEOAS, and a great tool for developers.

But we're concerned about performance when the client is a program, not a human using a browser. For our XML and JSON representations we've currently suppressed the generation of the related links, since they triple the representation sizes and thus substantially affect serialization/deserialization, memory usage, and bandwidth. Our other efficiency concern is that with pure HATEOAS, client programs will be making several times the number of HTTP requests as they browse down from the well-known link to the information they need. So it seems best, from an efficiency standpoint, if clients have the knowledge of the links encoded in them.

But doing that means the client must do a lot of string concatenation to form the URIs,
which is error prone and makes it hard to rearrange the resource name space. Therefore we use a templating system where the client code selects a template and asks it to expand itself from a parameter object. This is a type of form-filling.

I'm really eager to see what others have experienced on this. HATEOAS seems like a good idea aside from the performance aspects.

Edit: Our templates are part of a Java client library we wrote on top of the Restlet framework. The client library handles all details of HTTP requests/responses, HTTP headers, deserialization/serialization, GZIP encoding, etc. This makes the actual client code quite concise, and helps to insulate it from some server side changes.

Roy Fielding's blog entry about HATEOAS has a very relevant and interesting discussion following it.

疾风者 2024-08-19 18:55:31

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

So far I have built two clients that access REST services. Both use HATEOAS exclusively. I have had a huge amount of success being able to update server functionality without updating the client.

I use xml:base to enable relative urls to reduce the noise in my xml documents. Other than loading images, and other static data I usually only follow links on user requests so the performance overhead of links is not significant for me.

On the clients, the only common functionality that I have felt the need to create is wrappers around my media types and a class to manage links.


Update:

There seem to be two distinct ways to deal with REST interfaces from the client's perspective. The first is where the client knows what information it wants to get and knows the links it needs to traverse to get to that information. The second approach is useful when there is a human user of the client application controlling which links to follow and the client may not know in advance what media type will be returned from the server. For entertainment value, I call these two types of client, the data miner and the dispatcher, respectively.

The Data Miner

For example, imagine for a moment that the Twitter API was actually RESTful and I wanted write a client that would retreive most recent status message of the most recent follower of a particular twitter user.

Assuming I was using the awesome new Microsoft.Http.HttpClient library, and I had written a few "ReadAs" extension methods to parse the XML coming from the twitter API, I imagine it would go something like this:

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; 

The Dispatcher

To better illustrate this example imagine you were implementing a client that rendered genealogy information. The client needs to be capable of showing the tree, drilling down to information about a particular person and viewing related images. Consider the following code snippet:

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

        }

The client application can use a completely generic mechanism to follow links and pass the response to this dispatching method. From here the switch statement passes control to a specific controller class that knows how to interpret and render the information based on the media type.

Obviously there are many more pieces to the client application, but these are the ones that correspond to HATEOAS. Feel free to ask me to clarify any points as I have skimmed over many details.

忘年祭陌 2024-08-19 18:55:31

诺基亚的 地点 API(网络存档 snapshot) 是 RESTful 的,并在整个过程中使用超媒体。其文档中也明确指出不鼓励使用 URI 模板/硬编码:

超媒体链接的使用

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

Nokia's Places API (web archive snapshot) is RESTful and uses hypermedia throughout. It is also clear in its documentation to discourage the use of URI templating/hardcoding:

Usage of Hypermedia Links

Your application must use the hypermedia links exposed in the JSON
responses for subsequent requests within a task flow, instead of
trying to construct a URI for the next steps via URI templates. By
using URI templates, your request would not include critical
information that is required to create the response for the next
request.

南渊 2024-08-19 18:55:31

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

Jim, I also was a little frustrated with the lack of examples with a RESTful client following HATEOAS, so I wrote blog post showing a proper HATEOAS example for creating and placing an order. There are surprisingly few examples of doing this through an API and I found it a touch confusing, but here is the link:
API Example Using Rest. Let me know what you think and what you think I did wrong.

陌生 2024-08-19 18:55:31

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

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 的子级以找出需要传递哪些参数,但是 这必须等到我有另外两个需要遵循的参数化表单

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

I've written two HATEOAS clients, once in Java and once in Ruby and I share your frustration. On both occasions there was a complete lack of lack of tooling support for what I was doing. For example, the REST API I was using would tell me what HTTP method to use for each hypertext control, but HttpClient doesn't let you pass in the method, so I ended up with the following ugly code (BTW all the code lives within a custom Ant task, hence the 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;
}

This ended up being the basis for a REST client utility methods that I use.

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

With this little bit of code, I can fairly easily write clients that will traverse the hypermedia controls in the documents that are returned. The main bit that is missing is support for form parameters. Fortunately for me all of the controls I'm using are parameterless except one (I follow the rule of three in regards to refactoring). For completeness here is what that code snippet looks like:

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

Now, what is should be doing is looking at the children of /license/update to figure out what parameters need to be passed, but that will have to wait until I have two more parameterised form that I need to follow.

BTW it after all of the effort, it has been extremely satisfying and easy to modify the server without impacting the client. It felt so good that I'm surprised it isn't outlawed in some states.

不顾 2024-08-19 18:55:31

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

在我看来,这个问题确实没有意义。

Your web browser of choice is a "pure HATEOAS" client to the entire WWW.

The question doesn't really make sense imo.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文