The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
このページを編集

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が提供する機能の上に多くの新機能を備えています。

エンドポイントの作成

はじめに

次のインポートをビルドファイルに追加します:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest</artifactId>
</dependency>
build.gradle
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

REpresentational State Transfer

エンドポイント

REST 呼び出しを処理するために呼び出される Java メソッド

URL / URI (Uniform Resource Locator / Identifier)

REST リソースの場所を特定するために使用 (仕様)

リソース

ドメインオブジェクトを表します。これは、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プレフィックスを定義します。これは、空であるか、または restrest/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 {

}

This will cause all rest endpoints to be resolve relative to /api, so the endpoint above with @Path("rest") would be accessible at /api/rest/. You can also set the quarkus.rest.path build time property to set the root path if you don’t want to use an annotation.

エンドポイントの宣言: HTTP メソッド

各エンドポイントメソッドには、次のいずれかのアノテーションを付ける必要があります。これにより、どの HTTP メソッドがそのメソッドにマップされるかが定義されます。

Table 1. HTTPメソッドアノテーション
アノテーション 使用方法

@GET

リソース表現を取得します。状態を変更しないでください。idempotent (HTTP docs)

@HEAD

ボディのない GET と同様に、リソースに関するメタデータを取得します (HTTP docs)

@POST

リソースを作成し、そのリソースへのリンクを取得します (HTTP docs)

@PUT

リソースを置き換えるか作成します。idempotent (HTTP docs) であるべきです。

@DELETE

既存のリソースを削除します。idempotent (HTTP docs)

@OPTIONS

リソースに関する情報を取得します。idempotent (HTTP docs) です。

@PATCH

リソースを更新するか、作成します。idempotent (HTTP docs) ではありません。

また、他のメソッドを @HttpMethod アノテーションを使用して宣言することで宣言することも出来ます。

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 endpointapplication/json

  • 任意のテキストメディアタイプのサブタイプワイルドカードである text/*

  • 任意のメディアタイプのワイルドカードである */*

エンドポイントクラスに@Produces または @Consumes アノテーションをつけることができます。 これらを使用し、エンドポイントが HTTP リクエストボディとして受け入れ、HTTP レスポンスボディとして生成するメディアタイプを 1 つまたは複数指定できます。これらのクラスアノテーションは、各メソッドに適用されます。

また、@Produces@Consumes アノテーションをメソッドにつけることもできます。そのアノテーションは最終的にクラスアノテーションをオーバーライドします。

MediaType クラスには、あらかじめ定義された特定のメディアタイプを指し示すために使用できる多くの定数があります。

See the ネゴシエーション section for more information.

リクエストパラメーターへのアクセス

パラメーター名の情報を生成するために、-parameters (javac) または <parameters> または <maven.compiler.parameters> (Maven) を使ってコンパイラーを設定することを忘れないでください。

次の HTTP リクエスト要素は、エンドポイントメソッドによって取得される可能性があります。

Table 2. HTTP request parameter annotations
HTTP 要素 アノテーション 使用方法

パスパラメーター

@RestPath (あるいは無指定)

URI テンプレートパラメーター (URI Template specification の簡易バージョン)。 詳細は、URI パラメーター を参照してください。

クエリーパラメーター

@RestQuery

URI クエリーパラメーター の値

ヘッダ

@RestHeader

HTTP ヘッダ の値

Cookie

@RestCookie

HTTP cookie の値

フォームパラメーター

@RestForm

HTTP URL-encoded FORM の値

マトリックスパラメーター

@RestMatrix

URI パスセグメントパラメーター の値

これらのアノテーションはそれぞれ、参照する要素の名前を指定することができ、指定していない場合は、アノテーションされたメソッドのパラメーターの名前が使用されます。

クライアントが次の 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;
    }
}
@RestPath アノテーションはオプションです。名前が既存のURIテンプレート変数と一致するパラメータは、自動的に @RestPath を持つと想定されます。

また、これに対してJakarta RESTアノテーション @PathParam または @QueryParam , @HeaderParam , @CookieParam , @FormParam ,@MatrixParam のいずれかを使用することができます。ただし、パラメータ名を指定する必要があります。

より詳細なユースケースは、 パラメーターマッピング を参照してください。

