1. 概要

この記事では、 Spring REST Service での検出可能性の実装と、HATEOAS制約を満たすことに焦点を当てます。

この記事では、SpringMVCに焦点を当てています。 私たちの記事SpringHATEOASの紹介では、SpringBootでHATEOASを使用する方法について説明しています。

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

Webレイヤーの個別の側面または懸念事項としての検出可能性は、HTTP要求を処理するコントローラーから切り離す必要があります。 この目的のために、コントローラーは、応答の追加操作を必要とするすべてのアクションのイベントを発生させます。

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

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

次に、コントローラー。2つの簡単な操作で– idで検索して作成します:

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

その後、これらのイベントを任意の数の分離されたリスナーで処理できます。 これらはそれぞれ、独自の特定のケースに焦点を当て、全体的なHATEOAS制約を満たすのに役立ちます。

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

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

HATEOAS に関する以前の投稿で説明したように、新しいリソースを作成する操作は、応答のLocationHTTPヘッダーでそのリソースのURIを返す必要があります。

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

@Component
class ResourceCreatedDiscoverabilityListener
  implements ApplicationListener<ResourceCreated>{

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

この例では、 ServletUriComponentsBuilder を使用しています。これは、現在のリクエストの使用に役立ちます。 このように、何も渡す必要はなく、静的にアクセスするだけです。

APIがResponseEntityを返す場合、 Locationsupportを使用することもできます。

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

単一のリソースを取得すると、クライアントはURIを検出して、そのタイプのすべてのリソースを取得できる必要があります。

@Component
class SingleResourceRetrievedDiscoverabilityListener
 implements ApplicationListener<SingleResourceRetrieved>{

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

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

リンクヘッダーは、検出可能性の目的で最もよく使用されるHTTPヘッダーの1つです このヘッダーを作成するユーティリティは、次のように簡単です。

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

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

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

HATEOAS制約を考慮して全体を通して実装する場合は、ここから開始します。 したがって、システムのすべてのメインURIは、ルートから検出可能である必要があります。

これについては、コントローラーを見てみましょう。

@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の変更。

結論として、RESTfulWebサービスのすべてのURIをc ool URI と見なす必要があるという理由だけで(そしてクールなURIは変更しない )–これは、APIを進化させるときにHATEOAS制約を順守することが非常に役に立たないという意味ではありません。

6. 発見可能性の警告

以前の記事に関するいくつかの議論で述べられているように、発見可能性の最初の目標は、ドキュメントを最小限に抑えるか、まったく使用しないことであり、クライアントが取得した応答を介してAPIの使用方法を学習して理解することです。

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

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

7. 結論

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

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