RESTEasy Reactive を使用して REST サービスを作成する
このガイドでは、QuarkusでRESTEasy Reactiveを使用してRESTサービスを記述する方法を説明します。
|
本書は、RESTEasy Reactiveのリファレンスガイドです。よりライトな紹介は、 Writing JSON REST servicesのガイド を参照してください。 |
エンドポイントの作成
はじめに
次のインポートをビルドファイルに追加します:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
implementation("io.quarkus:quarkus-resteasy-reactive")
これで、 org.acme.rest.Endpoint クラスで最初のエンドポイントを書くことができます:
package org.acme.rest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("")
public class Endpoint {
@GET
public String hello() {
return "Hello, World!";
}
}
用語解説
- REST
- エンドポイント
-
REST 呼び出しを処理するために呼び出される Java メソッド
- URL / URI (Uniform Resource Locator / Identifier)
-
REST リソースの場所を特定するために使用 (仕様)
- リソース
-
ドメインオブジェクトを表します。これは、API が提供および変更するものです。JAX-RS ではエンティティーとも呼ばれます。
- 表現
-
通信上でのリソースの表現方法は、Content typeによって異なります。
- Content type
-
text/plain や application/json などの特定の表現 (メディアタイプとも呼ばれます) を指定します
- HTTP
-
REST 呼び出しをルーティングするための基本的なワイヤープロトコル (HTTP 仕様 を参照)
- HTTP リクエスト
-
HTTP メソッド、ターゲット URI、ヘッダー、およびオプションのメッセージ本文で設定される HTTP 呼び出しのリクエスト部分
- HTTP レスポンス
-
HTTP レスポンスステータス、ヘッダー、およびオプションのメッセージ本文で設定される HTTP 呼び出しのレスポンス部分
エンドポイントの宣言:URI マッピング
@Path アノテーションを持つクラスは、HTTP メソッドアノテーション (下記参照) を持っていれば、そのメソッドを REST エンドポイントとして公開することができます。
@Path アノテーションは、これらのメソッドが公開される URI の接頭辞を定義します。これは空であるか、あるいは rest や rest/V1 のような接頭辞を含むことができます。
公開された各エンドポイントメソッドは、別の @Path アノテーションを持つことができ、それを含むクラスアノテーションを追加されます。例えば、これは rest/hello というエンドポイントを定義しています:
package org.acme.rest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("rest")
public class Endpoint {
@Path("hello")
@GET
public String hello() {
return "Hello, World!";
}
}
URI マッピングの詳細は、URI parameters を参照してください。
以下に示すように、 @ApplicationPath アノテーションを使用して、すべての REST エンドポイントのルートパスを設定できます。
package org.acme.rest;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/api")
public static class MyApplication extends Application {
}
これにより、すべての rest エンドポイントは /api に対する相対パスで解決されるようになり、上記の @Path ("rest") のエンドポイントには /api/rest/ でアクセスすることができるようになります。また、アノテーションを使用したくない場合は、 quarkus.resteasy-reactive.path ビルド時のプロパティーを設定して、ルートパスを設定することができます。
エンドポイントの宣言: HTTP メソッド
各エンドポイントメソッドには、次のいずれかのアノテーションを付ける必要があります。これにより、どの HTTP メソッドがそのメソッドにマップされるかが定義されます。
| アノテーション | 使用方法 |
|---|---|
本文のない |
|
リソースを作成し、そのリソースへのリンクを取得します (HTTP docs) |
|
@HttpMethod アノテーションを付与したアノテーションを宣言することにより、他の HTTP メソッドを宣言することもできます。
package org.acme.rest;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path;
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("CHEESE")
@interface CHEESE {
}
@Path("")
public class Endpoint {
@CHEESE
public String hello() {
return "Hello, Cheese World!";
}
}
エンドポイントの宣言: 表現/コンテンツタイプ
各エンドポイントメソッドは、特定のリソース表現を消費したり生成したりすることがあり、それは HTTP Content-Type header で示され、その中に次のような MIME (Media Type) 値が含まれています。
-
Stringを返すエンドポイントのデフォルトであるtext/plain。 -
HTML の
text/html(Qute テンプレート と同様) -
JSON REST endpoint の
application/json -
任意のテキストメディアタイプのサブタイプワイルドカードである
text/* -
任意のメディアタイプのワイルドカードである
*/*
@Produces または @Consumes アノテーションで、エンドポイントクラスをアノテーションできます。この場合、エンドポイントが HTTP リクエストボディとして受け入れ、HTTP レスポンスボディとして生成できるメディアタイプを一つまたは複数指定することが可能です。これらのクラスアノテーションは、各メソッドに適用されます。
MediaType クラスには、特定の事前定義されたメディアタイプを指すために使用できる多くの定数があります。
詳細は、ネゴシエーション を参照してください。
リクエストパラメーターへのアクセス
パラメーター名の情報を生成するために、 -parameters (javac) または <parameters> または <maven.compiler.parameters> (Maven) を使ってコンパイラーを設定することを忘れないでください。
|
次の HTTP リクエスト要素は、エンドポイントメソッドによって取得される可能性があります。
| HTTP 要素 | アノテーション | 使用方法 |
|---|---|---|
リンク:https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-common/2.16.12.Final/org/jboss/resteasy/reactive/RestPath.html[ |
URI テンプレートパラメーター (URI Template specification の簡易バージョン)。 詳細は、URI パラメーター を参照してください。 |
|
クエリーパラメーター |
||
ヘッダ |
HTTP header の値 |
|
Cookie |
HTTP cookie の値 |
|
フォームパラメーター |
||
マトリックスパラメーター |
これらのアノテーションごとに、参照する要素の名前を指定できます。指定しない場合は、アノテーションが付けられたメソッドパラメーターの名前が使用されます。
クライアントが次の HTTP 呼び出しを行った場合:
POST /cheeses;variant=goat/tomme?age=matured HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: level=hardcore
X-Cheese-Secret-Handshake: fist-bump
smell=strong
次に、このエンドポイントメソッドを使用してさまざまなパラメーターをすべて取得できます。
package org.acme.rest;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import org.jboss.resteasy.reactive.RestCookie;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestHeader;
import org.jboss.resteasy.reactive.RestMatrix;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;
@Path("/cheeses/{type}")
public class Endpoint {
@POST
public String allParams(@RestPath String type,
@RestMatrix String variant,
@RestQuery String age,
@RestCookie String level,
@RestHeader("X-Cheese-Secret-Handshake")
String secretHandshake,
@RestForm String smell) {
return type + "/" + variant + "/" + age + "/" + level + "/"
+ secretHandshake + "/" + smell;
}
}
JAX-RS のアノテーションも使用できます。@PathParam、@QueryParam、@HeaderParam、@CookieParam、@FormParam または @MatrixParam。しかし、パラメーター名の指定が必要です。
より詳細なユースケースは、 パラメータマッピング を参照してください。
|
RESTEasy Reactive リクエストパラメータ処理コードで例外が発生した場合、例外はデフォルトでログに出力されません(セキュリティ上の理由による)。 このため、特定の HTTP ステータスコードが返される理由を理解するのが難しい場合があります(JAX-RS では、さまざまなケースで直感的でないエラーコードの使用が義務付けられているため)。 このような場合、ユーザーは次のようにして
|
カスタムクラスでパラメータをグループ化
リクエストパラメータをメソッドパラメータとしてエンドポイントに宣言する代わりに、コンテナクラスでまとめることができます。したがって、前の例を次のように書き換えることができます。
package org.acme.rest;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import org.jboss.resteasy.reactive.RestCookie;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestHeader;
import org.jboss.resteasy.reactive.RestMatrix;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;
@Path("/cheeses/{type}")
public class Endpoint {
public static class Parameters {
@RestPath
String type;
@RestMatrix
String variant;
@RestQuery
String age;
@RestCookie
String level;
@RestHeader("X-Cheese-Secret-Handshake")
String secretHandshake;
@RestForm
String smell;
}
@POST
public String allParams(@BeanParam Parameters parameters) { (1)
return parameters.type + "/" + parameters.variant + "/" + parameters.age
+ "/" + parameters.level + "/" + parameters.secretHandshake
+ "/" + parameters.smell;
}
}
| 1 | BeanParam is required to comply with the JAX-RS specification so that libraries like OpenAPI can introspect the parameters. |
URI パラメーターの宣言
URI パラメーターを宣言し、パスで正規表現を使用できるため、たとえば、次のエンドポイントは /hello/stef/23 と /hello のリクエストを処理しますが、 /hello/stef/0x23 は処理しません。
package org.acme.rest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("hello")
public class Endpoint {
@Path("{name}/{age:\\d+}")
@GET
public String personalisedHello(String name, int age) {
return "Hello " + name + " is your age really " + age + "?";
}
@GET
public String genericHello() {
return "Hello stranger";
}
}
リクエスト本文へのアクセス
アノテーションのないメソッドパラメーターは、HTTP 表現からパラメーターの Java タイプにマッピングされた後、それが URI template parameter または context object でない限り、メソッド本文の脚注を受け取ります。
次のパラメータータイプは、そのままでサポートされます。
| タイプ | 使用方法 |
|---|---|
一時ファイル内のリクエスト本文全体 |
|
|
デコードされていないリクエスト本文全体 |
|
デコードされたリクエスト本文全体 |
デコードされたリクエスト本文全体 |
|
ブロッキングストリームのリクエスト本文 |
|
ブロッキングストリームのリクエスト本文 |
|
すべての Java プリミティブとそのラッパークラス |
Java プリミティブ型 |
大きな整数と小数。 |
|
JSON 値の型 |
|
Vert.x Buffer |
|
他の型 |
| さらにbody parameter types のサポートを追加することができます。 |
マルチパートフォームデータの処理
コンテンツタイプとして multipart/form-data を持つ HTTP リクエストを処理するために、通常の @RestForm というアノテーションを使うことが出来ますが、パーツをファイルとして、あるいはエンティティとしてアクセスできる特別な型があります。その使用例を見てみましょう。
ファイルアップロード、JSONエンティティ、文字列の説明を含むフォーム値を含むHTTPリクエストを想定すると、次のようなエンドポイントを書くことができます:
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.multipart.FileUpload;
@Path("multipart")
public class MultipartResource {
public static class Person {
public String firstName;
public String lastName;
}
@POST
public void multipart(@RestForm String description,
@RestForm("image") FileUpload file,
@RestForm @PartType(MediaType.APPLICATION_JSON) Person person) {
// do something
}
}
description パラメーターには、HTTP リクエストの description という部分に含まれるデータが格納されます(@RestForm が値を定義していない場合は、フィールド名が使用されるため)。一方、 file パラメータは、HTTP リクエストの image 部分にアップロードされたファイルに関するデータを含み、 person パラメーターは、 JSON ボディーリーダー を使用して Person エンティティーを読み込みます。
マルチパートリクエストの各パートのサイズは、 quarkus.http.limits.max-form-attribute-size の値に準拠する必要があります。デフォルトは 2048 バイトです。この設定を超えるパーツサイズのリクエストは、HTTP ステータスコード 413 になります。
FileUpload はアップロードされたファイルの様々なメタデータにアクセスすることができます。しかし、アップロードされたファイルへの処理が必要なだけなら、 java.nio.file.Path や java.io.File を使用することができます。
|
名前に関係なく、アップロードされたすべてのファイルのすべてのパーツにアクセスする必要がある場合は、 @RestForm(FileUpload.ALL) List<FileUpload> で実現出来ます。
@PartType is used to aid
in deserialization of the corresponding part of the request into the desired Java type. It is only required if
you need to use a special body parameter type for that particular parameter.
|
| 他のリクエストパラメータータイプと同様に、これらを container class にまとめるることもできます。 |
ファイルのアップロードを処理する場合、POJO を処理するコード内でファイルを永続的なストレージ (データベース、専用ファイルシステム、クラウドストレージなど) に移動させることが非常に重要です。そうしないと、リクエストが終了したときに、ファイルにアクセスできなくなります。さらに、 quarkus.http.body.delete-uploaded-files-on-end を true に設定すると、HTTP レスポンスの送信時に、アップロードされたファイルが削除されます。この設定を無効にすると、ファイルはサーバーのファイルシステム上 (quarkus.http.body.uploads-directory 設定オプションで定義したディレクトリー) に存在しますが、アップロードされたファイルは UUID ファイル名で保存されて追加のメタデータが保存されないため、これらのファイルは本質的にファイルのランダムダンプとなります。
|
同様に、RESTEasy Reactive は、ユーザーがサーバーからファイルをダウンロードできるようにするための Multipart Form データを生成することができます。例えば、公開したい情報を保持する POJO を、次のように書くことができます。
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
public class DownloadFormData {
@RestForm
String name;
@RestForm
@PartType(MediaType.APPLICATION_OCTET_STREAM)
File file;
}
そして、次のようなリソースを介してこの POJO を公開します。
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("multipart")
public class Endpoint {
@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("file")
public DownloadFormData getFile() {
// return something
}
}
さらに、クラス MultipartFormDataOutput を使用して、フォームのパーツを手動で追加することも可能です:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.server.core.multipart.MultipartFormDataOutput;
@Path("multipart")
public class Endpoint {
@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("file")
public MultipartFormDataOutput getFile() {
MultipartFormDataOutput form = new MultipartFormDataOutput();
form.addFormData("person", new Person("John"), MediaType.APPLICATION_JSON_TYPE);
form.addFormData("status", "a status", MediaType.TEXT_PLAIN_TYPE)
.getHeaders().putSingle("extra-header", "extra-value");
return form;
}
}
この最後の方法は、出力のパーツに追加のヘッダーを追加することができます。
| 当面、マルチパートデータを返すことはエンドポイントをブロックすることに限定されます。 |
不正な入力の処理
マルチパートボディの読み取りの一環として、RESTEasy Reactive はリクエストの各部分に対して適切な MessageBodyReader を呼び出します。これらのパーツのいずれかで IOException が発生した場合 (たとえば、Jackson が JSON パーツをデシリアライズできなかった場合)、 org.jboss.resteasy.reactive.server.multipart.MultipartPartReadingException が出力されます。 例外のマッピング で説明したように、この例外がアプリケーションによって処理されない場合、HTTP 400 応答がデフォルトで返されます。
レスポンス本文を返す
HTTP レスポンスを返すには、単にメソッドから必要なリソースを返します。メソッドの戻り値の型とオプションのコンテントタイプをもとに、HTTP レスポンスへのシリアライズ方法を決定します (より詳細な情報については ネゴシエーション を参照ください)。
HTTP レスポンス から読み取れる、あらかじめ定義された任意の型を返すことができ、それ以外の型は型からJSON にマッピングされます。
さらに、次の戻り値の型もサポートされています。
| タイプ | 使用方法 |
|---|---|
指定されたパスで指定されたファイルの内容 |
|
指定されたパスで指定されたファイルの部分的な内容 |
|
ファイルの部分的な内容 |
|
Vert.x AsyncFile (完全または部分的) |
あるいは、 Uni 、Multi または CompletionStage など、前述の戻り値の型に解決して、リアクティブ型 を返すことも可能です。
他のレスポンスプロパティーの設定
手動でレスポンスを設定する
ステータスコードやヘッダーなど、本文だけでなく HTTP レスポンスに多くのプロパティーを設定する必要がある場合は、リソースメソッドからメソッドに org.jboss.resteasy.reactive.RestResponse を返すようにすることができます。この例は次のようになります。
package org.acme.rest;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NewCookie;
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.RestResponse.ResponseBuilder;
@Path("")
public class Endpoint {
@GET
public RestResponse<String> hello() {
// HTTP OK status with text/plain content type
return ResponseBuilder.ok("Hello, World!", MediaType.TEXT_PLAIN_TYPE)
// set a response header
.header("X-Cheese", "Camembert")
// set the Expires response header to two days from now
.expires(Date.from(Instant.now().plus(Duration.ofDays(2))))
// send a new cookie
.cookie(new NewCookie("Flavour", "chocolate"))
// end of builder API
.build();
}
}
また、JAX-RS タイプの レスポンス を使用することもできますが、エンティティーに強く型付けされるわけではありません。
|
アノテーションの使用
または、静的な値でステータスコードや HTTP ヘッダーのみを設定する必要がある場合は、それぞれ @org.jboss.resteasy.reactive.ResponseStatus および/または ResponseHeader を使用できます。この例は次のようになります。
package org.acme.rest;
import org.jboss.resteasy.reactive.Header;
import org.jboss.resteasy.reactive.ResponseHeaders;
import org.jboss.resteasy.reactive.ResponseStatus;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("")
public class Endpoint {
@ResponseStatus(201)
@ResponseHeader(name = "X-Cheese", value = "Camembert")
@GET
public String hello() {
return "Hello, World!";
}
}
リダイレクトサポート
@POST 、 @PUT 、 @DELETE のエンドポイントを処理する場合、アクションの実行後に @GET のエンドポイントにリダイレクトするのが一般的で、ユーザーが2回目のアクションをトリガーせずにページを再読み込みできるようにします。これを実現するには、複数の方法があります。
RestResponseの使用
RestResponse を戻り値の型として使用し、適切なリダイレクトURIを作成することは、以下の例のように可能です:
package org.acme.rest;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.reactive.RestResponse;
@Path("/fruits")
public class FruitResource {
public static class Fruit {
public Long id;
public String name;
public String description;
public Fruit() {
}
public Fruit(Long id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
}
private final Map<Long, Fruit> fruits = new ConcurrentHashMap<>();
private final AtomicLong ids = new AtomicLong(0);
public FruitResource() {
Fruit apple = new Fruit(ids.incrementAndGet(), "Apple", "Winter fruit");
fruits.put(apple.id, apple);
Fruit pinneapple = new Fruit(ids.incrementAndGet(), "Pineapple", "Tropical fruit");
fruits.put(pinneapple.id, pinneapple);
}
// when invoked, this method will result in an HTTP redirect to the GET method that obtains the fruit by id
@POST
public RestResponse<Fruit> add(Fruit fruit, @Context UriInfo uriInfo) {
fruit.id = ids.incrementAndGet();
fruits.put(fruit.id, fruit);
// seeOther results in an HTTP 303 response with the Location header set to the value of the URI
return RestResponse.seeOther(uriInfo.getAbsolutePathBuilder().path(Long.toString(fruit.id)).build());
}
@GET
@Path("{id}")
public Fruit byId(Long id) {
return fruits.get(id);
}
}
非同期/リアクティブサポート
エンドポイントメソッドがレスポンスする前に非同期またはリアクティブタスクを実行する必要がある場合は、メソッドを宣言して( Mutiny の) Uni 型を返すことができます 。この場合、現在の HTTP リクエストはメソッドの後で、返されたUni インスタンスが値に解決され、前述のルールに従って正確にレスポンスにマッピングされるまで自動的に一時停止されます。
package org.acme.rest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import io.smallrye.mutiny.Uni;
@Path("escoffier")
public class Endpoint {
@GET
public Uni<Book> culinaryGuide() {
return Book.findByIsbn("978-2081229297");
}
}
これにより、book がデータベースから取得されている間、イベントループスレッドをブロックせず、book がクライアントに送信できるようになるまで Quarkus がより多くのリクエストに対応し、このリクエストを終了させることができます。詳しくは、実行モデルドキュメント を参照してください。
CompletionStage 戻り値の型もサポートされています。
ストリーミングサポート
レスポンスを要素ごとにストリーミングする場合は、エンドポイントメソッドに( Mutiny の) Multi 型を返すようにすることができます。これは、テキストまたはバイナリーデータのストリーミングに特に役立ちます。
この例では、 Reactive Messaging HTTP を使用して、テキストデータをストリーミングする方法を示しています。
package org.acme.rest;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import org.eclipse.microprofile.reactive.messaging.Channel;
import io.smallrye.mutiny.Multi;
@Path("logs")
public class Endpoint {
@Inject
@Channel("log-out")
Multi<String> logs;
@GET
public Multi<String> streamLogs() {
return logs;
}
}
| レスポンスフィルターは、ストリーミングされたレスポンスでは呼び出されません。これは、ヘッダーまたは HTTP ステータスコードを設定できるという誤った印象を与えるためです。これは、最初のレスポンスの後では当てはまりません。レスポンスの一部がすでに書き込まれている可能性があるため、例外マッパーも呼び出されません。 |
サーバー送信イベント (SSE) のサポート
レスポンスで JSON オブジェクトをストリーミングする場合は、エンドポイントメソッドに @Produces(MediaType.SERVER_SENT_EVENTS) でアノテーションを付けるだけで、 Server-Sent Events を使用できます。そして指定した場合、 @RestStreamElementType (MediaType.APPLICATION_JSON) を使用し各要素は JSONにシリアライズ されるべきです。
package org.acme.rest;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestStreamElementType;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.reactive.messaging.annotations.Channel;
@Path("escoffier")
public class Endpoint {
// Inject our Book channel
@Inject
@Channel("book-out")
Multi<Book> books;
@GET
// Each element will be sent as JSON
@RestStreamElementType(MediaType.APPLICATION_JSON)
// by using @RestStreamElementType, we don't need to add @Produces(MediaType.SERVER_SENT_EVENTS)
public Multi<Book> stream() {
return books;
}
}
HTTP キャッシング機能の制御
RESTEasy Reactive は、HTTP キャッシングセマンティックスの処理、つまり 'Cache-Control' HTTP ヘッダーの設定を容易にするために、@Cache と @NoCache というアノテーションを提供します。
これらのアノテーションはリソースメソッドまたはリソースクラス (この場合、同じアノテーションを 含まない クラスのすべてのリソースメソッドに適用されます) に配置することができ、ユーザーがドメインオブジェクトを返すことができ、明示的に Cache-Control HTTP ヘッダーを構築することに対処する必要はありません。
@Cache が複雑な Cache-Control ヘッダーを構築するのに対し、@NoCache は何もキャッシュさせたくない、つまり Cache-Control: nocache というシンプルな表記法になっています。
Cache-Control ヘッダーの詳細は、 RFC 7234 を参照してください
|
コンテキストオブジェクトへのアクセス
エンドポイントメソッドが次のタイプのパラメーターを受け取る場合、フレームワークが提供するコンテキストオブジェクトは多数あります。
| タイプ | 使用方法 |
|---|---|
すべてのリクエストヘッダー |
|
現在のエンドポイントメソッドとクラスに関する情報 (リフレクションが必要) |
|
現在のユーザーとロールへのアクセス |
|
現在のエンドポイントメソッドとクラスに関する情報 (反映は不要) |
|
現在のエンドポイントとアプリケーション URI に関する情報を提供します |
|
詳細: 現在の JAX-RS アプリケーションクラス |
|
詳細: デプロイされた JAX-RS アプリケーションに関する設定 |
|
詳細: JAX-RS プロバイダーへのランタイムアクセス |
|
詳細: 現在の HTTP メソッドへのアクセスと 条件 |
|
詳細: エンドポイントのインスタンスへのアクセス |
|
詳細:RESTEasy 現在の要求/レスポンスへのリアクティブアクセス |
|
詳細: 複雑な SSE のユースケース |
|
詳細: Vert.xHTTP リクエスト |
|
詳細: Vert..x HTTP レスポンス |
たとえば、現在ログインしているユーザーの名前を返す方法は次のとおりです。
package org.acme.rest;
import java.security.Principal;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.SecurityContext;
@Path("user")
public class Endpoint {
@GET
public String userName(SecurityContext security) {
Principal user = security.getUserPrincipal();
return user != null ? user.getName() : "<NOT LOGGED IN>";
}
}
同じタイプのフィールドに @Inject を使用して、これらのコンテキストオブジェクトを挿入することもできます。
package org.acme.rest;
import java.security.Principal;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.SecurityContext;
@Path("user")
public class Endpoint {
@Inject
SecurityContext security;
@GET
public String userName() {
Principal user = security.getUserPrincipal();
return user != null ? user.getName() : "<NOT LOGGED IN>";
}
}
または、エンドポイントコンストラクターでも:
package org.acme.rest;
import java.security.Principal;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.SecurityContext;
@Path("user")
public class Endpoint {
SecurityContext security;
Endpoint(SecurityContext security) {
this.security = security;
}
@GET
public String userName() {
Principal user = security.getUserPrincipal();
return user != null ? user.getName() : "<NOT LOGGED IN>";
}
}
JSON シリアライゼーション
io.quarkus:quarkus-resteasy-reactive をインポートする代わりに、以下のモジュールのいずれかをインポートすることで、JSON のサポートを受けることができます。
| GAV | 使用方法 |
|---|---|
|
|
|
いずれの場合も、これらのモジュールをインポートすることで、HTTPメッセージボディをJSONから読み取ったり、より具体的なシリアライズ方法が登録されていない型 に JSONをシリアライズできるようになります。
詳細なJackson固有の機能
quarkus-resteasy-reactive-jackson エクステンションを使用する場合、RESTEasy Reactive がサポートするいくつかの高度な機能があります。
セキュア・シリアライゼーション
JSON をシリアライズするために Jackson を使用する場合、RESTEasy Reactive は、現在のユーザのroleに基づいてシリアライズされるフィールドのセットを制限する機能を提供します。これは、返されるPOJOのフィールド(またはゲッター)を @io.quarkus.resteasy.reactive.jackson.SecureField でアノテーションするだけで実現できます。
簡単な例を挙げると、次のようになります:
Person という名前のPOJOがあり、以下のようになっているとします:
package org.acme.rest;
import io.quarkus.resteasy.reactive.jackson.SecureField;
public class Person {
@SecureField(rolesAllowed = "admin")
private final Long id;
private final String first;
private final String last;
public Person(Long id, String first, String last) {
this.id = id;
this.first = first;
this.last = last;
}
public Long getId() {
return id;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
}
Person を使用する非常にシンプルな JAX-RS Resource は次のようになります:
package org.acme.rest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("person")
public class Person {
@Path("{id}")
@GET
public Person getPerson(Long id) {
return new Person(id, "foo", "bar");
}
}
Assuming security has been set up for the application (see our guide for more details), when a user with the admin role
performs an HTTP GET on /person/1 they will receive:
{
"id": 1,
"first": "foo",
"last": "bar"
}
レスポンスを閉じます。
ただし、 admin ロールを持たないユーザーは次のものを受け取ります。
{
"first": "foo",
"last": "bar"
}
この安全なシリアル化を実行するために、追加の設定を適用する必要はありません。ただし、ユーザーは @io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization と @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization アノテーションを使って、特定の JAX-RS Resource クラスまたはメソッドを選択することも可能です。
|
@JsonView support
JAX-RS のメソッドに @JsonView のアノテーションを付けると、返される POJO のシリアル化をメソッド単位でカスタマイズすることが可能です。これは例で説明するのが一番わかりやすいでしょう。
@JsonView の一般的な使用法は、特定のメソッドの特定のフィールドを非表示にすることです。その流れの中で、2 つのビューを定義しましょう。
public class Views {
public static class Public {
}
public static class Private extends Public {
}
}
シリアル化中にフィールドを非表示にする User POJO があると仮定します。この簡単な例は次のとおりです。
public class User {
@JsonView(Views.Private.class)
public int id;
@JsonView(Views.Public.class)
public String name;
}
このユーザーを返す JAX-RS メソッドによっては、シリアル化から id フィールドを除外したい場合があります。たとえば、安全でないメソッドでこのフィールドを公開しないようにしたい場合があります。RESTEasy Reactive でそれを実現する方法を、次の例に示します。
@JsonView(Views.Public.class)
@GET
@Path("/public")
public User userPublic() {
return testUser();
}
@JsonView(Views.Private.class)
@GET
@Path("/private")
public User userPrivate() {
return testUser();
}
結果の userPublic メソッドがシリアル化されると、 Public ビューに含まれないため、 id フィールドはレスポンスに含まれません。ただし、 userPrivate の結果には、シリアル化されたときに期待されるように id が含まれます。
メソッドごとに完全にカスタマイズされたシリアル化
JAX-RS のメソッドごとに POJO のシリアル化を完全にカスタマイズする必要がある場合があります。このような場合、 @io.quarkus.resteasy.reactive.jackson.CustomSerialization アノテーションは素晴らしいツールで、自由に設定できる com.fasterxml.jackson.databind.ObjectWriter というメソッド単位で設定することが可能です。
ユースケースの例を次に示します。
@CustomSerialization(UnquotedFields.class)
@GET
@Path("/invalid-use-of-custom-serializer")
public User invalidUseOfCustomSerializer() {
return testUser();
}
ここで、 UnquotedFields は次のように定義された BiFunction です。
public static class UnquotedFields implements BiFunction<ObjectMapper, Type, ObjectWriter> {
@Override
public ObjectWriter apply(ObjectMapper objectMapper, Type type) {
return objectMapper.writer().without(JsonWriteFeature.QUOTE_FIELD_NAMES);
}
}
基本的にこのクラスが行うことは、フィールド名に引用符を含めないよう Jackson に強制することです。
このカスタマイズは @CustomSerialization(UnquotedFields.class) を使用する JAX-RS メソッドのシリアル化にのみ行われることに注意することが重要です。
XML シリアライゼーション
XML サポートを有効にするには、プロジェクトに quarkus-resteasy-reactive-jaxb エクステンションを追加します。
| GAV | 使用方法 |
|---|---|
|
このモジュールをインポートすると、HTTP メッセージ本文を XML から読み込み、XML にシリアル化することができます (all the types not already registered with a more specific serialisation)。
高度な JAXB 固有の機能
quarkus-resteasy-reactive-jaxb エクステンションを使用する場合、RESTEasy Reactive がサポートするいくつかの高度な機能があります。
JAXB コンポーネントを注入する
JAXB resteasy リアクティブエクステンションは、ユーザーに対して透過的にリクエストとレスポンスをシリアライズおよびアンシリアライズします。ただし、JAXB コンポーネントをより細かく制御する必要がある場合は、JAXBContext、Marshaller、または Unmarshaller コンポーネントのいずれかを Bean に注入できます。
@ApplicationScoped
public class MyService {
@Inject
JAXBContext jaxbContext;
@Inject
Marshaller marshaller;
@Inject
Unmarshaller unmarshaller;
// ...
}
|
Quarkus は、 |
JAXB 設定をカスタマイズする
JAXB コンテキスト、および/または Marshaller/Unmarshaller コンポーネントの JAXB 設定をカスタマイズするには、型 io.quarkus.jaxb.runtime.JaxbContextCustomizer の CDI Bean を定義することをお勧めします。
カスタムモジュールを登録する必要がある場合の例は次のようになります。
@Singleton
public class RegisterCustomModuleCustomizer implements JaxbContextCustomizer {
// For JAXB context configuration
@Override
public void customizeContextProperties(Map<String, Object> properties) {
}
// For Marshaller configuration
@Override
public void customizeMarshaller(Marshaller marshaller) throws PropertyException {
marshaller.setProperty("jaxb.formatted.output", Boolean.TRUE);
}
// For Unmarshaller configuration
@Override
public void customizeUnmarshaller(Unmarshaller unmarshaller) throws PropertyException {
// ...
}
}
|
3 つのメソッドすべてを実装する必要はありませんが、必要なものだけを実装する必要があります。 |
または、次のようにして、独自の JAXBContext Bean を提供できます。
public class CustomJaxbContext {
// Replaces the CDI producer for JAXBContext built into Quarkus
@Singleton
@Produces
JAXBContext jaxbContext() {
// ...
}
}
|
カスタム JAXB コンテキストインスタンスを提供する場合は、XML シリアル化に使用するクラスを登録する必要があることに注意してください。これは、Quarkus がカスタム JAXB コンテキストインスタンスを自動検出されたクラスで更新しないことを意味します。 |
Web リンクのサポート
Web リンクのサポートを有効にするには、プロジェクトに quarkus-resteasy-reactive-links エクステンションを追加します。
| GAV | 使用方法 |
|---|---|
|
このモジュールをインポートすると、エンドポイントリソースに @InjectRestLinks アノテーションを付けるだけで、レスポンスの HTTP ヘッダに Web リンクを挿入することができるようになります。返される Web リンクを宣言するには、リンクされたメソッドで @RestLink アノテーションを使用する必要があります。この例としては、以下のようなものがあります。
@Path("/records")
public class RecordsResource {
@GET
@RestLink(rel = "list")
@InjectRestLinks
public List<Record> getAll() {
// ...
}
@GET
@Path("/{id}")
@RestLink(rel = "self")
@InjectRestLinks(RestLinkType.INSTANCE)
public TestRecord get(@PathParam("id") int id) {
// ...
}
@PUT
@Path("/{id}")
@RestLink
@InjectRestLinks(RestLinkType.INSTANCE)
public TestRecord update(@PathParam("id") int id) {
// ...
}
@DELETE
@Path("/{id}")
@RestLink
public TestRecord delete(@PathParam("id") int id) {
// ...
}
}
上記のリソース内でメソッド getAll によって定義されたエンドポイント /records を curl を使用して呼び出すと、Web リンクヘッダーが取得されます。
& curl -i localhost:8080/records
Link: <http://localhost:8080/records>; rel="list"
このリソースは型 Record の単一のインスタンスを返さないため、メソッド get、 update、および delete のリンクは挿入されません。これで、エンドポイント /records/1 を呼び出すと、次の Web リンクが表示されます。
& curl -i localhost:8080/records/1
Link: <http://localhost:8080/records>; rel="list"
Link: <http://localhost:8080/records/1>; rel="self"
Link: <http://localhost:8080/records/1>; rel="update"
Link: <http://localhost:8080/records/1>; rel="delete"
get `, `update `, `delete の各メソッドでは、パスパラメータ "id" を使用しており、エンティティタイプ "Record" にフィールド "id" が存在するため、Web リンクは返されるリンクに適切に値 "1" を入力します。これに加えて、エンティティ・タイプのどのフィールドとも一致しないパス・パラメータを持つウェブ・リンクを生成することも可能です。たとえば、次のメソッドでは、パスパラメータ "text" を使用していますが、エンティティRecordには "text" という名前のフィールドがありません:
@Path("/records")
public class RecordsResource {
// ...
@GET
@Path("/search/{text}")
@RestLink(rel = "search records by free text")
@InjectRestLinks
public List<Record> search(@PathParam("text") String text) { (4)
// ...
}
// ...
}
このリソースの生成されたウェブリンクは次のようになります Link: <http://localhost:8080/search/{text}>; rel="search records by free text" 。
最後に、 delete リソースを呼び出すと、 delete メソッドに @InjectRestLinks アノテーションが付いていないため、ウェブリンクが表示されないはずです。
Web リンクレジストリーへのプログラムによるアクセス
RestLinksProvider Bean を挿入するだけで、プログラムで Web リンクレジストリーにアクセスできます。
@Path("/records")
public class RecordsResource {
@Inject
RestLinksProvider linksProvider;
// ...
}
型 RestLinksProvider の挿入された Bean を使用すると、メソッド RestLinksProvider.getTypeLinks を使用して型ごとにリンクを取得するか、メソッド RestLinksProvider.getInstanceLinks を使用して具象インスタンスによってリンクを取得できます。
JSON Hypertext Application Language ( HAL ) のサポート
HAL 標準は、ウェブリンクを表現するためのシンプルなフォーマットです。
HAL サポートを有効にするには、プロジェクトに quarkus-hal エクステンションを追加します。また、HAL には JSON サポートが必要なため、 quarkus-resteasy-reactive-jsonb または quarkus-resteasy-reactive-jackson エクステンションを追加する必要があります。
| GAV | 使用方法 |
|---|---|
|
エクステンションを追加したら、次は REST リソースにアノテーションを付け、メディアタイプ application/hal+json (または RestMediaType.APPLICATION_HAL_JSON を使用) を生成できるようにします。例えば、以下のようになります。
@Path("/records")
public class RecordsResource {
@GET
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@RestLink(rel = "list")
@InjectRestLinks
public List<Record> getAll() {
// ...
}
@GET
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@Path("/{id}")
@RestLink(rel = "self")
@InjectRestLinks(RestLinkType.INSTANCE)
public TestRecord get(@PathParam("id") int id) {
// ...
}
}
ここで、エンドポイント /records と /records/{id} は、 json と hal + json の両方のメディアタイプを受け入れて、Hal 形式でレコードを出力します。
例えば、 curl を使用して /records エンドポイントを呼び出してレコードのリストを返す場合、HAL 形式は次のようになります。
& curl -H "Accept:application/hal+json" -i localhost:8080/records
{
"_embedded": {
"items": [
{
"id": 1,
"slug": "first",
"value": "First value",
"_links": {
"self": {
"href": "http://localhost:8081/records/1"
},
"list": {
"href": "http://localhost:8081/records"
}
}
},
{
"id": 2,
"slug": "second",
"value": "Second value",
"_links": {
"self": {
"href": "http://localhost:8081/records/2"
},
"list": {
"href": "http://localhost:8081/records"
}
}
}
]
},
"_links": {
"list": {
"href": "http://localhost:8081/records"
}
}
}
1 つのインスタンスのみを返すリソース /records/1 を呼び出すと、出力は次のようになります。
& curl -H "Accept:application/hal+json" -i localhost:8080/records/1
{
"id": 1,
"slug": "first",
"value": "First value",
"_links": {
"self": {
"href": "http://localhost:8081/records/1"
},
"list": {
"href": "http://localhost:8081/records"
}
}
}
最後に、次の例で説明するように、 HalCollectionWrapper (エンティティのリストを返す) または HalEntityWrapper (単一オブジェクトを返す)を返すことで、リソースにプログラム的に追加のHALリンクを提供することも可能です。
@Path("/records")
public class RecordsResource {
@Inject
RestLinksProvider linksProvider;
@GET
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@RestLink(rel = "list")
public HalCollectionWrapper getAll() {
List<Record> list = // ...
HalCollectionWrapper halCollection = new HalCollectionWrapper(list, "collectionName", linksProvider.getTypeLinks(Record.class));
halCollection.addLinks(Link.fromPath("/records/1").rel("first-record").build());
return halCollection;
}
@GET
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@Path("/{id}")
@RestLink(rel = "self")
@InjectRestLinks(RestLinkType.INSTANCE)
public HalEntityWrapper get(@PathParam("id") int id) {
Record entity = // ...
HalEntityWrapper halEntity = new HalEntityWrapper(entity, linksProvider.getInstanceLinks(entity));
halEntity.addLinks(Link.fromPath("/records/1/parent").rel("parent-record").build());
return halEntity;
}
}
CORS フィルター
クロスオリジンリソース共有 (CORS)は、ウェブページ上の制限されたリソースを、最初のリソースが提供されたドメイン以外の別のドメインから要求できるようにするメカニズムです。
Quarkus には、HTTP レイヤーレベルの CORS フィルターが付属しています。使用方法については、HTTPリファレンスドキュメント を参照してください。
より高度な使用法
ここでは、最初は知る必要がないかもしれませんが、より複雑なユースケースに役立つ可能性のある、より詳細なトピックをいくつか紹介します。
実行モデル、ブロッキング、非ブロッキング
RESTEasy Reactive は、次の 2 つのメインスレッドタイプを使用して実装されます。
-
イベントループスレッド: HTTPリクエストからのバイトの読み込みとHTTPレスポンスへのバイトの書き戻しを担当します。
-
ワーカースレッド: プールされ、長時間実行される操作の負荷を軽減するために使用できます
イベントループスレッド (IO スレッドとも呼ばれます) は、すべての IO 操作を非同期で実際に実行し、それらの IO 操作の完了に関心のあるリスナーをトリガーする役割を果たします。
デフォルトでは、スレッド RESTEasy Reactive は、メソッドのシグネチャーに応じてエンドポイントメソッドを実行します。メソッドが次のタイプのいずれかを返す場合、それは非ブロッキングと見なされ、デフォルトで IO スレッドで実行されます。
-
io.smallrye.mutiny.Uni -
io.smallrye.mutiny.Multi -
java.util.concurrent.CompletionStage -
org.reactivestreams.Publisher -
Kotlin の
suspendedメソッド
この最良の推測アプローチは、操作の大部分がデフォルトで正しいスレッドで実行されることを意味します。リアクティブコードを記述している場合、メソッドは通常、これらの型の 1 つを返し、IO スレッドで実行されます。ブロッキングコードを記述している場合、メソッドは通常、結果を直接返し、これらはワーカースレッドで実行されます。
@Blocking アノテーションと @NonBlocking アノテーションを使用してこの動作をオーバーライドできます。これは、メソッド、クラス、または javax.ws.rs.core.Application レベルで適用できます。
以下の例は、デフォルトの動作をオーバーライドし、 Uni を返しても、常にワーカースレッドで実行されます。
package org.acme.rest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import io.smallrye.common.annotation.Blocking;
@Path("yawn")
public class Endpoint {
@Blocking
@GET
public Uni<String> blockingHello() throws InterruptedException {
// do a blocking operation
Thread.sleep(1000);
return Uni.createFrom().item("Yaaaawwwwnnnnnn…");
}
}
ほとんどの場合、 Mutiny 、 Hibernate Reactive 、または Quarkusリアクティブエクステンション などを使って、同じブロック操作を非同期/リアクティブに実現する方法があります。
package org.acme.rest;
import java.time.Duration;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import io.smallrye.mutiny.Uni;
@Path("yawn")
public class Endpoint {
@GET
public Uni<String> blockingHello() throws InterruptedException {
return Uni.createFrom().item("Yaaaawwwwnnnnnn…")
// do a non-blocking sleep
.onItem().delayIt().by(Duration.ofSeconds(2));
}
}
メソッドまたはクラスに javax.transaction.Transactional アノテーションが付けられている場合、それもブロッキングメソッドとして扱われます。これは、JTA がブロッキングテクノロジーであり、Hibernate や JDBC などの他のブロッキングテクノロジーで一般的に使用されているためです。クラスでの明示的な @Blocking または @NonBlocking は、この動作をオーバーライドします。
例外のマッピング
もしアプリケーションがエラー時に正規の HTTP コード以外を返す必要がある場合、最善の方法は、フレームワークが WebApplicationException またはそのサブタイプを使用して適切な HTTP レスポンスを送信する結果となる例外を投げることです。
package org.acme.rest;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
@Path("cheeses/{cheese}")
public class Endpoint {
@GET
public String findCheese(String cheese) {
if(cheese == null)
// send a 400
throw new BadRequestException();
if(!cheese.equals("camembert"))
// send a 404
throw new NotFoundException("Unknown cheese: " + cheese);
return "Camembert is a very nice cheese";
}
}
エンドポイントメソッドが JAX-RS を認識しない別のサービスレイヤーに呼び出しを委任している場合は、サービス例外を HTTP レスポンスに変換する方法が必要であり、メソッドに @ServerExceptionMapper を使用してこれを行うことができます。例外マッパーは処理する例外タイプのパラメーターが 1 つあり、その例外を RestResponse (あるいは Uni<RestResponse<?>>) に変換します。
package org.acme.rest;
import java.util.Map;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import org.jboss.resteasy.reactive.RestResponse;
class UnknownCheeseException extends RuntimeException {
public final String name;
public UnknownCheeseException(String name) {
this.name = name;
}
}
@ApplicationScoped
class CheeseService {
private static final Map<String, String> cheeses =
Map.of("camembert", "Camembert is a very nice cheese",
"gouda", "Gouda is acceptable too, especially with cumin");
public String findCheese(String name) {
String ret = cheeses.get(name);
if(ret != null)
return ret;
throw new UnknownCheeseException(name);
}
}
@Path("cheeses/{cheese}")
public class Endpoint {
@Inject
CheeseService cheeses;
@ServerExceptionMapper
public RestResponse<String> mapException(UnknownCheeseException x) {
return RestResponse.status(Response.Status.NOT_FOUND, "Unknown cheese: " + x.name);
}
@GET
public String findCheese(String cheese) {
if(cheese == null)
// send a 400
throw new BadRequestException();
return cheeses.findCheese(cheese);
}
}
| REST エンドポイントクラスで定義された例外マッパーは、例外が同じクラスで出力された場合にのみ呼び出されます。グローバル例外マッパーを定義する場合は、REST エンドポイントクラスの外部で定義するだけです。 |
package org.acme.rest;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import org.jboss.resteasy.reactive.RestResponse;
class ExceptionMappers {
@ServerExceptionMapper
public RestResponse<String> mapException(UnknownCheeseException x) {
return RestResponse.status(Response.Status.NOT_FOUND, "Unknown cheese: " + x.name);
}
}
JAX-RSの方式で例外マッパー を宣言することもできます。
例外マッパーは、次のパラメータータイプのいずれかを宣言できます。
| タイプ | 使用方法 |
|---|---|
例外タイプ |
処理する例外タイプを定義します |
Context objects> のいずれか |
|
現在のリクエストにアクセスするためのコンテキストオブジェクト |
次の戻り値の型のいずれかを宣言できます。
| タイプ | 使用方法 |
|---|---|
|
例外が発生したときにクライアントに送信するレスポンス |
例外が発生したときにクライアントに送信する非同期レスポンス |
|
例外が発生した場合、RESTEasy Reactive はデフォルトでログを記録しません (セキュリティ上の理由による)。 このため、特定の例外処理コードが呼び出された (または呼び出されなかった) 理由を理解するのが難しい場合があります。 RESTEasy Reactive が例外マッピングコードを実行する前に実際の例外をログするようにするには、次のように
|
リクエストまたはレスポンスフィルター
アノテーション経由
リクエスト処理の次のフェーズで呼び出される関数を宣言できます。
-
エンドポイントメソッドが識別される前: 事前ルーティングリクエストフィルター
-
ルーティング後、エンドポイントメソッドが呼び出される前: 通常のリクエストフィルター
-
エンドポイントメソッドが呼び出された後: レスポンスフィルター
これらのフィルターを使用すると、リクエスト URI、HTTP メソッドの調査、ルーティングへの影響、リクエストヘッダーの検索または変更、リクエストの中止、レスポンスの変更など、さまざまなことができます。
リクエストフィルターは、 @ServerRequestFilter アノテーションで宣言することが出来ます:
import java.util.Optional;
class Filters {
@ServerRequestFilter(preMatching = true)
public void preMatchingFilter(ContainerRequestContext requestContext) {
// make sure we don't lose cheese lovers
if("yes".equals(requestContext.getHeaderString("Cheese"))) {
requestContext.setRequestUri(URI.create("/cheese"));
}
}
@ServerRequestFilter
public Optional<RestResponse<Void>> getFilter(ContainerRequestContext ctx) {
// only allow GET methods for now
if(ctx.getMethod().equals(HttpMethod.GET)) {
return Optional.of(RestResponse.status(Response.Status.METHOD_NOT_ALLOWED));
}
return Optional.empty();
}
}
|
リクエストフィルターは通常、リクエストを処理するメソッドが実行されるのと同じスレッドで実行されます。つまり、リクエストを処理するメソッドが しかし、リクエストを処理するメソッドがワーカスレッドで実行されるにも関わらず、フィルターをイベントループで実行する必要がある場合、 |
同様に、レスポンスフィルターは @ServerResponseFilter アノテーションで宣言することが可能です。
class Filters {
@ServerResponseFilter
public void getFilter(ContainerResponseContext responseContext) {
Object entity = responseContext.getEntity();
if(entity instanceof String) {
// make it shout
responseContext.setEntity(((String)entity).toUpperCase());
}
}
}
フィルターは、次のパラメータータイプのいずれかを宣言できます。
| タイプ | 使用方法 |
|---|---|
Context objects> のいずれか |
|
現在のリクエストにアクセスするためのコンテキストオブジェクト |
|
現在のレスポンスにアクセスするためのコンテキストオブジェクト |
|
出力された例外、または |
次の戻り値の型のいずれかを宣言できます。
| タイプ | 使用方法 |
|---|---|
フィルターチェーンを続行する代わりにクライアントに送信するレスポンス、またはフィルターチェーンを続行する必要がある場合は null |
|
フィルターチェーンを続行する代わりにクライアントに送信するオプションのレスポンス、またはフィルターチェーンを続行する必要がある場合は空の値 |
|
フィルターチェーンを続行する代わりにクライアントに送信する非同期レスポンス、またはフィルターチェーンを続行する必要がある場合は null |
@NameBinding メタアノテーションを使用して、フィルターが実行されるリソースメソッドを制限できます。
|
JAX-RS の方法
HTTP リクエストとレスポンスの両方とも、それぞれ ContainerRequestFilter または ContainerResponseFilter の実装を提供することで、 インターセプトすることができます。これらのフィルタは、メッセージに関連付けられたメタデータを処理するのに適しています。HTTP ヘッダ、クエリパラメーター、メディアタイプ、その他のメタデータです。また、ユーザーがエンドポイントにアクセスする権限を持っていない場合など、リクエスト処理を中止する機能も持っています。
ContainerRequestFilter を使用して、サービスにロギング機能を追加してみましょう。 ContainerRequestFilter を実装して、 @Provider アノテーションをつけることで実現できます。
package org.acme.rest.json;
import io.vertx.core.http.HttpServerRequest;
import org.jboss.logging.Logger;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;
@Provider
public class LoggingFilter implements ContainerRequestFilter {
private static final Logger LOG = Logger.getLogger(LoggingFilter.class);
@Context
UriInfo info;
@Context
HttpServerRequest request;
@Override
public void filter(ContainerRequestContext context) {
final String method = context.getMethod();
final String path = info.getPath();
final String address = request.remoteAddress().toString();
LOG.infof("Request %s %s from IP %s", method, path, address);
}
}
これで、RESTメソッドが呼び出されるたびに、リクエストがコンソールにログとして記録されるようになりました。
2019-06-05 12:44:26,526 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /legumes from IP 127.0.0.1
2019-06-05 12:49:19,623 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 0:0:0:0:0:0:0:1
2019-06-05 12:50:44,019 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request POST /fruits from IP 0:0:0:0:0:0:0:1
2019-06-05 12:51:04,485 INFO [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 127.0.0.1
リーダーとライター: マッピングエンティティーと HTTP 本文
エンドポイントメソッドが(RestResponse<?> もしくはエンティティの Response を返し)オブジェクトを返すときはいつでも、RESTEasy Reactive は、それを HTTP レスポンスボディにマップする方法を探します。
同様に、エンドポイントメソッドがオブジェクトをパラメーターとして受け取る場合は常に、HTTP リクエストの本文をそのオブジェクトにマッピングする方法を探します。
これは、MessageBodyReader と MessageBodyWriter インターフェイスのプラグインシステムによって行われ、どの Java タイプからどのメディアタイプへマッピングするか、またそのタイプの Java インスタンスとどのように HTTP 本文をやり取りするかを定義する役割を持っています。
たとえば、エンドポイントに独自の Cheese 型がある場合:
package org.acme.rest;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
class Cheese {
public String name;
public Cheese(String name) {
this.name = name;
}
}
@Path("cheese")
public class Endpoint {
@GET
public Cheese sayCheese() {
return new Cheese("Cheeeeeese");
}
@PUT
public void addCheese(Cheese cheese) {
System.err.println("Received a new cheese: " + cheese.name);
}
}
次に、 @Provider でアノテーションを付けたボディリーダー/ライターを使用して読み取りと書き込みの方法を定義できます:
package org.acme.rest;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
@Provider
public class CheeseBodyHandler implements MessageBodyReader<Cheese>,
MessageBodyWriter<Cheese> {
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return type == Cheese.class;
}
@Override
public void writeTo(Cheese t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream)
throws IOException, WebApplicationException {
entityStream.write(("[CheeseV1]" + t.name)
.getBytes(StandardCharsets.UTF_8));
}
@Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return type == Cheese.class;
}
@Override
public Cheese readFrom(Class<Cheese> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
InputStream entityStream)
throws IOException, WebApplicationException {
String body = new String(entityStream.readAllBytes(), StandardCharsets.UTF_8);
if(body.startsWith("[CheeseV1]"))
return new Cheese(body.substring(11));
throw new IOException("Invalid cheese: " + body);
}
}
ライターのパフォーマンスを最大限に高めたい場合は、 MessageBodyWriter の代わりに ServerMessageBodyWriter を拡張できます。こちらでは、リフレクションの使用を減らし、ブロッキング IO レイヤーをバイパスできます。
package org.acme.rest;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Provider;
import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo;
import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter;
import org.jboss.resteasy.reactive.server.spi.ServerRequestContext;
@Provider
public class CheeseBodyHandler implements MessageBodyReader<Cheese>,
ServerMessageBodyWriter<Cheese> {
// …
@Override
public boolean isWriteable(Class<?> type, ResteasyReactiveResourceInfo target,
MediaType mediaType) {
return type == Cheese.class;
}
@Override
public void writeResponse(Cheese t, ServerRequestContext context)
throws WebApplicationException, IOException {
context.serverResponse().end("[CheeseV1]" + t.name);
}
}
リーダーおよびライターインターセプター
リクエストとレスポンスをインターセプトできるように、@Providerでアノテーションが付けられたクラスの ReaderInterceptor または WriterInterceptor を拡張することによってリーダーとライターをインターセプトすることもできます。
このエンドポイントを見ると、次のようになります。
package org.acme.rest;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
@Path("cheese")
public class Endpoint {
@GET
public String sayCheese() {
return "Cheeeeeese";
}
@PUT
public void addCheese(String cheese) {
System.err.println("Received a new cheese: " + cheese);
}
}
次のようなリーダーとライターのインターセプターを追加できます。
package org.acme.rest;
import java.io.IOException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.ReaderInterceptorContext;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
@Provider
public class CheeseIOInterceptor implements ReaderInterceptor, WriterInterceptor {
@Override
public void aroundWriteTo(WriterInterceptorContext context)
throws IOException, WebApplicationException {
System.err.println("Before writing " + context.getEntity());
context.proceed();
System.err.println("After writing " + context.getEntity());
}
@Override
public Object aroundReadFrom(ReaderInterceptorContext context)
throws IOException, WebApplicationException {
System.err.println("Before reading " + context.getGenericType());
Object entity = context.proceed();
System.err.println("After reading " + entity);
return entity;
}
}
RESTEasy リアクティブおよび REST クライアントリアクティブインタラクション
Quarkus では、RESTEasy Reactive エクステンションと REST クライアントリアクティブエクステンション は同じインフラストラクチャを共有しています。この考慮の重要な結果の一つは、(JAX-RS の意味での) プロバイダーのリストを共有しているということです。
例えば、 WriterInterceptor を宣言した場合、デフォルトではサーバーの呼び出しとクライアントの呼び出しの両方をインターセプトしますが、これは望ましい動作ではないかもしれません。
しかし、このデフォルトの動作を変更してプロバイダを制約することができます。
-
プロバイダに
@ConstrainedTo(RuntimeType.SERVER)アノテーションを追加することで、 サーバー コールのみを考慮します。 -
プロバイダに
@ConstrainedTo(RuntimeType.CLIENT)アノテーションを追加することで、 クライアント コールのみを考慮します。
パラメータマッピング
すべてのリクエストパラメーター は String として宣言することができます。しかし、それだけでなく、次のいずれかの型でも宣言できます。
-
ParamConverterProviderを通じて登録済のParamConverterが存在する型 -
プリミティブ型
-
単一の
String引数を受け入れるコンストラクターを持つ型。 -
Stringという引数を持ち、その型のインスタンスを返すvalueOfまたはfromStringという名前の静的メソッドを持っている型。もし両方のメソッドが存在する場合は、valueOfが使用されます。ただし、その型がenumの場合はfromStringが使用されます。 -
List<T>、Set<T>、またはSortedSet<T>。ここでTは上記の基準を満たします。
次の例は、これらすべての可能性を示しています。
package org.acme.rest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
import org.jboss.resteasy.reactive.RestQuery;
@Provider
class MyConverterProvider implements ParamConverterProvider {
@Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType,
Annotation[] annotations) {
// declare a converter for this type
if(rawType == Converter.class) {
return (ParamConverter<T>) new MyConverter();
}
return null;
}
}
// this is my custom converter
class MyConverter implements ParamConverter<Converter> {
@Override
public Converter fromString(String value) {
return new Converter(value);
}
@Override
public String toString(Converter value) {
return value.value;
}
}
// this uses a converter
class Converter {
String value;
Converter(String value) {
this.value = value;
}
}
class Constructor {
String value;
// this will use the constructor
public Constructor(String value) {
this.value = value;
}
}
class ValueOf {
String value;
private ValueOf(String value) {
this.value = value;
}
// this will use the valueOf method
public static ValueOf valueOf(String value) {
return new ValueOf(value);
}
}
@Path("hello")
public class Endpoint {
@Path("{converter}/{constructor}/{primitive}/{valueOf}")
@GET
public String conversions(Converter converter, Constructor constructor,
int primitive, ValueOf valueOf,
@RestQuery List<Constructor> list) {
return converter + "/" + constructor + "/" + primitive
+ "/" + valueOf + "/" + list;
}
}
前提条件
以下のようないくつかの条件の下で、HTTPでは条件付きのリクエストが可能です :
-
リソースの最終変更日
-
リソースの状態またはバージョンを指定するためのリソースのハッシュコードに似たリソースタグ
Request コンテキストオブジェクトを使用して、条件付きリクエスト検証を行う方法を見てみましょう。
package org.acme.rest;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Date;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
@Path("conditional")
public class Endpoint {
// It's important to keep our date on seconds because that's how it's sent to the
// user in the Last-Modified header
private Date date = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
private int version = 1;
private EntityTag tag = new EntityTag("v1");
private String resource = "Some resource";
@GET
public Response get(Request request) {
// first evaluate preconditions
ResponseBuilder conditionalResponse = request.evaluatePreconditions(date, tag);
if(conditionalResponse != null)
return conditionalResponse.build();
// preconditions are OK
return Response.ok(resource)
.lastModified(date)
.tag(tag)
.build();
}
@PUT
public Response put(Request request, String body) {
// first evaluate preconditions
ResponseBuilder conditionalResponse = request.evaluatePreconditions(date, tag);
if(conditionalResponse != null)
return conditionalResponse.build();
// preconditions are OK, we can update our resource
resource = body;
date = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
version++;
tag = new EntityTag("v" + version);
return Response.ok(resource)
.lastModified(date)
.tag(tag)
.build();
}
}
GET /conditional を初めて呼び出すと、このようなレスポンスが返ってきます。
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
ETag: "v1"
Last-Modified: Wed, 09 Dec 2020 16:10:19 GMT
Content-Length: 13
Some resource
これで、新しいバージョンを取得する必要があるかどうかを確認したい場合は、次のようなリクエストを行うことができます:
GET /conditional HTTP/1.1
Host: localhost:8080
If-Modified-Since: Wed, 09 Dec 2020 16:10:19 GMT
そして、次のようなレスポンスが返ってきます:
HTTP/1.1 304 Not Modified
なぜなら、そのリソースはその日以降に変更されていないからです。これはリソースを送信する手間を省くだけでなく、ユーザーが同時に変更されたことを検出するのにも役立ちます。たとえば、あるクライアントがリソースを更新したいが、別のユーザーがその後にリソースを変更したとしましょう。この場合、前の GET リクエストに続いて、この更新を行うことができます。
PUT /conditional HTTP/1.1
Host: localhost:8080
If-Unmodified-Since: Wed, 09 Dec 2020 16:25:43 GMT
If-Match: v1
Content-Length: 8
Content-Type: text/plain
newstuff
また、他のユーザーが GET と PUT の間のリソースを変更した場合は、次の回答が返されます。
HTTP/1.1 412 Precondition Failed
ETag: "v2"
Content-Length: 0
ネゴシエーション
REST (および HTTP) の主な考え方の 1 つは、リソースはその表現に依存せず、クライアントとサーバーの両方が、望むだけ多くのメディアタイプでリソースを自由に表現できることです。これにより、サーバーは複数の表現のサポートを宣言し、クライアントはどの表現をサポートするかを宣言し、適切なものを提供されるようにすることができます。
次のエンドポイントは、プレーンテキストまたは JSON での cheese の提供をサポートしています。
package org.acme.rest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.fasterxml.jackson.annotation.JsonCreator;
class Cheese {
public String name;
@JsonCreator
public Cheese(String name) {
this.name = name;
}
@Override
public String toString() {
return "Cheese: " + name;
}
}
@Path("negotiated")
public class Endpoint {
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@GET
public Cheese get() {
return new Cheese("Morbier");
}
@Consumes(MediaType.TEXT_PLAIN)
@PUT
public Cheese putString(String cheese) {
return new Cheese(cheese);
}
@Consumes(MediaType.APPLICATION_JSON)
@PUT
public Cheese putJson(Cheese cheese) {
return cheese;
}
}
JSON の場合、ユーザーは Accept ヘッダーでどの表現を取得するかを選択することができます。
> GET /negotiated HTTP/1.1
> Host: localhost:8080
> Accept: application/json
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 18
<
< {"name":"Morbier"}
そしてテキストの場合:
> GET /negotiated HTTP/1.1
> Host: localhost:8080
> Accept: text/plain
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 15
<
< Cheese: Morbier
同様に、2 つの異なる表現を PUT することができます。JSON:
> PUT /negotiated HTTP/1.1
> Host: localhost:8080
> Content-Type: application/json
> Content-Length: 16
>
> {"name": "brie"}
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=UTF-8
< Content-Length: 15
<
< {"name":"brie"}
またはプレーンテキスト:
> PUT /negotiated HTTP/1.1
> Host: localhost:8080
> Content-Type: text/plain
> Content-Length: 9
>
> roquefort
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=UTF-8
< Content-Length: 20
<
< {"name":"roquefort"}
HTTP圧縮
HTTP レスポンスのボディは、デフォルトでは圧縮されていません。 quarkus.http.enable-compression=true を使用して HTTP 圧縮サポートを有効にできます。
圧縮サポートが有効であれば、以下の場合にレスポンスボディが圧縮されます。
-
ルートメソッドが
@io.quarkus.vertx.http.Compressedでアノテーションされている、または -
Content-Typeヘッダーが設定されており、その値はquarkus.http.compress-media-typesを介して設定された圧縮メディアタイプである。
次の場合、レスポンスボディは圧縮されません。
-
ルートメソッドが
@io.quarkus.vertx.http.Uncompressedでアノテーションされている、または -
Content-Typeヘッダーが設定されていない。
デフォルトでは、 text/html , text/plain , text/xml , text/css , text/javascript , application/javascript のメディアタイプが圧縮されます。
|
| クライアントが HTTP 圧縮をサポートしていない場合、レスポンスボディは圧縮されません。 |
JAX-RSクラスを含める/除外する
ビルドタイム条件の使用
Quarkusでは、CDI Beanの場合と同様に、ビルド時の条件に応じて、JAX-RSリソース、プロバイダー、および機能を直接包含または除外することができます。したがって、さまざまなJAX-RSクラスは、プロファイル条件( @io.quarkus.arc.profile.IfBuildProfile または @io.quarkus.arc.profile.UnlessBuildProfile )および/またはプロパティ条件( io.quarkus.arc.properties.IfBuildProperty または io.quarkus.arc.properties.UnlessBuildProperty )でアノテーションすることができ、構築時にどの条件でこれらのJAX-RSクラスが含まれるべきかをQuarkusに示すことができます。
次の例では、Quarkusは、ビルドプロファイル app1 が有効になっている場合に限り、 ResourceForApp1Only リソースを含めます。
@IfBuildProfile("app1")
public class ResourceForApp1Only {
@GET
@Path("sayHello")
public String sayHello() {
return "hello";
}
}
JAX-RS アプリケーションが検出され、メソッド getClasses() および、または getSingletons() がオーバーライドされている場合、 Quarkus はビルド時の条件を無視し、 JAX-RS アプリケーションで定義されているもののみを考慮することに注意してください。
実行時プロパティの使用
Quarkusでは、 @io.quarkus.resteasy.reactive.server.EndpointDisabled アノテーションを使用して、実行時プロパティの値に基づいてJAX-RSリソースを条件付きで無効にすることも可能です。
次の例では、アプリケーションで some.property が "disable" に設定されている場合、Quarkus は実行時に RuntimeResource を除外します。
@EndpointDisabled(name = "some.property", stringValue = "disable")
public class RuntimeResource {
@GET
@Path("sayHello")
public String sayHello() {
return "hello";
}
}
RESTEasy Reactive クライアント
RESTEasy Reactiveには、サーバー側に加えて、ノンブロッキングを核とした新しいMicroProfile Rest Clientの実装が搭載されています。
なお、RESTEasy Reactiveでは、 quarkus-rest-client エクステンションが正常に動作しない場合があります。 quarkus-rest-client-reactive を代わりに使用して下さい。
リアクティブ・クライアントの詳細については、REST Client Reactiveガイド を参照してください。