When an exception occurs in Quarkus REST request parameter handling code, the exception is not printed by default to the log (for security reasons). This can sometimes make it hard to understand why certain HTTP status codes are returned (as the Jakarta REST mandates the use of non-intuitive error codes in various cases). In such cases, users are encouraged to set the logging level for the org.jboss.resteasy.reactive.server.handlers.ParameterHandler category to DEBUG like so:

quarkus.log.category."org.jboss.resteasy.reactive.server.handlers.ParameterHandler".level=DEBUG

カスタムクラスでパラメータをグループ化

リクエストパラメータをメソッドパラメータとしてエンドポイントに宣言する代わりに、コンテナクラスでまとめることができます。したがって、前の例を次のように書き換えることができます。

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仕様に準拠する必要があります。

Record classes are also supported, so you could rewrite the previous example as a record:

    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){}

Declaring URI parameters

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

リクエストボディへのアクセス

アノテーションのないメソッドパラメーターは、HTTP 表現からパラメーターの Java タイプにマッピングされた後、それが URI template parameter または context object でない限り、メソッド本文の脚注を受け取ります。

次のパラメーター型は、そのままでサポートされます。

Table 3. Request body parameter types
タイプ 使用方法

File

一時ファイル内のリクエストボディ全体

byte[]

デコードされていないリクエストボディ全体

char[]

デコードされたリクエストボディ全体

String

デコードされたリクエストボディ全体

InputStream

ブロッキングストリームのリクエストボディ

Reader

ブロッキングストリームのリクエストボディ

すべての Java プリミティブとそのラッパークラス

Java プリミティブ型

BigDecimal, BigInteger

大きな整数と小数

JsonArray, JsonObject, JsonStructure, JsonValue

JSON 値の型

Buffer

Vert.x Buffer

他の型

mapped from JSON to that type になります

さらに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
    }
}

The description parameter will contain the data contained in the part of HTTP request called description (because @RestForm does not define a value, the field name is used), while the file parameter will contain data about the uploaded file in the image part of HTTP request, and the person parameter will read the Person entity using the JSON body reader.

マルチパートリクエストの各パートのサイズは、 quarkus.http.limits.max-form-attribute-size の値に準拠する必要があり、そのデフォルトは 2048 バイトです。この設定を超えるサイズのパートを持つリクエストは、HTTPステータスコード413となります。

FileUpload はアップロードされたファイルの様々なメタデータにアクセスすることができます。しかし、アップロードされたファイルへの処理が必要なだけなら、java.nio.file.Pathjava.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.
Just like for any other request parameter type, you can also group them into a container class.
ファイルのアップロードを処理する場合、POJO を処理するコード内でファイルを永続的なストレージ (データベース、専用ファイルシステム、クラウドストレージなど) に移動させることが非常に重要です。そうしないと、リクエストが終了したときに、ファイルにアクセスできなくなります。さらに、quarkus.http.body.delete-uploaded-files-on-end を true に設定すると、HTTP レスポンスの送信時に、アップロードされたファイルが削除されます。この設定を無効にすると、ファイルはサーバーのファイルシステム上 (quarkus.http.body.uploads-directory 設定オプションで定義したディレクトリー) に存在しますが、アップロードされたファイルは UUID ファイル名で保存されて追加のメタデータが保存されないため、これらのファイルは本質的にファイルのランダムダンプとなります。

Resourceメソッドが様々なタイプのマルチパートリクエストを処理する必要がある場合、リクエストのすべてのパートにアクセスできるように、 org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput メソッドタイプを使用することができます。

次のコードは、パートに対して反復処理を行い、集約されたデータのリストを返すという簡単な例です:

@Path("/test")
public static class Resource {

    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces(MediaType.APPLICATION_JSON)
    public List<Item> hello(MultipartFormDataInput input) throws IOException {
        Map<String, Collection<FormValue>> map = input.getValues();
        List<Item> items = new ArrayList<>();
        for (var entry : map.entrySet()) {
            for (FormValue value : entry.getValue()) {
                items.add(new Item(
                        entry.getKey(),
                        value.isFileItem() ? value.getFileItem().getFileSize() : value.getValue().length(),
                        value.getCharset(),
                        value.getFileName(),
                        value.isFileItem(),
                        value.getHeaders()));
            }

        }
        return items;
    }

    public static class Item {
        public final String name;
        public final long size;
        public final String charset;
        public final String fileName;
        public final boolean isFileItem;
        public final Map<String, List<String>> headers;

