Quarkus REST(旧RESTEasy Reactive)によるRESTサービスの作成
このガイドでは、QuarkusでQuarkus RESTを使用してRESTサービスを記述する方法を説明します。
Quarkus RESTのリファレンスガイドです。より軽量な紹介については、 JSON RESTサービスの作成ガイドを 参照してください。 |
Quarkus RESTとは何ですか?
Quarkus RESTは、共通の Vert.x レイヤーで動作するようにゼロから書かれた新しい Jakarta REST (旧称 JAX-RS) の実装で、完全にリアクティブです。 Quarkusと非常に緊密に統合されており、その結果、多くの作業をビルド時に移行できます。
Jakarta RESTの実装として使えるはずですが、その上、ブロッキングとノンブロッキングの両方のエンドポイントに対して素晴らしいパフォーマンスを発揮し、Jakarta RESTが提供する機能の上に多くの新機能を備えています。
エンドポイントの作成
はじめに
次のインポートをビルドファイルに追加します:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest")
これで、 org.acme.rest.Endpoint
クラスで最初のエンドポイントを書くことができます:
package org.acme.rest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("")
public class Endpoint {
@GET
public String hello() {
return "Hello, World!";
}
}
用語解説
- REST
- エンドポイント
-
REST 呼び出しを処理するために呼び出される Java メソッド
- URL / URI (Uniform Resource Locator / Identifier)
-
REST リソースの場所を特定するために使用 (specification)
- リソース
-
ドメインオブジェクトを表します。これは、APIが提供し、変更するものです。Jakarta RESTでは
entity
とも呼ばれます。 - 表現
-
通信上でのリソースの表現方法は、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 jakarta.ws.rs.GET;
import jakarta.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 jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/api")
public static class MyApplication extends Application {
}
これにより、すべての REST エンドポイントが /api
を基準に解決されるので、上記の @Path ("rest")
のエンドポイントは
/api/rest/
でアクセスできるようになります。また、アノテーションを使用しない場合、 quarkus.rest.path
ビルド時プロパティーを設定してルートパスを設定することもできます。
エンドポイントの宣言: HTTP メソッド
各エンドポイントメソッドには、次のいずれかのアノテーションを付ける必要があります。これにより、どの HTTP メソッドがそのメソッドにマップされるかが定義されます。
アノテーション | 使用法 |
---|---|
リソース表現を取得します。状態を変更しないでください。idempotent (HTTP docs) |
|
本文のない |
|
リソースを作成し、そのリソースへのリンクを取得します (HTTP docs) |
|
リソースを置き換えるか作成します。idempotent (HTTP docs) であるべきです。 |
|
既存のリソースを削除します。idempotent (HTTP docs) |
|
リソースに関する情報を取得します。idempotent (HTTP docs) です。 |
|
リソースを更新するか、作成します。idempotent (HTTP docs) ではありません。 |
@HttpMethod
アノテーションを付与したアノテーションを宣言することにより、他の HTTP メソッドを宣言することもできます。
package org.acme.rest;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import jakarta.ws.rs.HttpMethod;
import jakarta.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 リクエストボディーとして使用できる 1 つ以上のメディアタイプを指定するか、 https レスポンスボディーとして生成できます。これらのクラスアノテーションは、各メソッドに適用されます。
MediaType
クラスには多くの定数があり、
特定の定義済みメディアタイプを指すために使用できます。
詳細は、ネゴシエーション セクションを参照してください。
リクエストパラメーターへのアクセス
パラメーター名の情報を生成するために、 -parameters (javac) または <parameters> または <maven.compiler.parameters> (Maven) を使ってコンパイラーを設定することを忘れないでください。
|
次の HTTP リクエスト要素は、エンドポイントメソッドによって取得される可能性があります。
HTTP 要素 | アノテーション | 使用法 |
---|---|---|
リンク:https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-common/3.19.4/org/jboss/resteasy/reactive/RestPath.html`@RestPath` (または何もない) |
URI テンプレートパラメーター (URI Template specification の簡易バージョン)。 詳細は、URI parameters を参照してください。 |
|
クエリーパラメーター |
||
ヘッダ |
HTTP ヘッダ の値 |
|
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 jakarta.ws.rs.POST;
import jakarta.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;
}
}
これについては、@PathParam
、
@QueryParam
、
@HeaderParam
、
@CookieParam
、
@FormParam
または
@MatrixParam
の Jakarta REST アノテーションリンクを使用することもできますが、
パラメーター名を指定する必要があります。
より高度なユースケース向けについては、パラメーターマッピング を参照してください。
Quarkus RESTリクエストパラメータ処理コードで例外が発生した場合、例外はデフォルトでログに出力されません(セキュリティ上の理由から)。このため、特定のHTTPステータスコードが返される理由を理解するのが難しい場合があります(jakarta RESTでは、さまざまなケースで直感的でないエラーコードの使用が義務付けられているため)。このような場合、
|
カスタムクラスでパラメータをグループ化
リクエストパラメータをメソッドパラメータとしてエンドポイントに宣言する代わりに、コンテナクラスでまとめることができます。したがって、前の例を次のように書き換えることができます。
package org.acme.rest;
import jakarta.ws.rs.POST;
import jakarta.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 は、OpenAPIのようなライブラリがパラメータを確認できるように、Jakarta REST仕様に準拠する必要があります。 |
レコードクラスもサポートされているため、前の例をレコードとして書き直すことができます。
public record Parameters(
@RestPath
String type,
@RestMatrix
String variant,
@RestQuery
String age,
@RestCookie
String level,
@RestHeader("X-Cheese-Secret-Handshake")
String secretHandshake,
@RestForm
String smell){}
URI パラメーターの宣言
URI パラメーターを宣言し、パスで正規表現を使用できるため、たとえば、次のエンドポイントは /hello/stef/23
と /hello
のリクエストを処理しますが、 /hello/stef/0x23
は処理しません。
package org.acme.rest;
import jakarta.ws.rs.GET;
import jakarta.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";
}
}
リクエストボディへのアクセス
アノテーションのないメソッドパラメーターはメソッドボディーを受け取ります。body.[1] からマッピングされた後、 HTTP 表現をパラメーターの Java 型に変換します。
次のパラメーター型は、そのままでサポートされます。
タイプ | 使用法 |
---|---|
一時ファイル内のリクエストボディ全体 |
|
|
デコードされていないリクエストボディ全体 |
|
デコードされたリクエストボディ全体 |
デコードされたリクエストボディ全体 |
|
ブロッキングストリームのリクエストボディ |
|
ブロッキングストリームのリクエストボディ |
|
すべての Java プリミティブとそのラッパークラス |
Java プリミティブタイプ。 |
大きな整数と小数 |
|
JSON 値のタイプ |
|
Vert.x Buffer |
|
他のタイプ |
さらにサポートを追加できます body parameter types。 |
マルチパートフォームデータの処理
コンテンツタイプが multipart/form-data
の HTTP リクエストを処理するために、通常の @RestForm
アノテーションを使うことができますが、パーツをファイルとして、あるいはエンティティーとしてアクセスできる特別な型があります。その使用例を見てみましょう。
ファイルアップロード、JSONエンティティ、文字列の説明を含むフォーム値を含むHTTPリクエストを想定すると、次のようなエンドポイントを書くことができます:
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.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 を使用して、任意の Javav の型にリクエストの対応部分をデシリアライズする際の補助とします。特定のパラメーターに special body parameter type を使用する必要がある場合にのみ必要です。
|
他のリクエストパラメータータイプと同様に、これらを container class にまとめるることもできます。 |
ファイルのアップロードを処理する場合、POJO を処理するコード内でファイルを永続的なストレージ (データベース、専用ファイルシステム、クラウドストレージなど) に移動させることが非常に重要です。そうしないと、リクエストが終了したときに、ファイルにアクセスできなくなります。さらに、 quarkus.http.body.delete-uploaded-files-on-end を true に設定すると、HTTP レスポンスの送信時に、アップロードされたファイルが削除されます。この設定を無効にすると、ファイルはサーバーのファイルシステム上 (quarkus.http.body.uploads-directory 設定オプションで定義したディレクトリー) に存在しますが、アップロードされたファイルは UUID ファイル名で保存されて追加のメタデータが保存されないため、これらのファイルは本質的にファイルのランダムダンプとなります。
|
Resourceメソッドが様々なタイプのマルチパートリクエストを処理する必要がある場合、リクエストのすべてのパートにアクセスできるように、 次のコードは、パートに対して反復処理を行い、集約されたデータのリストを返すという簡単な例です:
|
不正な入力の処理
マルチパートボディの読み取りの一環として、Quarkus REST はリクエストの各部分に対して適切な MessageBodyReaderMessageBodyReader
を呼び出します。
これらのパーツのいずれかで IOException
が発生した場合 (たとえば、Jackson が JSON パーツをデシリアライズできなかった場合)、 org.jboss.resteasy.reactive.server.multipart.MultipartPartReadingException
が出力されます。
例外のマッピング に記載されているように、この例外がアプリケーションによって処理されない場合、デフォルトで HTTP 400 応答が返されます。
マルチパート出力
同様に、Quarkus REST は、ユーザーがサーバーからファイルをダウンロードできるようにするための Multipart Form データを生成できます。たとえば、公開する情報を保持する POJO を、次のように記述できます。
import jakarta.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 jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("multipart")
public class Endpoint {
@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("file")
public DownloadFormData getFile() {
// return something
}
}
さらに、クラス MultipartFormDataOutput
を使用して、フォームのパーツを手動で追加することも可能です:
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.server.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;
}
}
この最後の方法は、出力のパーツに追加のヘッダーを追加することができます。
当面、マルチパートデータを返すことはエンドポイントをブロックすることに限定されます。 |
レスポンスボディを返す
HTTP レスポンスを返すには、単にメソッドから必要なリソースを返します。メソッドの戻り値の型とオプションのコンテントタイプをもとに、HTTP レスポンスへのシリアライズ方法を決定します (より詳細な情報については ネゴシエーション を参照ください)。
HTTP response から読み取ることができる定義済みのタイプのいずれかを返すことができ、またその他のタイプは from that type to JSON にマップされます。
さらに、次の戻り値の型もサポートされています。
タイプ | 使用法 |
---|---|
指定されたパスで指定されたファイルの内容 |
|
指定されたパスで指定されたファイルの部分的な内容 |
|
ファイルの部分的な内容 |
|
Vert.x AsyncFile (完全または部分的) |
あるいは、Uni
、Multi
または CompletionStage
など、前述の戻り値の型に対して解決して、reactive type を返すことも可能です。
他のレスポンスプロパティーの設定
手動でレスポンスを設定する
ステータスコードやヘッダーなど、本文だけでなく HTTP レスポンスに多くのプロパティーを設定する必要がある場合は、リソースメソッドからメソッドに org.jboss.resteasy.reactive.RestResponse
を返すようにすることができます。この例は次のようになります。
package org.acme.rest;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.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();
}
}
また、Jakarta REST型 Response を使用することもできますが、
エンティティーに対して厳密に型指定されていません。
|
アノテーションの使用
または、静的な値でステータスコードや HTTP ヘッダーのみを設定する必要がある場合は、それぞれ @org.jboss.resteasy.reactive.ResponseStatus
および/または ResponseHeader
を使用できます。この例は次のようになります。
package org.acme.rest;
import org.jboss.resteasy.reactive.Header;
import org.jboss.resteasy.reactive.ResponseHeader;
import org.jboss.resteasy.reactive.ResponseStatus;
import jakarta.ws.rs.GET;
import jakarta.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 jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.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 の) ink:https://javadoc.io/doc/io.smallrye.reactive/mutiny/2.1.0/io.smallrye.mutiny/io/smallrye/mutiny/Uni.html[Uni
] 型を返すことができます。この場合、現在の HTTP リクエストはメソッドの後で、返された Uni
インスタンスが値に解決され、前述のルールに従って正確にレスポンスにマッピングされるまで自動的に一時停止されます。
package org.acme.rest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.smallrye.mutiny.Uni;
@Path("escoffier")
public class Endpoint {
@GET
public Uni<Book> culinaryGuide() {
return Book.findByIsbn("978-2081229297");
}
}
これにより、書籍がデータベースから取得されている間はイベントループスレッドをブロックせず、書籍がクライアントに送信される準備が整うまでQuarkusがより多くのリクエストに対応し、このリクエストを終了させることができるようになります。詳細は、Execution Model documentation を参照してください。
CompletionStage
戻り値の型もサポートされています。
ストリーミングサポート
レスポンスを要素ごとにストリーミングする場合は、エンドポイントメソッドに (Mutiny の) Multi
型を返すようにすることができます。これは、テキストまたはバイナリーデータのストリーミングに特に役立ちます。
この例では、 Reactive Messaging HTTP を使用して、テキストデータをストリーミングする方法を示しています。
package org.acme.rest;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.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 ステータスコードを設定できるという誤った印象を与えるためです。これは、最初のレスポンスの後では当てはまりません。レスポンスの一部がすでに書き込まれている可能性があるため、例外マッパーも呼び出されません。 |
ヘッダーとステータスのカスタマイズ
カスタムHTTPヘッダーやHTTPレスポンスを設定する必要がある場合は、代わりに次のように org.jboss.resteasy.reactive.RestMulti
を返却します :
package org.acme.rest;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.reactive.messaging.Channel;
import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;
@Path("logs")
public class Endpoint {
@Inject
@Channel("log-out")
Multi<String> logs;
@GET
public Multi<String> streamLogs() {
return RestMulti.fromMultiData(logs).status(222).header("foo", "bar").build();
}
}
ヘッダーやステータスを非同期呼び出しの結果からのみ取得できるようなより高度なケースでは、 RestMulti.fromUniResponse
を使用する必要があります。
このような使用例を次に示します。
package org.acme.rest;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.List;import java.util.Map;import org.eclipse.microprofile.reactive.messaging.Channel;
import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;
@Path("logs")
public class Endpoint {
interface SomeService {
Uni<SomeResponse> get();
}
interface SomeResponse {
Multi<byte[]> data();
String myHeader();
}
private final SomeService someService;
public Endpoint(SomeService someService) {
this.someService = someService;
}
@GET
public Multi<String> streamLogs() {
return RestMulti.fromUniResponse(someService.get(), SomeResponse::data, (r -> Map.of("MyHeader", List.of(r.myHeader()))));
}
}
同時ストリーム要素処理
デフォルトでは、パブリッシャーに通知される需要に 1 の値を使用して、 RestMulti`はラップされた `Multi
要素によって生成されたアイテム/要素のシリアル/連続順序を確保します。
複数のアイテムを同時処理/生成できるようにするには、 withDemand (long demand)
を使用します。
1 より大きい demand を使用すると、複数のアイテムを返す必要があり、各アイテムの生成に時間がかかる場合に有用です。つまり、並列/並行処理によってサービスの応答時間が向上する場合に役立ちます。ただし、並行処理はより多くのリソースを必要とし、アイテムの生成に必要なサービスやリソースに対する負荷が上昇することに注意してください。また、 Multi.capDemandsTo(long)
および Multi.capDemandsUsing(LongFunction)
の使用も検討してください。
以下の例では 5 つの (JSON) 文字列が生成されますが、返される JSON 配列内の文字列の 順序 は 確保されません。以下の例は、単純な型だけでなく、JSON オブジェクトでも機能します。
package org.acme.rest;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;
@Path("message-stream")
public class Endpoint {
@GET
public Multi<String> streamMessages() {
Multi<String> sourceMulti = Multi
.createBy()
.merging()
.streams(
Multi.createFrom().items(
"message-1",
"message-2",
"message-3",
"message-4",
"message-5"
)
);
return RestMulti
.fromMultiData(sourceMulti)
.withDemand(5)
.build();
}
}
応答例、順序は非決定的です。
"message-3"
"message-5"
"message-4"
"message-1"
"message-2"
複数のJSONオブジェクトの返却
デフォルトでは、 RestMulti
は、メディアタイプが application/json
の場合、ラップされた`Multi` によって生成されたアイテム/要素を JSON 配列として返します。JSON 配列にラップされていない個別の JSON オブジェクトを返すには、 encodeAsArray(false)
を使用してください(デフォルトは encodeAsArray(true
です)。この方法で複数のオブジェクトをストリーミングするには、クライアント側で若干異なるパース処理が必要になりますが、オブジェクトは順次パースおよび消費できるため、巨大な結果を一括でデシリアライズする必要はありません。
以下の例では、このように配列にラップされていない5つの(JSON)文字列を生成します:
"message-1"
"message-2"
"message-3"
"message-4"
"message-5"
package org.acme.rest;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;
@Path("message-stream")
public class Endpoint {
@GET
public Multi<String> streamMessages() {
Multi<String> sourceMulti = Multi
.createBy()
.merging()
.streams(
Multi.createFrom().items(
"message-1",
"message-2",
"message-3",
"message-4",
"message-5"
)
);
return RestMulti
.fromMultiData(sourceMulti)
.encodeAsJsonArray(false)
.build();
}
}
サーバー送信イベント (SSE) のサポート
レスポンスにJSONオブジェクトをストリーミングしたい場合は、エンドポイントメソッドに @Produces(MediaType.SERVER_SENT_EVENTS)
アノテーションを付けるだけで、Server-Sent Events を使用することができます。 そして各要素に @RestStreamElementType(MediaType.APPLICATION_JSON)
を指定することで、serialised to JSON されます。
package org.acme.rest;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestStreamElementType;
import io.smallrye.mutiny.Multi;
import org.eclipse.microprofile.reactive.messaging.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;
}
}
さまざまなイベントタイプを区別するために SSE メッセージの event
フィールドを指定する必要がある場合など、カスタマイズされた SSE メッセージを作成すると便利な場合があります。
リソースメソッドは Multi<jakarta.ws.rs.sse.OutboundSseEvent>
を返します。また、挿入された jakarta.ws.rs.sse.Sse
を使用して OutboundSseEvent
インスタンスを作成できます。
package org.acme.rest;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.sse.OutboundSseEvent;
import jakarta.ws.rs.sse.Sse;
import org.jboss.resteasy.reactive.RestStreamElementType;
import io.smallrye.mutiny.Multi;
import org.eclipse.microprofile.reactive.messaging.Channel;
@Path("escoffier")
public class Endpoint {
@Inject
@Channel("book-out")
Multi<Book> books;
@Inject
Sse sse; (1)
@GET
@RestStreamElementType(MediaType.TEXT_PLAIN)
public Multi<OutboundSseEvent> stream() {
return books.map(book -> sse.newEventBuilder() (2)
.name("book") (3)
.data(book.title) (4)
.build());
}
}
1 | OutboundSseEvent を作成するためのサーバー側エントリーポイントを挿入します。 |
2 | 新しい送信イベントビルダーを作成します。 |
3 | イベント名、つまり SSE メッセージの event フィールドの値を設定します。 |
4 | データ、つまり SSE メッセージの data フィールドの値を設定します。 |
SSE 応答を返す場合、応答が利用可能になるまでヘッダーとステータスコードを遅延させることはできないため、返された HTTP ヘッダーとステータスコードを |
HTTP キャッシング機能の制御
Quarkus REST は @Cache
および @NoCache
アノテーションを提供し、
HTTP キャッシュセマンティクス ('Cache-Control' HTTP ヘッダーの設定) を容易にします。
これらのアノテーションはリソースメソッドまたはリソースクラス (この場合、同じアノテーションを 含まない クラスのすべてのリソースメソッドに適用されます) に配置することができ、ユーザーがドメインオブジェクトを返すことができ、明示的に Cache-Control
HTTP ヘッダーを構築することに対処する必要はありません。
@Cache
は複雑な`Cache-Control` ヘッダーを構築しますが、@NoCache
は、キャッシュしない (Cache-Control: nocache
) ことを指示する簡素化された表記です。
Cache-Control ヘッダーの詳細は、 RFC 7234 を参照してください
|
コンテキストオブジェクトへのアクセス
エンドポイントメソッドが次のタイプのパラメーターを受け取る場合、フレームワークが提供するコンテキストオブジェクトは多数あります。
タイプ | 使用法 |
---|---|
すべてのリクエストヘッダー |
|
現在のエンドポイントメソッドとクラスに関する情報 (リフレクションが必要) |
|
現在のユーザーとロールへのアクセス |
|
現在のエンドポイントメソッドとクラスに関する情報 (反映は不要) |
|
現在のエンドポイントとアプリケーション URI に関する情報を提供します |
|
高度: 現在のJakarta RESTのアプリケーションクラス |
|
高度: デプロイされたJakarta RESTアプリケーションに関する設定 |
|
高度: Jakarta RESTプロバイダへの実行時アクセス |
|
詳細: 現在の HTTP メソッドへのアクセスと 前提条件 |
|
詳細: エンドポイントのインスタンスへのアクセス |
|
上級: 現在のリクエスト/レスポンスへの Quarkus REST アクセス |
|
詳細: 複雑な SSE のユースケース |
|
詳細: Vert.xHTTP リクエスト |
|
詳細: Vert..x HTTP レスポンス |
たとえば、現在ログインしているユーザーの名前を返す方法は次のとおりです。
package org.acme.rest;
import java.security.Principal;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.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>";
}
}
これらのコンテキスト・オブジェクトは、https://javadoc.io/static/jakarta.inject/jakarta.inject-api/2.0.1/jakarta.inject/jakarta/inject/Inject.html[ @Inject
] を使って、同じ型のフィールドに注入することもできます:
package org.acme.rest;
import java.security.Principal;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.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 jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.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-rest
をインポートする代わりに、以下のモジュールのいずれかをインポートして、JSON のサポートを受けることができます。
GAV | 使用法 |
---|---|
|
|
|
どちらの場合も (all the types not already registered with a more specific serialisation)、これらのモジュールをインポートすると、HTTP メッセージ本文を JSON から読み取って JSON にシリアル化できます。
Jackson 固有の機能
例外処理
デフォルトでは、Quarkus は MismatchedInputException
には、組み込みの ExceptionMapper
を提供し、HTTP 400 ステータスコードを返します。
エンティティーのシリアル化中の問題について、開発モードとテストモードで適切なエラーメッセージが表示されます。
Jackson に関連するさまざまな例外を一律に処理する必要がある状況があります。たとえば、アプリケーションがすべての この場合の解決策の 1 つとして、次のように設定してください。
これにより、Quarkus は基本的に |
セキュア・シリアライゼーション
JSON をシリアライズするために Jackson を使用する場合、Quarkus REST は、現在のユーザの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;
@SecureField(rolesAllowed = "${role:admin}") (1)
private String address;
public Person(Long id, String first, String last, String address) {
this.id = id;
this.first = first;
this.last = last;
this.address = address;
}
public Long getId() {
return id;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
1 | io.quarkus.resteasy.reactive.jackson.SecureField.rolesAllowed プロパティは、 jakarta.annotation.security.RolesAllowed アノテーションと同じ方法で、プロパティ式 をサポートします。
詳細は、Web エンドポイントの認可ガイド の「標準セキュリティアノテーション」セクションを参照してください。 |
Person
を使用する非常にシンプルな Jakarta REST Resource は次のようになります:
package org.acme.rest;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
@Path("person")
public class PersonResource {
@Path("{id}")
@GET
public Person getPerson(Long id) {
return new Person(id, "foo", "bar", "Brick Lane");
}
@Produces(APPLICATION_JSON) (1)
@Path("/friend/{id}")
@GET
public Response getPersonFriend(Long id) {
var person = new Person(id, "foo", "bar", "Brick Lane");
return Response.ok(person).build();
}
}
1 | @SecureField アノテーションは、生成されたコンテンツタイプが 'application/json' タイプであることを Quarkus が認識した場合にのみ有効です。 |
現在、 io.smallrye.mutiny.Multi リアクティブ型を返すリソースメソッドから返されるデータを保護するために @SecureField アノテーションを使用できません。
|
|
アプリケーションのセキュリティーが設定されていると仮定すると (詳細は ガイド を参照)、 admin
ロールを持つユーザーが /person/1
に対して HTTP GET を実行すると、次のようなメッセージが表示されます。
{
"id": 1,
"first": "foo",
"last": "bar",
"address", "Brick Lane"
}
レスポンスを閉じます。
ただし、 admin
ロールを持たないユーザーは次のものを受け取ります。
{
"first": "foo",
"last": "bar"
}
この安全なシリアライズを行うために、追加の設定を適用する必要はありません。ただし、ユーザーは @io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization および @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization アノテーションを使用して、特定の Jakarta REST Resource クラスまたはメソッドに対するオプトインまたはオプトアウトを行うことができます。
|
SecureField.rolesAllowed プロパティーで設定された設定式は、 @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization アノテーションが使用されている場合でも、アプリケーションの起動時に検証されます。
|
@JsonView support
Jakarta RESTメソッドに @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;
}
このユーザーを返す Jakarta REST メソッドによっては、 id
フィールドをシリアライズから除外したい場合があります。たとえば、安全でないメソッドでは、このフィールドを公開しないようにする場合があります。Quarkus REST で実現する方法は、次の例のとおりです。
@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
を含みます。
@JsonView
is also supported for deserializing the request body. So for example in the following code:
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public RestResponse<User> create(@JsonView(Views.Public.class) User user) {
return RestResponse.status(CREATED, user);
}
inside the create
method, user
would only contain the value of name
while id
would always be null
(regardless if the JSON request contained it)
リフレクションのないJacksonのシリアライズとデシリアライズ
標準の Jackson シリアライズでは、オブジェクトを JSON 表現に変換する際に、大量のリフレクションを使用してオブジェクトを調査します。しかし、Quarkus の基本的な方針としては、可能な限りリフレクションの使用を避け、代わりにビルド時のコード生成を活用することが一般的です。
このため、Jackson の StdSerializer
の実装をビルド時に自動生成し、JSON に変換する各クラスごとに専用のシリアライザを作成することが可能です。生成されたシリアライザは、Quarkus によって実行時に使用され、REST エンドポイントから返されるオブジェクトの JSON シリアライズをリフレクションを一切使用せずに実行できます。
この機能はデフォルトではオフになっていますが、設定プロパティー quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true
を設定することで有効にできます。
メソッドごとに完全にカスタマイズされたシリアル化/デシリアル化
Jakarta REST の各メソッド単位、または各リソース単位で、POJO のシリアライズ/デシリアライズを完全にカスタマイズする必要がある場合があります。
そのようなユースケースでは、REST メソッドやリソースのクラスレベルで @io.quarkus.resteasy.reactive.jackson.CustomSerialization
および @io.quarkus.resteasy.reactive.jackson.CustomDeserialization
アノテーションを使用できます。
これらのアノテーションを利用することで、 com.fasterxml.jackson.databind.ObjectWriter
や com.fasterxml.jackson.databind.ObjectReader
を完全に設定できます。
以下は、 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)
を使用する Jakarta REST メソッドのシリアライズに対してのみ実行されることに注意する必要があります。
前の例に従って、フィールド名が引用符で囲まれていない JSON リクエストを読み取るように com.fasterxml.jackson.databind.ObjectReader
をカスタマイズしてみましょう。
@CustomDeserialization(SupportUnquotedFields.class)
@POST
@Path("/use-of-custom-deserializer")
public void useOfCustomSerializer(User request) {
// ...
}
ここで、 SupportUnquotedFields
は次のように定義された BiFunction
です。
public static class SupportUnquotedFields implements BiFunction<ObjectMapper, Type, ObjectReader> {
@Override
public ObjectReader apply(ObjectMapper objectMapper, Type type) {
return objectMapper.reader().with(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES);
}
}
XML シリアライゼーション
XML サポートを有効にするには、プロジェクトに quarkus-rest-jaxb
エクステンションを追加します。
GAV | 使用法 |
---|---|
|
all the types not already registered with a more specific serialisation の場合、このモジュールをインポートすると、HTTP メッセージ本文を XML から読み取って XML にシリアライズできます。
JAXB Quarkus REST エクステンションは、リソースで使用され、JAXB シリアル化を必要とするクラスを自動的に検出します。次に、これらのクラスを、JAXB メッセージリーダーとライターによって内部的に使用されるデフォルトの JAXBContext
に登録します。
しかし、状況によっては、これらのクラスが原因で JAXBContext
が失敗することがあります。例えば、異なる Java パッケージで同じクラス名を使用している場合などです。このような場合、アプリケーションはビルド時に失敗し、問題の原因となったJAXB例外が出力されるので、適切に修正することができます。また、 quarkus.jaxb.exclude-classes
というプロパティを使用して、問題の原因となるクラスを除外することもできます。任意のリソースで必要とされるクラスを除外する場合、JAXB Quarkus REST エクステンションは、除外されたクラスを含むカスタム JAXBContext
を作成およびキャッシュするため、パフォーマンスの低下は最小限に抑えられます。
たとえば、
|
高度な JAXB 固有の機能
quarkus-resteasy-reactive-jaxb
エクステンションを使用する場合、Quarkus REST がサポートする高度な機能がいくつかあります。
JAXB コンポーネントを注入する
JAXB Quarkus REST エクステンションは、ユーザーに対して透過的にリクエストとレスポンスをシリアライズおよびアンシリアライズします。ただし、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-rest-links
エクステンションを追加します。
GAV | 使用法 |
---|---|
|
このモジュールをインポートすると、エンドポイントのリソースに @InjectRestLinks
アノテーションを付与するだけで、HTTP レスポンスヘッダーに Web リンクを挿入できるようになります。
返される Web リンクを宣言するには、リンク先となるメソッドに @RestLink
アノテーションを使用する必要があります。
Record
の場合は以下のようになります。
public class Record {
// The class must contain/inherit either and `id` field, an `@Id` or `@RestLinkId` annotated field.
// When resolving the id the order of preference is: `@RestLinkId` > `@Id` > `id` field.
private int id;
public Record() {
}
protected Record(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
Web リンクのサポートを有効にする例は次のようになります。
@Path("/records")
public class RecordsResource {
@GET
@RestLink(rel = "list")
@InjectRestLinks
public List<Record> getAll() {
// ...
}
@GET
@Path("/{id}")
@RestLink(rel = "self")
@InjectRestLinks(RestLinkType.INSTANCE)
public Record get(@PathParam("id") int id) {
// ...
}
@PUT
@Path("/{id}")
@RestLink
@InjectRestLinks(RestLinkType.INSTANCE)
public Record update(@PathParam("id") int id) {
// ...
}
@DELETE
@Path("/{id}")
@RestLink
public Record 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)
// ...
}
// ...
}
このリソースに対して生成された Web リンクは 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 ハイパーテキストアプリケーション言語 (HAL) のサポート
HAL 標準は、Web リンクを表す単純な形式です。
HAL のサポートを有効にするには、 quarkus-hal
エクステンションをプロジェクトに追加します。また、 HAL は JSON サポートを必要とするため、 quarkus-rest-jsonb
または quarkus-rest-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 Record 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<T>
(エンティティのリスト) または HalEntityWrapper<T>
(単一オブジェクト) を返すことで、リソースにプログラム的に追加の HAL リンクを提供することも可能です。
@Path("/records")
public class RecordsResource {
@Inject
HalService halService;
@GET
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@RestLink(rel = "list")
public HalCollectionWrapper<Record> getAll() {
List<Record> list = // ...
HalCollectionWrapper<Record> halCollection = halService.toHalCollectionWrapper( list, "collectionName", 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<Record> get(@PathParam("id") int id) {
Record entity = // ...
HalEntityWrapper<Record> halEntity = halService.toHalWrapper(entity);
halEntity.addLinks(Link.fromPath("/records/1/parent").rel("parent-record").build());
return halEntity;
}
}
CORS フィルター
クロスオリジンリソース共有 (CORS)は、ウェブページ上の制限されたリソースを、最初のリソースが提供されたドメイン以外の別のドメインから要求できるようにするメカニズムです。
Quarkus には、HTTP レイヤーレベルの CORS フィルターが含まれています。 CORS フィルターとその使用方法の詳細は、Quarkus のクロスオリジンリソース共有ガイドの CORS フィルター セクションを参照してください。
より高度な使用法
ここでは、最初は知る必要がないかもしれませんが、より複雑なユースケースに役立つ可能性のある、より詳細なトピックをいくつか紹介します。
実行モデル、ブロッキング、ノンブロッキング
Quarkus REST は、主に 2 つのスレッドタイプを使用して実装されます。
-
イベントループスレッド: HTTPリクエストからのバイトの読み込みとHTTPレスポンスへのバイトの書き戻しを担当します。
-
ワーカースレッド: プールされ、長時間実行される操作の負荷を軽減するために使用できます
イベントループスレッド (IO スレッドとも呼ばれます) は、すべての IO 操作を非同期で実際に実行し、それらの IO 操作の完了に関心のあるリスナーをトリガーする役割を果たします。
デフォルトでは、Quarkus REST スレッドは、メソッドのシグネチャーに応じてエンドポイントメソッドを実行します。メソッドが次のタイプのいずれかを返す場合、それは非ブロッキングと見なされ、デフォルトで IO スレッドで実行されます。
-
io.smallrye.mutiny.Uni
-
io.smallrye.mutiny.Multi
-
java.util.concurrent.CompletionStage
-
org.reactivestreams.Publisher
-
Kotlin の
suspended
メソッド
この「最善の推測」アプローチは、ほとんどの操作がデフォルトで正しいスレッドで実行されることを意味します。リアクティブなコードを書いている場合、メソッドは通常これらの型のいずれかを返し、IOスレッドで実行されます。ブロッキングコードを書いている場合、メソッドは通常、結果を直接返し、これらはワーカースレッドで実行されます。
@Blocking と @NonBlocking アノテーションを使用してこの動作をオーバーライドすることができます。これは、メソッド、クラス、 jakarta.ws.rs.core.Application
の各レベルで適用することができます。
以下の例は、デフォルトの動作をオーバーライドし、 Uni
を返しても、常にワーカースレッドで実行されます。
package org.acme.rest;
import jakarta.ws.rs.GET;
import jakarta.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 jakarta.ws.rs.GET;
import jakarta.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));
}
}
メソッドやクラスに jakarta.transaction.Transactional
のアノテーションを付けると、そのメソッドもブロッキングメソッドとして扱われます。これは、JTAがブロッキング技術であり、一般的にHibernateやJDBCなどの他のブロッキング技術と共に使用されるためです。クラスに明示的に @Blocking
または @NonBlocking
を付けると、この動作は上書きされます。
例外のマッピング
もしアプリケーションがエラー時に正規の HTTP コード以外を返す必要がある場合、最適な方法として、フレームワークが WebApplicationException
またはその継承型を使用して適切な HTTP レスポンスを送信する結果となる例外を出力します。
package org.acme.rest;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.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";
}
}
出力された ` WebApplicationException ` 例外のログレベルを変更するには、次のようにプロパティー
|
エンドポイントメソッドが、Jakarta REST を認識していない、別のサービスレイヤーに呼び出しを移譲する場合、HTTP レスポンスにサービスの例外を変換する手段が必要です。また、メソッドに
@ServerExceptionMapper
アノテーションを付けて、処理する例外タイプのパラメーターを 1 つ指定し、その例外を RestResponse
(または
Uni<RestResponse<?>>
) に変換します。
package org.acme.rest;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.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);
}
}
デフォルトでは、 ただし、ユーザーはメソッドに対応するアノテーションを追加することでインターセプターを選択できます。 |
例外を ただし、Java の一部の例外タイプは、他の例外のラッパーとしてのみ機能します。多くの場合、チェック済み例外は、メソッドの 例外マッパーがラッパー例外のいずれかによってラップされている場合でも、例外タイプに対して例外マッパーが呼び出されるようにする場合は、例外ラッパータイプで
その例外ラッパータイプを制御しない場合は、任意のクラスにアノテーションを配置し、適用される例外ラッパータイプをアノテーションパラメーターとして指定できます。
|
REST エンドポイントクラスで定義された例外マッパーは、例外が同じクラスで出力された場合にのみ呼び出されます。グローバル例外マッパーを定義する場合は、REST エンドポイントクラスの外部で定義するだけです。
Jakarta REST の方法で例外マッパー を宣言することもできます。 |
例外マッパーは、次のパラメーター型のいずれかを宣言できます。
タイプ | 使用法 |
---|---|
例外型 |
処理する例外型を定義します |
Context objects のいずれか |
|
現在のリクエストにアクセスするためのコンテキストオブジェクト |
次の戻り値の型のいずれかを宣言できます。
タイプ | 使用法 |
---|---|
|
例外が発生したときにクライアントに送信するレスポンス |
例外が発生したときにクライアントに送信する非同期レスポンス |
例外が発生した場合、Quarkus REST はデフォルトでログを記録しません (セキュリティー上の理由による)。このため、特定の例外処理コードが呼び出された (または呼び出されなかった) 理由を理解するのが難しい場合があります。Quarkus REST が例外マッピングコードを実行する前に実際の例外をログ記録するには、次のように
|
リクエストまたはレスポンスフィルター
アノテーション経由
リクエスト処理の次のフェーズで呼び出される関数を宣言できます。
-
エンドポイントメソッドが識別される前: 事前マッチングリクエストフィルター
-
ルーティング後、エンドポイントメソッドが呼び出される前: 通常のリクエストフィルター
-
エンドポイントメソッドが呼び出された後: レスポンスフィルター
これらのフィルターを使用すると、リクエスト 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());
}
}
}
このような応答フィルターは、handled 例外でも呼び出されます。
フィルターは、次のパラメーター型のいずれかを宣言できます。
タイプ | 使用法 |
---|---|
Context objects のいずれか |
|
現在のリクエストにアクセスするためのコンテキストオブジェクト |
|
現在のレスポンスにアクセスするためのコンテキストオブジェクト |
|
出力例外および handled の例外または |
次の戻り値の型のいずれかを宣言できます。
タイプ | 使用法 |
---|---|
フィルターチェーンを続行する代わりにクライアントに送信するレスポンス、またはフィルターチェーンを続行する必要がある場合は null |
|
フィルターチェーンを続行する代わりにクライアントに送信するオプションのレスポンス、またはフィルターチェーンを続行する必要がある場合は空の値 |
|
フィルターチェーンを続行する代わりにクライアントに送信する非同期レスポンス、またはフィルターチェーンを続行する必要がある場合は null |
@NameBinding メタアノテーションを使用して、フィルターが実行されるリソースメソッドを制限できます。
|
ジャカルタRESTの方式
HTTP リクエストとレスポンスの両方とも、それぞれ ContainerRequestFilter
または ContainerResponseFilter
の実装を提供することで、 インターセプトすることができます。これらのフィルタは、メッセージに関連付けられたメタデータを処理するのに適しています。HTTP ヘッダ、クエリパラメーター、メディアタイプ、その他のメタデータです。また、ユーザーがエンドポイントにアクセスする権限を持っていない場合など、リクエスト処理を中止する機能も持っています。
ContainerRequestFilter
を使用して、サービスにロギング機能を追加してみましょう。 ContainerRequestFilter
を実装して、 @Provider
アノテーションをつけることで実現できます。
package org.acme.rest.json;
import io.vertx.core.http.HttpServerRequest;
import org.jboss.logging.Logger;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.UriInfo;
import jakarta.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
を返す場合)、Quarkus REST は、オブジェクトなどを HTTP レスポンス本文にマッピングする方法を探します。
同様に、エンドポイントメソッドがオブジェクトをパラメーターとして受け取る場合は常に、HTTP リクエストの本文をそのオブジェクトにマッピングする方法を探します。
これは、MessageBodyReader
および MessageBodyWriter
インターフェイスのプラグ可能なシステムを介して行われます。
これらは、どの Java 型から/にマッピングするか、どのメディア型にマッピングするか、
HTTP 本文をそのタイプの Java インスタンスにどのように変換するか、そのタイプの Java インスタンスから HTTP 本文をどのように変換するかを定義します。
たとえば、エンドポイントに独自の Cheese
型がある場合:
package org.acme.rest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.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 jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.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 jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.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
or
WriterInterceptor
を拡張することでリーダーとライターをインターセプトできます。
このエンドポイントを見ると、次のようになります。
package org.acme.rest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.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 jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.ext.Provider;
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.ReaderInterceptorContext;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.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;
}
}
Quarkus REST と REST クライアントのインタラクション
Quarkusでは、Quarkus REST エクステンションと REST Clientエクステンション は、同じインフラストラクチャを共有しています。この考慮の重要な結果 の1 つは、(Jakarta REST の意味での)プロバイダーのリストを共有することです。
例えば、 WriterInterceptor
を宣言した場合、デフォルトではサーバーの呼び出しとクライアントの呼び出しの両方をインターセプトしますが、これは望ましい動作ではないかもしれません。
しかし、このデフォルトの動作を変更してプロバイダを制約することができます。
-
プロバイダに
@ConstrainedTo(RuntimeType.SERVER)
アノテーションを追加することで、 サーバー コールのみを考慮します。 -
プロバイダに
@ConstrainedTo(RuntimeType.CLIENT)
アノテーションを追加することで、 クライアント コールのみを考慮します。
パラメーターマッピング
Request Parameters はすべて、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 jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.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;
}
}
クエリーパラメーター値の分離
通常、 String
値のコレクションは、同じクエリーパラメーターが複数回出現する際に使用される値を取得するために使用されます。
たとえば、次のリソースメソッドの場合:
@Path("hello")
public static class HelloResource {
@GET
public String hello(@RestQuery("name") List<String> names) {
if (names.isEmpty()) {
return "hello world";
} else {
return "hello " + String.join(" ", names);
}
}
}
以下のリクエストの場合:
GET /hello?name=foo&name=bar HTTP/1.1
names
変数には foo
と bar
の両方が含まれ、応答は hello foo bar
になります。
ただし、何らかの区切り文字に基づいて、単一のクエリーパラメーターを、値のコレクションに変換する必要があることは珍しくありません。このような場合に、 @org.jboss.resteasy.reactive.Separator
アノテーションが役立ちます。
リソースメソッドを次のように更新します。
@Path("hello")
public static class HelloResource {
@GET
public String hello(@RestQuery("name") @Separator(",") List<String> names) {
if (names.isEmpty()) {
return "hello world";
} else {
return "hello " + String.join(" ", names);
}
}
}
次のリクエストを使用します。
GET /hello?name=foo,bar HTTP/1.1
応答は hello foo bar
になります。
前提条件
以下のようないくつかの条件の下で、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 jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.EntityTag;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response;
import jakarta.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 jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.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 、 application/json 、 application/graphql+json 、および application/xhtml+xml のメディアタイプが圧縮されます。
|
クライアントが HTTP 圧縮をサポートしていない場合、レスポンスボディは圧縮されません。 |
Jakarta RESTクラスのインクルード/エクスクルード
ビルドタイム条件の使用
Quarkusでは、CDI Beanと同様に、ビルド時の条件によって、Jakarta RESTリソース、プロバイダ、フィーチャーを直接取り込んだり除外したりすることができます。したがって、さまざまなJakarta RESTクラスにプロファイル条件( @io.quarkus.arc.profile.IfBuildProfile
または @io.quarkus.arc.profile.UnlessBuildProfile
)やプロパティ条件( io.quarkus.arc.properties.IfBuildProperty
または io.quarkus.arc.properties.UnlessBuildProperty
)をアノテーションして、ビルド時にQuarkusに対して、どのような条件の下でJakarta RESTクラスを含めるべきかを示すことができます。
次の例では、Quarkusは、ビルドプロファイル app1
が有効になっている場合に限り、 ResourceForApp1Only
リソースを含めます。
@IfBuildProfile("app1")
public class ResourceForApp1Only {
@GET
@Path("sayHello")
public String sayHello() {
return "hello";
}
}
Jakarta REST Applicationが検出され、メソッド getClasses()
、 getSingletons()
がオーバーライドされている場合、Quarkusはビルド時の条件を無視し、Jakarta REST Applicationで定義されているもののみを考慮することに注意してください。
実行時プロパティの使用
Quarkusでは、 @io.quarkus.resteasy.reactive.server.EndpointDisabled
アノテーションを使用して、実行時プロパティの値に基づいてjakarta RESTリソースを条件付きで無効化することもできます。
次の例では、アプリケーションで some.property
が "disable"
に設定されている場合、Quarkus は実行時に RuntimeResource
を除外します。
@EndpointDisabled(name = "some.property", stringValue = "disable")
public class RuntimeResource {
@GET
@Path("sayHello")
public String sayHello() {
return "hello";
}
}
この機能はネイティブビルドでは動作しません。 |
RESTクライアント
Quarkus RESTには、サーバー側だけでなく、新しいMicroProfile RESTクライアントの実装も含まれています。
quarkus-resteasy-client
エクステンションモジュールは Quarkus REST では使用できませんので、代わりに quarkus-rest-client
を使用してください。
REST クライアントの詳細については、 REST クライアントガイド を参照してください。