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

有人知道遵循HATEOAS原则的RESTful客户端的例子吗?,rest,client,web-services,hateoas,Rest,Client,Web Services,Hateoas,因此,到现在为止,我已经明白了我们都应该实现我们的RESTful服务,提供使客户能够遵循该原则的表示。虽然从理论上讲这一切都很有意义,但我一直在浏览网页,以找到一个很好的例子,说明一些客户机代码严格遵循了这一理念 我读得越多,我就越觉得这是一场学术讨论,因为实际上没有人这么做!人们可以尽情抱怨WS-*堆栈的许多缺陷,但至少很清楚如何编写客户机:您可以解析WSDL并生成代码 现在我明白了,对于一个好的RESTful服务来说,这是不必要的:您只需要知道所涉及的关系和表示,并且应该能够动态地对这些关系

因此,到现在为止,我已经明白了我们都应该实现我们的RESTful服务,提供使客户能够遵循该原则的表示。虽然从理论上讲这一切都很有意义,但我一直在浏览网页,以找到一个很好的例子,说明一些客户机代码严格遵循了这一理念

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

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

这些只是我的一些半生不熟的想法,但我只是担心如果我现在就开始编写一个正确的RESTful API,没有人能够真正使用它!或者至少使用它将是一个巨大的后遗症,因为人们将不得不额外编写胶水代码来解释我提供的关系和表示

有人能从客户的角度解释一下吗?有人能展示一个正确的动态/反应式RESTful客户机代码的示例,这样我就可以了解我实际为之写作的读者了吗?(最好是一个提供一些抽象的客户端API示例)否则它就完全是理论性的了


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

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

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

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

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

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


Roy Fielding的讨论非常相关和有趣。

到目前为止,我已经构建了两个访问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;
            }

        }
客户端应用程序可以使用完全通用的
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;
}
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();
    }