        public Item(String name, long size, String charset, String fileName, boolean isFileItem,
                Map<String, List<String>> headers) {
            this.name = name;
            this.size = size;
            this.charset = charset;
            this.fileName = fileName;
            this.isFileItem = isFileItem;
            this.headers = headers;
        }
    }
}

不正な入力の処理

As part of reading the multipart body, Quarkus REST invokes the proper MessageBodyReaderMessageBodyReader for each part of the request. If an IOException occurs for one of these parts (for example if Jackson was unable to deserialize a JSON part), then a org.jboss.resteasy.reactive.server.multipart.MultipartPartReadingException is thrown. If this exception is not handled by the application as mentioned in 例外のマッピング, an HTTP 400 response is returned by default.

マルチパート出力

Similarly, Quarkus REST can produce Multipart Form data to allow users download files from the server. For example, we could write a POJO that will hold the information we want to expose as:

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

この最後の方法は、出力のパーツに追加のヘッダーを追加することができます。

当面、マルチパートデータを返すことはエンドポイントをブロックすることに限定されます。

レスポンスボディを返す

In order to return an HTTP response, simply return the resource you want from your method. The method return type and its optional content type will be used to decide how to serialise it to the HTTP response (see the ネゴシエーション section for more advanced information).

HTTP レスポンス から読み取れる、あらかじめ定義された任意の型を返すことができ、それ以外の型は型からJSON にマッピングされます。

さらに、次の戻り値の型もサポートされています。

Table 4. Additional response body parameter types
タイプ 使用方法

Path

指定されたパスで指定されたファイルの内容

PathPart

指定されたパスで指定されたファイルの部分的な内容

FilePart

ファイルの部分的な内容

AsyncFile

Vert.x AsyncFile (完全または部分的)

あるいは、 UniMulti または CompletionStage など、前述の戻り値の型に解決して、リアクティブ型 を返すことも可能です。

他のレスポンスプロパティーの設定

手動でレスポンスを設定する

ステータスコードやヘッダーなど、本文だけでなく 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);
    }
}

RedirectExceptionの使用

Users can also throw jakarta.ws.rs.RedirectionException from a method body to get Quarkus REST to perform the desired redirect.

非同期/リアクティブサポート

エンドポイントメソッドがレスポンスする前に非同期またはリアクティブタスクを実行する必要がある場合は、メソッドを宣言して( Mutiny の) 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");
    }
}

This allows you to not block the event-loop thread while the book is being fetched from the database, and allows Quarkus to serve more requests until your book is ready to be sent to the client and terminate this request. See Execution Model documentation for more information.

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

In more advanced cases where the headers and / or status can only be obtained from the results of an async call, the RestMulti.fromUniResponse needs to be used. Here is an example of such a use case:

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

Concurrent stream element processing

By default, RestMulti ensures serial/sequential order of the items/elements produced by the wrapped Multi by using a value of 1 for the demand signaled to the publishers. To enable concurrent processing/generation of multiple items, use withDemand(long demand).

Using a demand higher than 1 is useful when multiple items shall be returned and the production of each item takes some time, i.e. when parallel/concurrent production improves the service response time. Be aware the concurrent processing also requires more resources and puts a higher load on services or resources that are needed to produce the items. Also consider using Multi.capDemandsTo(long) and Multi.capDemandsUsing(LongFunction).

The example below produces 5 (JSON) strings, but the order of the strings in the returned JSON array is not guaranteed. The below example also works for JSON objects and not just simple types.

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

Example response, the order is non-deterministic.

"message-3"
"message-5"
"message-4"
"message-1"
"message-2"

複数のJSONオブジェクトの返却

By default, RestMulti returns items/elements produced by the wrapped Multi as a JSON array, if the media-type is application/json. To return separate JSON objects that are not wrapped in a JSON array, use encodeAsArray(false) (encodeAsArray(true) is the default). Note that streaming multiple objects this way requires a slightly different parsing on the client side, but objects can be parsed and consumed as they appear without having to deserialize a possibly huge result at once.

以下の例では、このように配列にラップされていない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) のサポート

If you want to stream JSON objects in your response, you can use Server-Sent Events by just annotating your endpoint method with @Produces(MediaType.SERVER_SENT_EVENTS) and specifying that each element should be serialised to JSON with @RestStreamElementType(MediaType.APPLICATION_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;
    }
}

