Spring RESTサービスのHATEOAS

Spring RESTサービスのHATEOAS

1. 概要

この記事では、implementation of discoverability in a Spring REST ServiceとHATEOAS制約を満たすことに焦点を当てます。

2. イベントによる発見可能性の分離

HTTPリクエストを処理するDiscoverability as a separate aspect or concern of the web layer should be decoupled from the controller。 この目的のために、コントローラーは、応答の追加操作を必要とするすべてのアクションのイベントを起動します。

まず、イベントを作成しましょう。

public class SingleResourceRetrieved extends ApplicationEvent {
    private HttpServletResponse response;

    public SingleResourceRetrieved(Object source, HttpServletResponse response) {
        super(source);

        this.response = response;
    }

    public HttpServletResponse getResponse() {
        return response;
    }
}
public class ResourceCreated extends ApplicationEvent {
    private HttpServletResponse response;
    private long idOfNewResource;

    public ResourceCreated(Object source,
      HttpServletResponse response, long idOfNewResource) {
        super(source);

        this.response = response;
        this.idOfNewResource = idOfNewResource;
    }

    public HttpServletResponse getResponse() {
        return response;
    }
    public long getIdOfNewResource() {
        return idOfNewResource;
    }
}

次に、the Controller, with 2 simple operations – find by id and create:

@RestController
@RequestMapping(value = "/foos")
public class FooController {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private IFooService service;

    @GetMapping(value = "foos/{id}")
    public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
        Foo resourceById = Preconditions.checkNotNull(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrieved(this, response));
        return resourceById;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void create(@RequestBody Foo resource, HttpServletResponse response) {
        Preconditions.checkNotNull(resource);
        Long newId = service.create(resource).getId();

        eventPublisher.publishEvent(new ResourceCreated(this, response, newId));
    }
}

We can then handle these events with any number of decoupled listeners.これらはそれぞれ、独自の特定のケースに焦点を当て、全体的なHATEOAS制約を満たすのに役立ちます。

リスナーは呼び出しスタックの最後のオブジェクトである必要があり、リスナーに直接アクセスする必要はありません。そのため、それらは公開されていません。

3. 新しく作成されたリソースのURIを検出可能にする

応答のprevious post on HATEOASthe operation of creating a new Resource should return the URI of that resource in the Location HTTP headerで説明したように。

リスナーを使用してこれを処理します。

@Component
class ResourceCreatedDiscoverabilityListener
  implements ApplicationListener{

    @Override
    public void onApplicationEvent(ResourceCreated resourceCreatedEvent){
       Preconditions.checkNotNull(resourceCreatedEvent);

       HttpServletResponse response = resourceCreatedEvent.getResponse();
       long idOfNewResource = resourceCreatedEvent.getIdOfNewResource();

       addLinkHeaderOnResourceCreation(response, idOfNewResource);
   }
   void addLinkHeaderOnResourceCreation
     (HttpServletResponse response, long idOfNewResource){
       URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri().
         path("/{idOfNewResource}").buildAndExpand(idOfNewResource).toUri();
       response.setHeader("Location", uri.toASCIIString());
    }
}

この例では、we’re making use of the ServletUriComponentsBuilder –現在のリクエストの使用に役立ちます。 このように、何も渡す必要がなく、静的にアクセスするだけです。

APIがResponseEntityを返す場合–Location supportを使用することもできます。

4. 単一のリソースの取得

単一のリソースを取得すると、そのタイプのthe client should be able to discover the URI to get all Resourcesは次のようになります。

@Component
class SingleResourceRetrievedDiscoverabilityListener
 implements ApplicationListener{

    @Override
    public void onApplicationEvent(SingleResourceRetrieved resourceRetrievedEvent){
        Preconditions.checkNotNull(resourceRetrievedEvent);

        HttpServletResponse response = resourceRetrievedEvent.getResponse();
        addLinkHeaderOnSingleResourceRetrieval(request, response);
    }
    void addLinkHeaderOnSingleResourceRetrieval(HttpServletResponse response){
        String requestURL = ServletUriComponentsBuilder.fromCurrentRequestUri().
          build().toUri().toASCIIString();
        int positionOfLastSlash = requestURL.lastIndexOf("/");
        String uriForResourceCreation = requestURL.substring(0, positionOfLastSlash);

        String linkHeaderValue = LinkUtil
          .createLinkHeader(uriForResourceCreation, "collection");
        response.addHeader(LINK_HEADER, linkHeaderValue);
    }
}

リンクリレーションのセマンティクスは、several microformatsで指定および使用されているが、まだ標準化されていない“collection”リレーションタイプを使用することに注意してください。

The Link header is one of the most used HTTP headersfor the purposes of discoverability.このヘッダーを作成するユーティリティは非常に単純です。

public class LinkUtil {
    public static String createLinkHeader(String uri, String rel) {
        return "<" + uri + ">; rel="" + rel + """;
    }
}

5. ルートでの発見可能性

ルートは、サービス全体のエントリポイントです。これは、APIを初めて使用するときにクライアントが接触するものです。

全体でHATEOAS制約を考慮して実装する場合は、ここから開始します。 したがって、all the main URIs of the system have to be discoverable from the root.

このためのコントローラーを見てみましょう。

@GetMapping("/")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void adminRoot(final HttpServletRequest request, final HttpServletResponse response) {
    String rootUri = request.getRequestURL().toString();

    URI fooUri = new UriTemplate("{rootUri}{resource}").expand(rootUri, "foos");
    String linkToFoos = LinkUtil.createLinkHeader(fooUri.toASCIIString(), "collection");
    response.addHeader("Link", linkToFoos);
}

もちろん、これは、Fooリソースの単一のサンプルURIに焦点を当てた概念の図解です。 実際の実装では、同様に、クライアントに公開されたすべてのリソースのURIを追加する必要があります。

5.1. 発見可能性はURIの変更ではありません

これは議論の余地がある点です。一方で、HATEOASの目的は、クライアントにAPIのURIを検出させ、ハードコードされた値に依存しないようにすることです。 一方、これはWebの仕組みではありません。はい、URIは検出されますが、ブックマークもされます。

微妙だが重要な違いはAPIの進化です。古いURIは引き続き機能しますが、APIを検出するクライアントは新しいURIを検出する必要があります。これにより、APIが動的に変更され、 APIの変更。

結論– RESTful WebサービスのすべてのURIをchttps://www.w3.org/TR/cooluris/ [ool]URIs(およびクールなURIdon’t change)と見なす必要があるという理由だけで)–これは、APIを進化させるときにHATEOAS制約を順守することが非常に役に立たないという意味ではありません。

6. 発見可能性の警告

以前の記事に関するいくつかの議論が述べているように、the first goal of discoverability is to make minimal or no use of documentationは、クライアントに、取得した応答を介してAPIの使用方法を学習および理解させます。

実際、これは、ドキュメントなしで、これほどまでにフェッチされた理想と見なされるべきではありません。つまり、すべての新しいWebページを消費する方法です。 したがって、RESTのコンテキストで概念がより問題がある場合、それが可能かどうかの問題ではなく、技術的な実装の問題でなければなりません。

技術的に言えば、私たちはまだ完全に機能するソリューションとはほど遠い状態です。仕様とフレームワークのサポートはまだ進化しているため、いくつかの妥協が必要です。

7. 結論

この記事では、Spring MVCを使用したRESTfulサービスのコンテキストでの発見可能性の特徴の実装について説明し、ルートでの発見可能性の概念に触れました。

これらすべての例とコードスニペットの実装はmy GitHub projectにあります。これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。