Sometimes it’s useful to create a customized SSE message, for example if you need to specify the event field of a SSE message to distinguish various event types. A resource method may return Multi<jakarta.ws.rs.sse.OutboundSseEvent> and an injected jakarta.ws.rs.sse.Sse can be used to create OutboundSseEvent instances.

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 Inject the server-side entry point for creating OutboundSseEvents.
2 Create a new outbound event builder.
3 Set the event name, i.e. the value of the event field of a SSE message.
4 Set the data, i.e. the value of the data field of a SSE message.

Manipulation of the returned HTTP headers and status code is not possible via RestMulti.fromUniResponse because when returning SSE responses the headers and status code cannot be delayed until the response becomes available.

HTTP キャッシング機能の制御

Quarkus REST provides the @Cache and @NoCache annotations to facilitate handling HTTP caching semantics, i.e. setting the 'Cache-Control' HTTP header.

これらのアノテーションはリソースメソッドまたはリソースクラス (この場合、同じアノテーションを 含まない クラスのすべてのリソースメソッドに適用されます) に配置することができ、ユーザーがドメインオブジェクトを返すことができ、明示的に Cache-Control HTTP ヘッダーを構築することに対処する必要はありません。

@Cache が複雑な Cache-Control ヘッダーを構築するのに対し、@NoCache は何もキャッシュさせたくない、つまり Cache-Control: nocache というシンプルな表記法になっています。

Cache-Control ヘッダーの詳細は、 RFC 7234 を参照してください

コンテキストオブジェクトへのアクセス

エンドポイントメソッドが次のタイプのパラメーターを受け取る場合、フレームワークが提供するコンテキストオブジェクトは多数あります。

Table 5. コンテキスト・オブジェクト
タイプ 使用方法

HttpHeaders

すべてのリクエストヘッダー

ResourceInfo

現在のエンドポイントメソッドとクラスに関する情報 (リフレクションが必要)

SecurityContext

現在のユーザーとロールへのアクセス

SimpleResourceInfo

現在のエンドポイントメソッドとクラスに関する情報 (反映は不要)

UriInfo

現在のエンドポイントとアプリケーション URI に関する情報を提供します

Application

高度: 現在のJakarta RESTのアプリケーションクラス

Configuration

高度: デプロイされたJakarta RESTアプリケーションに関する設定

Providers

高度: Jakarta RESTプロバイダへの実行時アクセス

Request

詳細: 現在の HTTP メソッドへのアクセスと 条件

ResourceContext

詳細: エンドポイントのインスタンスへのアクセス

ServerRequestContext

Advanced: Quarkus REST access to the current request/response

Sse

詳細: 複雑な SSE のユースケース

HttpServerRequest

詳細: Vert.xHTTP リクエスト

HttpServerResponse

詳細: 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 シリアライゼーション

Instead of importing io.quarkus:quarkus-rest, you can import either of the following modules to get support for JSON:

GAV 使用方法

io.quarkus:quarkus-rest-jackson

Jackson サポート

io.quarkus:quarkus-rest-jsonb

JSON-B サポート

いずれの場合も、これらのモジュールをインポートすることで、HTTPメッセージボディをJSONから読み取ったり、より具体的なシリアライズ方法が登録されていない型 に JSONをシリアライズできるようになります。

Jackson-specific features

例外処理

By default, Quarkus provides a built-in ExceptionMapper for MismatchedInputException which returns an HTTP 400 status code along with a good error message in Dev and Test modes, about what went wrong during serialization of an entity.

There are situations where various Jackson related exceptions need to handled in a uniform way.For example, the application may need to handle all JsonMappingException the same way. This becomes a problem when taking JAX-RS / Jakarta REST rules into account, because the exception mapper ExceptionMapper for MismatchedInputException would be used instead of the user provide ExceptionMapper for JsonMappingException (as MismatchedInputException is a subtype of JsonMappingException).

One solution for this case is to configure the following:

quarkus.class-loading.removed-resources."io.quarkus\:quarkus-rest-jackson"=io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.class

which essentially makes Quarkus ignore the ExceptionMapper for MismatchedInputException completely.

セキュア・シリアライゼーション

When used with Jackson to perform JSON serialization, Quarkus REST provides the ability to limit the set of fields that are serialized based on the roles of the current user. This is achieved by simply annotating the fields (or getters) of the POJO being returned with @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 The io.quarkus.resteasy.reactive.jackson.SecureField.rolesAllowed property supports property expressions exactly in the same fashion the jakarta.annotation.security.RolesAllowed annotation does. For more information, please refer to the Standard security annotations section of the Authorization of web endpoints guide.

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 The @SecureField annotation is only effective when Quarkus recognizes that produced content type is the 'application/json' type.
Currently you cannot use the @SecureField annotation to secure your data returned from resource methods returning the io.smallrye.mutiny.Multi reactive type.

All resource methods returning data secured with the @SecureField annotation should be tested. Please make sure data are secured as you intended. Quarkus always attempts to detect fields annotated with the @SecureField annotation, however it may fail to infer returned type and miss the @SecureField annotation instance. If that happens, please explicitly enable secure serialization on the resource endpoint with the @EnableSecureSerialization annotation.

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",
  "address", "Brick Lane"
}

レスポンスを閉じます。

ただし、admin ロールを持たないユーザーは次のものを受け取ります。

{
  "first": "foo",
  "last": "bar"
}
この安全なシリアライズを行うために、追加の設定を適用する必要はありません。ただし、ユーザーは @io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization および @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization アノテーションを使用して、特定の Jakarta REST Resource クラスまたはメソッドに対するオプトインまたはオプトアウトを行うことができます。
Configuration expressions set with the SecureField.rolesAllowed property are validated during application startup even when the @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization annotation is used.
@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;
}

Depending on the Jakarta REST method that returns this user, we might want to exclude the id field from serialization. For example, you might want an insecure method to not expose this field. The way we can achieve that in Quarkus REST is shown in the following example:

@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 が含まれます。

Reflection-free Jackson serialization

Out-of-the-box Jackson serialization converts objects into their JSON representation by introspecting them through a heavy use of reflection. However, the general Quarkus philosophy is to avoid reflection as much as possible, often replacing it with build time code generation. For this reason it is possible to automatically generate at build time implementations of the Jackson StdSerializer, one for each class to be converted in JSON. These generated serializers can be subsequently used by Quarkus at runtime to perform the JSON serialization of the objects returned by a REST endpoint without any use of reflection.

This feature is turned off by default, but it can be enabled by setting the configuration property quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true.

Completely customized per method serialization/deserialization

There are times when you need to completely customize the serialization/deserialization of a POJO on a per Jakarta REST method basis or on a per Jakarta REST resource basis. For such use cases, you can use the @io.quarkus.resteasy.reactive.jackson.CustomSerialization and @io.quarkus.resteasy.reactive.jackson.CustomDeserialization annotations in the REST method or in the REST resource at class level. These annotations allow you to fully configure the com.fasterxml.jackson.databind.ObjectWriter/com.fasterxml.jackson.databind.ObjectReader.

Here is an example use case to customize the 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 メソッドのシリアライズに対してのみ実行されることに注意する必要があります。

Following the previous example, let’s now customize the com.fasterxml.jackson.databind.ObjectReader to read JSON requests with unquoted field names:

@CustomDeserialization(SupportUnquotedFields.class)
@POST
@Path("/use-of-custom-deserializer")
public void useOfCustomSerializer(User request) {
    // ...
}

where SupportUnquotedFields is a BiFunction defined as so:

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 シリアライゼーション

To enable XML support, add the quarkus-rest-jaxb extension to your project.

GAV 使用方法

io.quarkus:quarkus-rest-jaxb

XML サポート

このモジュールをインポートすると、HTTP メッセージ本文を XML から読み込み、XML にシリアル化することができます (all the types not already registered with a more specific serialisation)。

The JAXB Quarkus REST extension will automatically detect the classes that are used in the resources and require JAXB serialization. Then, it will register these classes into the default JAXBContext which is internally used by the JAXB message reader and writer.

However, in some situations, these classes cause the JAXBContext to fail: for example, when you’re using the same class name in different java packages. In these cases, the application will fail at build time and print the JAXB exception that caused the issue, so you can properly fix it. Alternatively, you can also exclude the classes that cause the issue by using the property quarkus.jaxb.exclude-classes. When excluding classes that are required by any resource, the JAXB Quarkus REST extension will create and cache a custom JAXBContext that will include the excluded class, causing a minimal performance degradance.

The property quarkus.jaxb.exclude-classes accepts a comma separated list of either fully qualified class names or package names. Package names must be suffixed by .* and all classes in the specified package and its subpackages will be excluded.

For instance, when setting quarkus.jaxb.exclude-classes=org.acme.one.Model,org.acme.two.Model,org.acme.somemodel.*, the following elements are excluded:

  • The class org.acme.one.Model

  • The class org.acme.two.Model

  • All the classes in the org.acme.somemodel package and its subpackages

高度な JAXB 固有の機能

When using the quarkus-resteasy-reactive-jaxb extension there are some advanced features that Quarkus REST supports.

JAXB コンポーネントを注入する

The JAXB Quarkus REST extension will serialize and unserialize requests and responses transparently for users. However, if you need finer grain control over JAXB components, you can inject either the JAXBContext, Marshaller, or Unmarshaller components into your beans:

@ApplicationScoped
public class MyService {

    @Inject
    JAXBContext jaxbContext;

    @Inject
    Marshaller marshaller;

    @Inject
    Unmarshaller unmarshaller;

    // ...
}

Quarkus は、@XmlRootElement でアノテーションが付けられたすべてのクラスを自動的に検出し、それらを JAXB コンテキストにバインドします。

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 コンテキストインスタンスを自動検出されたクラスで更新しないことを意味します。

GAV 使用方法

io.quarkus:quarkus-rest-links

Web リンクのサポート

Importing this module will allow injecting web links into the response HTTP headers by just annotating your endpoint resources with the @InjectRestLinks annotation. To declare the web links that will be returned, you must use the @RestLink annotation in the linked methods. Assuming a Record looks like:

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

An example of enabling Web Links support would look like:

@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 の単一のインスタンスを返さないため、メソッド getupdate、および 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 アノテーションが付いていないため、ウェブリンクが表示されないはずです。

RestLinksProvider Bean を挿入するだけで、プログラムで Web リンクレジストリーにアクセスできます。

@Path("/records")
public class RecordsResource {

    @Inject
    RestLinksProvider linksProvider;

    // ...
}

RestLinksProvider の挿入された Bean を使用すると、メソッド RestLinksProvider.getTypeLinks を使用して型ごとにリンクを取得するか、メソッド RestLinksProvider.getInstanceLinks を使用して具象インスタンスによってリンクを取得できます。

JSON ハイパーテキストアプリケーション言語 (HAL) のサポート

HAL 標準は、Web リンクを表す単純な形式です。

To enable the HAL support, add the quarkus-hal extension to your project. Also, as HAL needs JSON support, you need to add either the quarkus-rest-jsonb or the quarkus-rest-jackson extension.

GAV 使用方法

io.quarkus:quarkus-hal

HAL

エクステンションを追加した後、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} は、jsonhal + 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"
        }
    }
}

Finally, you can also provide additional HAL links programmatically in your resource just by returning either HalCollectionWrapper<T> (to return a list of entities) or HalEntityWrapper<T> (to return a single object) as described in the following example:

@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 includes a CORS filter at the HTTP layer level. For more information about the CORS filters and their usage, see the CORS filter section of the Quarkus "Cross-origin resource sharing" guide.

より高度な使用法

ここでは、最初は知る必要がないかもしれませんが、より複雑なユースケースに役立つ可能性のある、より詳細なトピックをいくつか紹介します。

実行モデル、ブロッキング、ノンブロッキング

Quarkus REST is implemented using two main thread types:

  • イベントループスレッド: HTTPリクエストからのバイトの読み込みとHTTPレスポンスへのバイトの書き戻しを担当します。

  • ワーカースレッド: プールされ、長時間実行される操作の負荷を軽減するために使用できます

イベントループスレッド (IO スレッドとも呼ばれます) は、すべての IO 操作を非同期で実際に実行し、それらの IO 操作の完了に関心のあるリスナーをトリガーする役割を果たします。

By default, the thread Quarkus REST will run endpoint methods on depends on the signature of the method. If a method returns one of the following types then it is considered non-blocking, and will be run on the IO thread by default:

  • 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…");
    }
}

ほとんどの場合、 MutinyHibernate 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 を付けると、この動作は上書きされます。

デフォルトの動作のオーバーライド

デフォルトの動作をオーバーライドしたい場合は、アプリケーション内の jakarta.ws.rs.core.Application サブクラスに @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";
    }
}

You can change the log level of the thrown WebApplicationException exceptions by configuring the following property quarkus.log.category."WebApplicationException".level like so:

quarkus.log.category."WebApplicationException".level=DEBUG

If your endpoint method is delegating calls to another service layer which does not know of Jakarta REST, you need a way to turn service exceptions to an HTTP response, and you can do that using the @ServerExceptionMapper annotation on a method, with one parameter of the exception type you want to handle, and turning that exception into a RestResponse (or a 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);
    }
}

By default, methods annotated with @ServerExceptionMapper do not run CDI interceptors that apply to the other methods of the class (like ones needed for implementing security method level security).

Users however can opt into interceptors by adding the corresponding annotations to the method.

When mapping an exception to a @ServerExceptionMapper method, the cause of the exception normally does not come into play.

However, some exception types in Java only serve as wrappers for other exceptions. Often, checked exceptions are wrapped into RuntimeException just to not have them declared in method throws parameters. Working with CompletionStage for example, will require CompletionException. There are many such exception types that are just wrappers around the real cause of the exception.

If you wish to make sure your exception mapper is called for your exception type even when it is wrapped by one of those wrapper exceptions, you can use @UnwrapException on the exception wrapper type:

public class MyExceptionWrapper extends RuntimeException {
    public MyExceptionWrapper(Exception cause) {
        super(cause);
    }
}

If you don’t control that exception wrapper type, you can place the annotation on any class and specify the exception wrapper types it applies to as annotation parameter:

@UnwrapException({CompletionException.class, RuntimeException.class})
public class Mapper {

    @ServerExceptionMapper
    public Response handleMyException(MyException x) {
        // ...
    }

}

Εxception mappers defined in REST endpoint classes will only be called if the exception is thrown in the same class. If you want to define global exception mappers, simply define them outside a REST endpoint class:

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

また、 例外マッパー を、Jakarta RESTの方法で宣言することができます。

例外マッパーは、次のパラメーター型のいずれかを宣言できます。

Table 6. Exception mapper parameters
タイプ 使用方法

例外型

処理する例外型を定義します

Context objects> のいずれか

ContainerRequestContext

現在のリクエストにアクセスするためのコンテキストオブジェクト

次の戻り値の型のいずれかを宣言できます。

Table 7. Exception mapper return types
タイプ 使用方法

RestResponse または Response

例外が発生したときにクライアントに送信するレスポンス

Uni<RestResponse> または Uni<Response>

例外が発生したときにクライアントに送信する非同期レスポンス

When an exception occurs, Quarkus REST does not log it by default (for security reasons). This can sometimes make it hard to understand why certain exception handling code was invoked (or not invoked). To make Quarkus REST log the actual exception before an exception mapping code is run the org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext log category can be set to DEBUG like so:

quarkus.log.category."org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext".level=DEBUG

リクエストまたはレスポンスフィルター

アノテーション経由

You can declare functions that are invoked in the following phases of the request processing:

  • Before the endpoint method is identified: pre-matching request filter

  • ルーティング後、エンドポイントメソッドが呼び出される前: 通常のリクエストフィルター

  • エンドポイントメソッドが呼び出された後: レスポンスフィルター

これらのフィルターを使用すると、リクエスト 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();
    }
}

リクエストフィルターは通常、リクエストを処理するメソッドが実行されるのと同じスレッドで実行されます。つまり、リクエストを処理するメソッドが @Blocking でアノテーションされている場合は、フィルターもワーカスレッドで実行されます。メソッドに @NonBlocking のアノテーションがある場合 (あるいは全くアノテーションがない場合)、フィルターは同じイベントループのスレッドで実行されます。

しかし、リクエストを処理するメソッドがワーカスレッドで実行されるにも関わらず、フィルターをイベントループで実行する必要がある場合、@ServerRequestFilter (nonBlocking=true) を使用することができます。しかし、これらのフィルターはこの設定を使わず、ワーカースレッドで実行される どんな フィルターよりも前に実行される必要があることに注意してください。

Keep in mind however that the information above does not apply to pre-matching filters (@ServerRequestFilter(preMatching = true)). These filters are always run on an event-loop thread.

同様に、レスポンスフィルターは @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());
        }
    }
}

Such a response filter will also be called for handled exceptions.

フィルターは、次のパラメーター型のいずれかを宣言できます。

Table 8. Filter parameters
タイプ 使用方法

Context objects> のいずれか

ContainerRequestContext

現在のリクエストにアクセスするためのコンテキストオブジェクト

ContainerResponseContext

現在のレスポンスにアクセスするためのコンテキストオブジェクト

Throwable

Any thrown and handled exception, or null (only for response filters).

次の戻り値の型のいずれかを宣言できます。

Table 9. Filter return types
タイプ 使用方法

RestResponse<?> または Response

フィルターチェーンを続行する代わりにクライアントに送信するレスポンス、またはフィルターチェーンを続行する必要がある場合は null

Optional<RestResponse<?>> または Optional<Response>

フィルターチェーンを続行する代わりにクライアントに送信するオプションのレスポンス、またはフィルターチェーンを続行する必要がある場合は空の値

Uni<RestResponse<?>> または Uni<Response>

フィルターチェーンを続行する代わりにクライアントに送信する非同期レスポンス、またはフィルターチェーンを続行する必要がある場合は 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

A ContainerResponseFilter will also be called for handled exceptions.

リーダーとライター: マッピングエンティティーと HTTP ボディ

Whenever your endpoint methods return an object (or when they return a RestResponse<?> or Response with an entity), Quarkus REST will look for a way to map that into an HTTP response body.

同様に、エンドポイントメソッドがオブジェクトをパラメーターとして受け取る場合は常に、HTTP リクエストの本文をそのオブジェクトにマッピングする方法を探します。

これは、MessageBodyReaderMessageBodyWriter インターフェイスのプラグインシステムによって行われ、どの 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);
    }

}

If you want to get the most performance out of your writer, you can extend the ServerMessageBodyWriter instead of MessageBodyWriter where you will be able to use less reflection and bypass the blocking IO layer:

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);
    }
}
プロバイダークラスに Consumes/Produces アノテーションを追加すると、リーダー/ライターが適用するコンテンツタイプを制限することができます。

リーダーおよびライターインターセプター

リクエストとレスポンスをインターセプトするのと同様に、 ReaderInterceptor または WriterInterceptor を拡張し、 @Provider でアノテーションすることで、リーダーとライターもインターセプトすることができます。

このエンドポイントを見ると、次のようになります。

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 and REST Client interactions

In Quarkus, the Quarkus REST extension and the REST Client extension share the same infrastructure. One important consequence of this consideration is that they share the same list of providers (in the Jakarta REST meaning of the word).

例えば、 WriterInterceptor を宣言した場合、デフォルトではサーバーの呼び出しとクライアントの呼び出しの両方をインターセプトしますが、これは望ましい動作ではないかもしれません。

しかし、このデフォルトの動作を変更してプロバイダを制約することができます。

  • プロバイダに @ConstrainedTo(RuntimeType.SERVER) アノテーションを追加することで、 サーバー コールのみを考慮します。

  • プロバイダに @ConstrainedTo(RuntimeType.CLIENT) アノテーションを追加することで、 クライアント コールのみを考慮します。

パラメーターマッピング

すべてのリクエストパラメーターString として宣言することができます。しかし、それだけでなく、次のいずれかの型でも宣言できます。

  • リンク先のタイプ: ParamConverter は、登録済みの ParamConverterProvider から入手できます。

  • プリミティブ型

  • 単一の 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;
    }
}

Separating Query parameter values

Normally a collection of String values is used to capture the values used in multiple occurrences of the same query parameter. For example, for the following resource method:

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

and the following request:

GET /hello?name=foo&name=bar HTTP/1.1

the names variable will contain both foo and bar and the response will be hello foo bar.

It is not uncommon however to need to convert a single query parameter into a collection of values based on some delimiting character. That is where the @org.jboss.resteasy.reactive.Separator annotation comes into play.

If we update the resource method to:

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

and use the following request:

GET /hello?name=foo,bar HTTP/1.1

then the response will be hello foo bar.

日付の取り扱い

Quarkus REST supports the use of the implementations of java.time.Temporal (like java.time.LocalDateTime) as query, path, or form params. Furthermore, it provides the @org.jboss.resteasy.reactive.DateFormat annotation, which can be used to set a custom expected pattern. Otherwise, the JDK’s default format for each type is used implicitly.

前提条件

以下のようないくつかの条件の下で、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 ヘッダーが設定されていない。

By default, the following list of media types is compressed: text/html, text/plain, text/xml, text/css, text/javascript, application/javascript, application/json, application/graphql+json and 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 クライアントガイド を参照してください。

関連コンテンツ