はじめに
- Jakarta RESTful Web Services の Client API の使い方のメモ
- ここでは互換実装である Eclipse Jersey を使う
 
 - Java EE では JAX-RS と呼ばれていたやつ
- 正式名称は長いので、ここでは JAX-RS と表記する
 
 - JAX-RS は、 Java で RESTful ウェブサービスを構築するための標準仕様
 - 最初はサーバー側の API だけだったけど、 Java EE 7 (JAX-RS 2.0)でクライアント向けの API が増えた
 
環境
>gradle --version
------------------------------------------------------------
Gradle 8.8
------------------------------------------------------------
Build time:   2024-05-31 21:46:56 UTC
Revision:     4bd1b3d3fc3f31db5a26eecb416a165b8cc36082
Kotlin:       1.9.22
Groovy:       3.0.21
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.3 (Eclipse Adoptium 21.0.3+9-LTS)
OS:           Windows 10 10.0 amd64
Hello World
検証用のサーバー
package sandbox.jaxrs.server;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@WebServlet("/print/*")
public class PrintServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (ServletInputStream in = req.getInputStream()) {
            in.transferTo(buffer);
        }
        final Enumeration<String> names = req.getHeaderNames();
        String body = buffer.toString(StandardCharsets.UTF_8);
        System.out.printf("""
        method=%s
        requestURI=%s
        queryString=%s
        ===headers===
        %s
        ===body===
        %s
        ==========
        """, req.getMethod(), req.getRequestURI(), req.getQueryString(),
            headers(req), body);
        resp.setContentType(req.getContentType());
        try (ServletOutputStream out = resp.getOutputStream()) {
            out.write(body.getBytes(StandardCharsets.UTF_8));
        }
    }
    private String headers(HttpServletRequest req) {
        List<String> result = new ArrayList<>();
        Enumeration<String> names = req.getHeaderNames();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            Enumeration<String> headers = req.getHeaders(name);
            while (headers.hasMoreElements()) {
                String header = headers.nextElement();
                result.add(name + ": " + header);
            }
        }
        return String.join("\n", result);
    }
}
- 受け取ったリクエストの情報を標準出力に書き出すサーブレット
 - 
debug.warという war ファイルにビルドして Tomcat にデプロイして使う- 
http://localhost:8080/debug/printでアクセスできる状態になる 
 - 
 - レスポンスボディにはリクエストボディと同じ内容を書き出す
- 
Content-Typeもリクエストと同じものをレスポンスに設定するようにしている 
 - 
 
実装
plugins {
    id "java"
}
compileJava.options.encoding = "UTF-8"
sourceCompatibility = 21
targetCompatibility = 21
repositories {
    mavenCentral()
}
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
}
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.Response;
public class JaxRsClientMain {
    public static void main(String[] args) {
        try (
            Client client = ClientBuilder.newClient();
            Response response = client.target("http://localhost:8080/debug/print")
                                      .request()
                                      .post(Entity.text("Hello World"))
        ) {
            System.out.printf("""
            status = %s
            body = %s
            """, response.getStatus(), response.readEntity(String.class));
        }
    }
}
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 11
===body===
Hello World
==========
status = 200
body = Hello World
説明
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
}
- Eclipse Jersey のクライアントを使うには、 org.glassfish.jersey.core:jersey-client を依存に追加する
 
        try (
            Client client = ClientBuilder.newClient();
            Response response = client.target("http://localhost:8080/debug/print")
                                      .request()
                                      .post(Entity.text("Hello World"))
        ) {
            System.out.printf("""
            status = %s
            body = %s
            """, response.getStatus(), response.readEntity(String.class));
        }
- HTTPリクエストを送信するには 
Clientインスタンスを使用する - 
ClientBuilderでClientインスタンスを生成し、各種リクエストの情報を設定していく - HTTPレスポンスは 
Responseで取得でき、そこからレスポンスの情報にアクセスできる 
リクエスト送信の流れ
Client の生成
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
...
Client client = ClientBuilder.newClient();
- HTTPリクエストを送信するには、まず 
Clientのインスタンスを用意する - 
Clientインスタンスは、ClientBuilderのnewClientメソッドで取得できる - 
Clientインスタンスの生成と破棄にはコストがかかる可能性があるので、生成するインスタンスは少数にしておくことが推奨されている- スレッドセーフであることが明記されてないけど、こう推奨されている以上はインスタンスは使いまわしが前提だと信じている
 
 
Initialization as well as disposal of a Client instance may be a rather expensive operation.
It is therefore advised to construct only a small number of Client instances in the application.(翻訳)
クライアント インスタンスの初期化と破棄は、かなりコストのかかる操作になる可能性があります。
したがって、アプリケーション内に少数のクライアント インスタンスのみを構築することをお勧めします。https://jakarta.ee/specifications/restful-ws/3.1/apidocs/jakarta.ws.rs/jakarta/ws/rs/client/client
- 
ClientはAutoCloseableを継承しているので、不要になったらクローズが必要 
WebTarget の構築
import jakarta.ws.rs.client.WebTarget;
...
WebTarget target = client.target("http://localhost:8080/debug/print");
- 
ClientのtargetメソッドでWebTargetを生成する- 
targetメソッドにはリクエストの URI を指定する 
 - 
 - 
WebTargetはリクエスト先のリソースを表すクラスとなる - したがって、 
WebTargetでは URI を細かく構築できる 
パスを追加する
WebTarget target = client.target("http://localhost:8080/debug/print")
                         .path("foo")
                         .path("/bar");
System.out.println(target);
JerseyWebTarget { http://localhost:8080/debug/print/foo/bar }
- 
pathメソッドで URI にパスを追加できる 
クエリパラメータを追加する
WebTarget target = client.target("http://localhost:8080/debug/print")
                         .queryParam("foo", "a", "b", "c")
                         .queryParam("bar", "あ");
System.out.println(target);
JerseyWebTarget { http://localhost:8080/debug/print?foo=a&foo=b&foo=c&bar=%E3%81%82 }
- 
queryParamでクエリパラメータを追加できる - マルチバイト文字は自動的に URL エンコードされる
- エンコード時の文字コードは UTF-8 固定
 - JAX-RS の仕様上では文字コードについての言及は見当たらなかった
 
 
テンプレートを使用する
WebTarget template = client.target("http://localhost:8080/")
                         .path("{foo}").path("{bar}/{foo}")
                         .queryParam("hoge", "{hoge}");
WebTarget resolved = template.resolveTemplates(Map.of(
    "foo", "FOO",
    "bar", "fizz/buzz",
    "hoge", "ほ"
));
System.out.println("template=" + template);
System.out.println("resolved=" + resolved);
template=JerseyWebTarget { http://localhost:8080/{foo}/{bar}/{foo}?hoge={hoge} }
resolved=JerseyWebTarget { http://localhost:8080/FOO/fizz%2Fbuzz/FOO?hoge=%E3%81%BB }
- パスやクエリパラメータには埋め込みパラメータを設定できる
- パラメータは波括弧 (
{}) でパラメータ名を囲う形で記述する 
 - パラメータは波括弧 (
 - 
resolveTemplatesメソッドでパラメータに値を埋め込んだ新しいWebTargetを取得できる- 引数にはパラメータに埋め込む情報を持った 
Mapを渡す - 元の 
WebTargetの状態は変更されない 
 - 引数にはパラメータに埋め込む情報を持った 
 - 埋め込まれる値は自動的に URL エンコードされる(文字コードは UTF-8)
 - 
resolveTemplate(String, Object)を用いると単一のパラメータだけ解決することも可能 
URLエンコード済みの値を埋め込む
- 埋め込む値が既に URL エンコード済みの場合は 
resolveTemplatesFromEncodedを使用する 
WebTarget template =
    client.target("http://localhost:8080/{hoge}/{fuga}/{piyo}");
WebTarget resolved = template.resolveTemplatesFromEncoded(Map.of(
    "hoge", "fizz%2Fbuzz",
    "fuga", "fizz/buzz",
    "piyo", "fizz%buzz"
));
System.out.println("resolved=" + resolved);
resolved=JerseyWebTarget { http://localhost:8080/fizz%2Fbuzz/fizz/buzz/fizz%25buzz }
- 
fizz%2Fbuzzの%2Fの部分やfizz/buzzの/の部分は本来であれば URL エンコードが必要だが、resolveTemplatesFromEncodedはエンコード済みの値を渡している前提なのでエンコードは行われずそのままの形で埋め込まれるようになる - ただし、 
fizz%buzzの%のように後ろに2桁の16進数が続かない部分については明らかに URL エンコードされた値ではないので、エンコードされたうえで埋め込まれる 
スラッシュをエンコードしないようにする
WebTarget target = client.target("http://localhost:8080/{foo}/{bar}")
                         .resolveTemplate("foo", "fizz/buzz")
                         .resolveTemplate("bar", "fizz/buzz", false);
System.out.println(target);
JerseyWebTarget { http://localhost:8080/fizz%2Fbuzz/fizz/buzz }
- 
booleanのパラメータ(encodeSlashInPath) を受け取るメソッドが用意されており、falseを渡すとスラッシュの URL エンコーディングをせずにパラメータを埋め込むことができる 
URI に対しては不変
WebTarget first = client.target("http://localhost:8080/debug/print");
WebTarget second = first.path("foo");
WebTarget third = first.queryParam("a", "A");
System.out.println("first =" + first);
System.out.println("second=" + second);
System.out.println("third =" + third);
first =JerseyWebTarget { http://localhost:8080/debug/print }
second=JerseyWebTarget { http://localhost:8080/debug/print/foo }
third =JerseyWebTarget { http://localhost:8080/debug/print?a=A }
- 
pathやqueryParam,resolveTemplatesなど URI に変更を加えるメソッドを実行すると、変更後の URI を持つ新しいWebTargetインスタンスが返される - 元のインスタンスは変更されないので、共通部分を持った 
WebTargetを元に複数のWebTargetを派生させる、といった使い方ができる 
設定に対しては可変
WebTarget first = client.target("http://localhost:8080/debug/print");
WebTarget second = first.property("a", "A");
WebTarget third = second.register(new Object());
System.out.println("first.hash =" + first.hashCode());
System.out.println("second.hash=" + second.hashCode());
System.out.println("third.hash =" + third.hashCode());
first.hash =2114694065
second.hash=2114694065
third.hash =2114694065
- 
propertyやregisterなどの設定メソッドは、インスタンスの状態を変更して同じインスタンスを返す - 設定についての説明は後述
 
Invocation の構築
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.core.MediaType;
...
Invocation.Builder builder = target.request();
builder.accept(MediaType.APPLICATION_JSON)
       .header("Content-Type", "text/plain");
- 
WebTargetのrequestメソッドを実行すると、Invocationのビルダーを取得できる - 
Invocationは1つの HTTP リクエストを表すクラスで、ここまでで構築した URI の情報に加えヘッダーやHTTPメソッド・ボディなどの情報を持つ - 
Invocation.Builderでは、これらの情報を設定できる 
専用のヘッダー設定メソッド
import jakarta.ws.rs.core.CacheControl;
...
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
builder.accept(MediaType.APPLICATION_JSON)
       .cookie("key", "value")
       .cacheControl(cacheControl);
- 
Accept,Cookie,Cache-Controlなど、よく使われると思われるヘッダーについては専用の設定メソッドが用意されている- 他に 
acceptEncoding,acceptLanguageなどもある 
 - 他に 
 
汎用のヘッダー設定メソッド
builder.header("Content-Type", "text/plain");
- 
headerメソッドを使えば任意のヘッダーを設定できる 
リクエストの実行
import jakarta.ws.rs.core.Response;
...
Response response = builder.get();
- 
Builderにはリクエストを直接実行するためのメソッドが用意されている- 
get,post,put,delete,options,head,trace - 各メソッドは、名前が HTTP メソッドに対応している
 - 任意のHTTPメソッド指定したい場合は 
methodメソッドが利用できる 
 - 
 - レスポンスは 
Responseで返される 
ボディを指定する
import jakarta.ws.rs.client.Entity;
...
Entity<String> entity = Entity.text("Hello");
Response response = builder.post(entity);
- ボディは 
Entityで指定する- 
postやputなど、ボディを持つことができる HTTP メソッドに対応するメソッドであれば引数にEntityを受け取れるようになっている 
 - 
 - 
EntityのインスタンスはEntityにあるstaticなファクトリメソッドで生成できる- 
textメソッドを使ってEntityを作成すると、ヘッダーにContent-Type: text/plainが自動的に付与される - 同様に 
jsonやxmlなど、Content-Typeが自動的に付与されるファクトリメソッドがいくつか用意されている- 
text,json,xml,html,xhtml,formがある 
 - 
 - 
Content-Typeを明示的に指定したい場合はentityメソッドを使用するEntity.entity("Hello", MediaType.TEXT_PLAIN_TYPE)- 第二引数で 
MediaTypeを明示的に指定できる 
 
 - 
 - ボディのシリアライズ方法の詳細については長くなるで後述
 
一旦 Invocation で受け取ってから後で実行する
Invocation invocation = builder.buildGet();
Response response = invocation.invoke();
- 
BuilderのbuildGetなどのビルドメソッドを実行すると、そこまでに構築した HTTP リクエストの情報を保持したInvocationのインスタンスを取得できる- 他に HTTP メソッドを指定できるビルドメソッドとしては、 
buildPost,buildPut,buildDeleteがある - それ以外の HTTP メソッドについては 
buildメソッドを使用する(引数で任意の HTTP メソッドを指定できる) 
 - 他に HTTP メソッドを指定できるビルドメソッドとしては、 
 - 
Invocationの状態で保持しておけば、実際の HTTP リクエストは任意のタイミングで実施できるようになる - 
invokeメソッドを呼ぶと HTTP リクエストが実行され、レスポンスが返される - リクエストを非同期で実行したい場合は 
submitメソッドを使用する 
Future<Response> future = invocation.submit();
- この場合、戻り値が 
Futureになる 
レスポンスを取得する
import jakarta.ws.rs.core.Response;
...
try (Response response = invocation.invoke()) {
    String body = response.readEntity(String.class);
    System.out.printf("""
    status = %s
    === body ===
    %s
    ============
    """, response.getStatus(), body);
}
status = 200
=== body ===
Hello World
============
- HTTP レスポンスは 
Response型で受け取ることができる - このオブジェクトからは、 HTTP レスポンスの各種情報にアクセスできる
 - レスポンスボディの情報を取り出すには 
readEntityメソッドを使用する- 引数にはボディの情報を格納する Java の型を指定する
 - ボディのデシリアライズに関しての詳細は後述
 
 - 
ResponseはAutoCloseableを継承している- 一応 
Responseは、readEntityなどでエンティティの情報を読み取ると自動的にクローズされることになっている - ただし、エンティティを 
InputStreamで受け取った場合は明示的なクローズが必要となる - ややこしいので、事故らないようにするためにも 
Responseは常に try-with-resources に入れてクローズするように実装するのが良いのではないかと個人的に思う 
 - 一応 
 
直接エンティティだけを取得する
String body = builder.post(Entity.text("Hello World"), String.class);
System.out.println("body=" + body);
body=Hello World
- 
Invocation.Builderの直接 HTTP リクエストを実行するpostなどのメソッドやInvocationのinvokeなどのメソッドには、レスポンスのエンティティの型を引数で指定できるようになっている - これを指定すると、戻り値の型は 
Responseではなく、引数で指定したエンティティの型になり直接エンティティだけを受け取ることができるようになる- 要するに、いきなり 
ResponseのreadEntityを呼んでいる感じになる 
 - 要するに、いきなり 
 - レスポンスのステータスコードが 2xx 系でない場合は例外がスローされる
 
ボディ(エンティティ)のシリアライズ・デシリアライズ
ここでは HTTP メッセージのボディ(エンティティ)のシリアライズとデシリアライズがどのような仕組みで制御されているのか、どう調整できるのかについてまとめる。
エンティティプロバイダ
- リクエストおよびレスポンスボディ(エンティティ)のシリアライズ・デシリアライズは、エンティティプロバイダによって行われる
 - エンティティプロバイダは 
MessageBodyWriterおよびMessageBodyReaderインタフェースを実装することで作成できる - それぞれのインタフェースの定義は以下のようになっている
 
package jakarta.ws.rs.ext;
public interface MessageBodyWriter<T> {
    public boolean isWriteable(
        Class<?> type,
        Type genericType,
        Annotation[] annotations,
        MediaType mediaType);
    // ★ getSize メソッドは実装不要(詳細後述)
    public default long getSize(
        final T t,
        final Class<?> type,
        final Type genericType,
        final Annotation[] annotations,
        final MediaType mediaType) {
        return -1L;
    }
    
    public void writeTo(
        T t,
        Class<?> type,
        Type genericType,
        Annotation[] annotations,
        MediaType mediaType,
        MultivaluedMap<String, Object> httpHeaders,
        OutputStream entityStream)
            throws java.io.IOException, jakarta.ws.rs.WebApplicationException;
}
package jakarta.ws.rs.ext;
public interface MessageBodyReader<T> {
    public boolean isReadable(
        Class<?> type,
        Type genericType,
        Annotation[] annotations,
        MediaType mediaType);
    public T readFrom(
        Class<T> type,
        Type genericType,
        Annotation[] annotations,
        MediaType mediaType,
        MultivaluedMap<String, String> httpHeaders,
        InputStream entityStream)
            throws java.io.IOException, jakarta.ws.rs.WebApplicationException;
}
- これらのインタフェースを実装したエンティティプロバイダを JAX-RS のランタイムに登録しておく
 - すると、エンティティごとに適したエンティティプロバイダが選択され、シリアライズ・デシリアライズが行われる
 - 大雑把な流れを以下に示す
 
- JAX-RS のランタイムは、エンティティを書き出すときにまずは登録されている各 
MessageBodyWriterのisWritableを呼び出す- 
isWritableには、書き出そうとしているエンティティの型(Class)やメディアタイプなどの情報が渡される - 
isWritableは渡された情報をもとに、そのエンティティのシリアライズに対応しているかどうかを判定しbooleanで結果を返す(対応している場合はtrue) - JAX-RS のランタイムは、最初に 
trueを返したMessageBodyWriterのwriteToメソッドを使ってエンティティの書き出しを行う 
 - 
 - 読み取りの場合も同様で、登録されている各 
MessageBodyReaderのisReadableが呼ばれる- 
isReadableにはデシリアライズ後のエンティティの型やメディアタイプなどが渡されるので、デシリアライズに対応している場合はtrueを返すように実装する - JAX-RS ランタイムは最初に 
trueを返したMessageBodyReaderのreadFromメソッドを使ってエンティティのデシリアライズを行う 
 - 
 
MessageBodyWriter の getSize メソッドは実装不要
MessageBodyWriter の getSize メソッドは、 JAX-RS 2.0 では無視されるようになっている。
Content-Length ヘッダーに設定する値の計算は JAX-RS のランタイムが行うようになっている。
エンティティプロバイダの登録方法
実装
package sandbox.jaxrs.provider;
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 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 java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class MyLocalDateEntityProvider implements
        MessageBodyWriter<LocalDate>, MessageBodyReader<LocalDate> {
    private static final DateTimeFormatter FORMATTER
            = DateTimeFormatter.ofPattern("uuuu/MM/dd");
    @Override
    public boolean isWriteable(
            Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        return type == LocalDate.class;
    }
    @Override
    public void writeTo(
            LocalDate entity, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders,
            OutputStream entityStream) throws IOException, WebApplicationException {
        byte[] bytes = FORMATTER.format(entity).getBytes(StandardCharsets.UTF_8);
        entityStream.write(bytes);
        entityStream.flush();
    }
    @Override
    public boolean isReadable(
            Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        return type == LocalDate.class;
    }
    @Override
    public LocalDate readFrom(
            Class<LocalDate> type, Type genericType, Annotation[] annotations,
            MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
            InputStream entityStream) throws IOException, WebApplicationException {
        byte[] buffer = new byte[10];
        entityStream.read(buffer);
        String text = new String(buffer, StandardCharsets.UTF_8);
        return LocalDate.parse(text, FORMATTER);
    }
}
- 
LocalDateをyyyy/MM/dd形式の文字列にシリアライズ・デシリアライズするエンティティプロバイダ 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.MyLocalDateEntityProvider;
import java.time.LocalDate;
public class JaxRsClientMain {
    public static void main(String[] args) {
        LocalDate result = ClientBuilder.newClient()
                            .register(MyLocalDateEntityProvider.class)
                            .target("http://localhost:8080/debug/print")
                            .request()
                            .post(Entity.text(LocalDate.now()), LocalDate.class);
        System.out.println(result);
    }
}
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 10
===body===
2024/09/17
==========
2024-09-17
説明
        LocalDate result = ClientBuilder.newClient()
                            .register(MyLocalDateEntityProvider.class)
- エンティティプロバイダの登録は、 
Clientなどが提供しているregisterメソッドを使用して行う - 引数にプロバイダの 
Classオブジェクトを渡すことで登録できる - 
registerメソッドは、実際にはConfigurableインタフェースで定義されており、Clientはこれを継承している関係になっている - 他にも 
WebTargetもConfigurableを継承しているので、こちらでもプロバイダの登録ができる 
メディアタイプによる候補の絞り込み
実装
package sandbox.jaxrs.provider;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Produces(MediaType.APPLICATION_JSON)
public class MyLocalDateJsonEntityProvider implements MessageBodyWriter<LocalDate> {
    private static final DateTimeFormatter FORMATTER
            = DateTimeFormatter.ofPattern("uuuu/MM/dd");
    @Override
    public boolean isWriteable(
            Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        System.out.println("MyLocalDateJsonEntityProvider.isWritable");
        return type == LocalDate.class;
    }
    @Override
    public void writeTo(
            LocalDate localDate, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {
        String json = """
        {"date": "%s"}
        """.formatted(FORMATTER.format(localDate));
        entityStream.write(json.getBytes(StandardCharsets.UTF_8));
        entityStream.flush();
    }
}
- 
LocalDateを受け取って簡単な JSON に変換するエンティティプロバイダ(MessageBodyWriterのみ実装) - 
isWritableが呼ばれた標準出力にメッセージを出力するようにしている 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import sandbox.jaxrs.provider.MyLocalDateEntityProvider;
import sandbox.jaxrs.provider.MyLocalDateJsonEntityProvider;
import java.time.LocalDate;
public class JaxRsClientMain {
    public static void main(String[] args) {
        WebTarget target = ClientBuilder.newClient()
                            .register(MyLocalDateEntityProvider.class)
                            .register(MyLocalDateJsonEntityProvider.class)
                            .target("http://localhost:8080/debug/print");
        System.out.println("post as application/json");
        target.request().post(Entity.json(LocalDate.now()));
        System.out.println("post as plain/text");
        target.request().post(Entity.text(LocalDate.now()));
    }
}
- エンティティプロバイダとして 
MyLocalDateEntityProviderとMyLocalDateJsonEntityProviderの2つを登録 - 最初のリクエストは 
Entity.json()を使ってContent-Typeをapplication/jsonにして送信 - 2つ目のリクエストは 
Entity.text()を使ってplain/textで送信 
実行結果
post as application/json
MyLocalDateJsonEntityProvider.isWritable
post as plain/text
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
...
===body===
{"date": "2024/09/18"}
==========
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
...
===body===
2024/09/18
==========
説明
@Produces(MediaType.APPLICATION_JSON)
public class MyLocalDateJsonEntityProvider implements MessageBodyWriter<LocalDate> {
- 
MessageBodyWriterを実装したエンティティプロバイダにProducesアノテーションを付けることで、そのエンティティプロバイダを適用するメディアタイプを限定することができる - この場合、 
MediaType.APPLICATION_JSONを設定しているので、MyLocalDateJsonEntityProviderはメディアタイプがapplication/jsonの場合のみ候補として選択されるようになる- 
Entity.json()で作ったリクエストではisWritableが呼ばれているが、Entity.text()で作ったリクエストでは呼ばれなくなっている(候補にもならなくなっている) 
 - 
 - これにより、エンティティプロバイダの候補選びで無駄な 
isWritableの呼び出しを減らすことができるようになる - 
MessageBodyReaderを実装したエンティティプロバイダの候補を絞るにはConsumesアノテーションを使用する 
| インタフェース | アノテーション | 
|---|---|
MessageBodyWriter | 
Produces | 
MessageBodyReader | 
Consumes | 
デフォルトで提供されているエンティティプロバイダ
- JAX-RS の仕様として、以下の型・メディアタイプの組み合わせはサポートされていることが保証されている
 
| エンティティの型 | メディアタイプ | 
|---|---|
byte[] | 
*/* | 
java.lang.String | 
*/* | 
java.io.InputStream | 
*/* | 
java.io.Reader | 
*/* | 
java.io.File | 
*/* | 
jakarta.activation.DataSource | 
*/* | 
javax.xml.transform.Source | 
text/xmlapplication/xmlapplicaiton/*+xml
 | 
jakarta.ws.rs.core.MultivaluedMap | 
application/x-www-form-urlencoded | 
java.util.List<EntityPart> | 
multipart/form-data | 
jakarta.ws.rs.core.StreamingOutput | 
*/* | 
java.lang.Boolean | 
text/plain | 
java.lang.Character | 
text/plain | 
java.lang.Number | 
text/plain | 
- 
*/*は任意のメディアタイプを表している - プリミティブ型については、対応するラッパークラスにオートボクシングされることで処理される
 - なお、アプリケーションが登録したエンティティプロバイダは、組み込みのエンティティプロバイダよりも優先される
 
Jersey が提供しているよく使うエンティティプロバイダ
XML や JSON など、よく利用されるコンテンツタイプのエンティティプロバイダは Jersey によってあらかじめプラグインが用意されている。
JAXB (XML)
実装
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-jaxb:3.1.8"
    implementation "com.sun.xml.bind:jaxb-impl:4.0.5"
}
- JAXB で XML の変換を行えるようにするには、以下2つを追加する
- Jersey の JAXB 用モジュール(org.glassfish.jersey.media:jersey-media-jaxb)
 - JAXB の互換実装(com.sun.xml.bind:jaxb-impl)
 
 
package sandbox.jaxrs.client;
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class MyEntity {
    private int id;
    public MyEntity() {}
    public MyEntity(int id) {
        this.id = id;
    }
    // getter, setter, toString 省略
}
- 
@XmlRootElementでクラスを注釈しておく 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
public class JaxRsClientMain {
    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);
        MyEntity response = ClientBuilder.newClient()
                            .target("http://localhost:8080/debug/print")
                            .request()
                            .post(Entity.xml(entity), MyEntity.class);
        System.out.println(response);
    }
}
- 
Entity.xml()でエンティティを設定してリクエストを投げる 
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/xml
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 86
===body===
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><myEntity><id>1</id></myEntity>
==========
- 
MyEntityの内容が XML にシリアライズされて送信できている 
MyEntity{id=1}
- レスポンスの XML もデシリアライズされて取得できている
 
説明
@XmlRootElement
public class MyEntity {
- JAXB で処理されるには、エンティティが 
@XmlRootElementで注釈されている必要がある 
XmlRootElement でエンティティを注釈できない場合
- 変換したいエンティティを 
@XmlRootElementで注釈できない場合は、JAXBElementを使用する 
実装
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.GenericType;
import jakarta.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
public class JaxRsClientMain {
    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);
        JAXBElement<MyEntity> jaxbEntity
            = new JAXBElement<>(new QName("entity"), MyEntity.class, entity);
        JAXBElement<MyEntity> responseElement = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.xml(jaxbEntity), new GenericType<>() {});
        MyEntity response = responseElement.getValue();
        System.out.println(response);
    }
}
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/xml
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 82
===body===
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><entity><id>1</id></entity>
==========
MyEntity{id=1}
説明
        MyEntity entity = new MyEntity(1);
        JAXBElement<MyEntity> jaxbEntity
            = new JAXBElement<>(new QName("entity"), MyEntity.class, entity);
            ...
                .post(Entity.xml(jaxbEntity), ...);
- エンティティに 
XmlRootElementを付けられない場合は、JAXBElementにエンティティを入れることでアノテーションなしで変換ができるようになる - 
JAXBElementのコンストラクタには、以下を渡す- 第一引数には 
QNameを指定する- この値は、 XML のルートタグの名前として利用される
 
 - 第二引数には、エンティティの 
Classオブジェクトを渡す - 第三引数には、変換対象のエンティティ自身を渡す
 
 - 第一引数には 
 - 
JAXBElementをEntityに詰めてボディに設定する 
        JAXBElement<MyEntity> responseElement = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.xml(jaxbEntity), new GenericType<>() {});
- レスポンスは 
GenericTypeを使用し、同じくJAXBElementで受け取る 
JSON-B (JSON)
実装
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-binding:3.1.8"
}
package sandbox.jaxrs.client;
public class MyEntity {
    private int id;
    public MyEntity() {}
    // getter, setter, toString は省略
}
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
public class JaxRsClientMain {
    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);
        MyEntity response = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.json(entity), MyEntity.class);
        System.out.println(response);
    }
}
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 8
===body===
{"id":1}
==========
MyEntity{id=1}
説明
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-binding:3.1.8"
}
- Jackson で JSON のシリアライズ・デシリアライズを行いたい場合は、 org.glassfish.jersey.media:jersey-media-json-binding を依存関係に追加する
 - 依存関係を追加したら、それだけで JSON-B 用のエンティティプロバイダを使い始められる
- 実装には Eclipse Yasson が使用される
 - サービスローダーの仕組みで JSON-B のエンティティプロバイダが自動的に有功化されるようになっている
 
 
Jsonb を調整する
- 
Jsonbを調整したい場合は以下のようにする 
実装
package sandbox.jaxrs.provider;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import jakarta.ws.rs.ext.ContextResolver;
public class MyJsonbConfigProvider implements ContextResolver<Jsonb> {
    @Override
    public Jsonb getContext(Class<?> aClass) {
        JsonbConfig config = new JsonbConfig();
        config.withFormatting(true);
        return JsonbBuilder.create(config);
    }
}
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.MyJsonbConfigProvider;
public class JaxRsClientMain {
    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);
        MyEntity response = ClientBuilder.newClient()
                .register(MyJsonbConfigProvider.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.json(entity), MyEntity.class);
        System.out.println(response);
    }
}
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 15
===body===
{
    "id": 1
}
==========
説明
public class MyJsonbConfigProvider implements ContextResolver<Jsonb> {
    @Override
    public Jsonb getContext(Class<?> aClass) {
        JsonbConfig config = new JsonbConfig();
        config.withFormatting(true);
        return JsonbBuilder.create(config);
    }
}
- 
Jsonbを調整したい場合はContextResolverインタフェースを実装したクラスを作り、getContextでカスタマイズしたJsonbを返すように実装する 
        MyEntity response = ClientBuilder.newClient()
                .register(MyJsonbConfigProvider.class)
- 作成した自作の 
ContextResolverをregisterで登録することで有効化できる 
Jackson (JSON)
実装
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8"
}
package sandbox.jaxrs.client;
public class MyEntity {
    private int id;
    public MyEntity() {}
    // getter, setter, toString は省略
}
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
public class JaxRsClientMain {
    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);
        MyEntity response = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.json(entity), MyEntity.class);
        System.out.println(response);
    }
}
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 8
===body===
{"id":1}
==========
- JSON にシリアライズされて送信できている
 
MyEntity{id=1}
- JSON から 
MyEntityにデシリアライズできている 
説明
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8"
}
- Jackson で JSON のシリアライズ・デシリアライズを行いたい場合は、 org.glassfish.jersey.media:jersey-media-json-jackson を依存関係に追加する
 - 依存関係を追加したら、それだけで Jackson 用のエンティティプロバイダを使い始められる
- サービスローダーの仕組みで Jackson のエンティティプロバイダが自動的に有功化されるようになっている
 
 
ObjectMapper を調整する
- 
ObjectMapperを調整したい場合は以下のようにする 
実装
package sandbox.jaxrs.provider;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import jakarta.ws.rs.ext.ContextResolver;
public class MyObjectMapperProvider implements ContextResolver<ObjectMapper> {
    private final ObjectMapper objectMapper;
    public MyObjectMapperProvider() {
        objectMapper = new ObjectMapper();
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }
    @Override
    public ObjectMapper getContext(Class<?> type) {
        return objectMapper;
    }
}
- 
ContextResolverインタフェースを実装したクラスを作成 - 
SerializationFeature.INDENT_OUTPUTを有効化してシリアライズ時に JSON をフォーマットするように設定 - 
getContextでカスタマイズしたObjectMapperを返すように実装 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.MyObjectMapperProvider;
public class JaxRsClientMain {
    public static void main(String[] args) {
        MyEntity entity = new MyEntity(1);
        MyEntity response = ClientBuilder.newClient()
                .register(MyObjectMapperProvider.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.json(entity), MyEntity.class);
        System.out.println(response);
    }
}
- 
registerでMyObjectMapperProviderを登録するように設定 
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: application/json
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 16
===body===
{
  "id" : 1
}
==========
- JSON がフォーマットされている
 
説明
public class MyObjectMapperProvider implements ContextResolver<ObjectMapper> {
    private final ObjectMapper objectMapper;
    public MyObjectMapperProvider() {
        objectMapper = new ObjectMapper();
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }
    @Override
    public ObjectMapper getContext(Class<?> type) {
        return objectMapper;
    }
}
- 
ObjectMapperを調整したい場合はContextResolverインタフェースを実装したクラスを作り、getContextでカスタマイズしたObjectMapperを返すように実装する 
        MyEntity response = ClientBuilder.newClient()
                .register(MyObjectMapperProvider.class)
- 作成した自作の 
ContextResolverをregisterで登録することで有効化できる 
Jakarta EE 環境だと標準でサポートされているエンティティプロバイダ
- Jakarta EE 環境だと、 JSON や XML のエンティティプロバイダがデフォルトでサポートされるようになっている
 
実装
|-build.gradle
`-src/main/java/
  `-sandbox/jaxrs/
    |-MyApplication.java
    |-HelloResource.java
    `-MyEntity.java
plugins {
    id "war"
}
sourceCompatibility = 21
targetCompatibility = 21
compileJava.options.encoding = "UTF-8"
repositories {
    mavenCentral()
}
dependencies {
    compileOnly "jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"
    compileOnly "jakarta.xml.bind:jakarta.xml.bind-api:4.0.2"
}
war.archiveBaseName = "jaxrs"
- JAX-RS と JAXB を依存に追加した war プロジェクトの設定
 - ビルドすると 
jaxrs.warという war が生成されるように設定 
package sandbox.jaxrs;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/api")
public class MyApplication extends Application {
}
- 
@ApplicationPathの設定 - パスのルートを 
/apiで定義 
package sandbox.jaxrs;
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class MyEntity {
    public int id;
}
- ボディに使用するエンティティ
 - 
@XmlRootElementで注釈して JAXB のエンティティプロバイダの処理対象となるように定義 
package sandbox.jaxrs;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class HelloResource {
    @GET
    public void hello() {
        try (Client client = ClientBuilder.newClient()) {
            WebTarget target =
                client.target("http://localhost:8080/jaxrs/api/hello/world");
            MyEntity entity = new MyEntity();
            entity.id = 10;
            target.request().post(Entity.json(entity)).close();
            target.request().post(Entity.xml(entity)).close();
        }
    }
    @POST
    @Path("/world")
    @Consumes(MediaType.APPLICATION_JSON)
    public void worldJson(String entity) {
        System.out.println("== application/json ==");
        System.out.println(entity);
    }
    @POST
    @Path("/world")
    @Consumes(MediaType.APPLICATION_XML)
    public void worldXml(String entity) {
        System.out.println("== application/xml ==");
        System.out.println(entity);
    }
}
- JAX-RS のクライアント兼サーバーの実装
 - 
GET /api/helloでリクエストを受け付け、 JAX-RS Client API を利用してPOST /api/hello/worldで2つのリクエストを送信- 1つ目は 
MyEntityを JSON にして送信 - 2つ目は 
MyEntityを XML にして送信 
 - 1つ目は 
 - JSON と XML のそれぞれで受け取れるリソースメソッドを定義し、ボディの内容を標準出力している
 
実行結果
== application/json ==
{"id":10}
== application/xml ==
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><myEntity><id>10</id></myEntity>
※WildFly 33.0.2.Final にデプロイして実行
説明
- JAXB および JSON-B をサポートしている環境(Jakarta EE サーバー環境)では、それぞれのエンティティプロバイダがデフォルトで有効になっている
 - このため、依存関係などを追加しなくてもそのまま XML, JSON の変換ができる
 
設定
- 設定や拡張機能を追加できる API として 
Configurableというインタフェースが用意されている - 
ClientBuilderやClient,WebTargetなどはこのConfigurableを継承または実装しているため、同じ方法で設定が可能となっている - 
Configurableを介して設定できるモノには以下の3つがある- プロパティ(Properties)
- キー・バリューの設定
 
 - フィーチャー(Features)
- 複数のプロバイダやプロパティをまとめて設定するための仕組み
 - 
Featureインタフェースを実装して作成する 
 - プロバイダ(Providers)
- 様々な機能追加や拡張を提供するものの総称
 - 前述のエンティティとのマッピング機能を提供するエンティティプロバイダもこの1つ
 
 
 - プロパティ(Properties)
 
プロバイダ(Providers)
エンティティプロバイダ(Entity Providers)
エンティティプロバイダ を参照。
コンテキストプロバイダ(Context Providers)
- 他のプロバイダに対してコンテキストを提供するプロバイダ
 - 例えば Jackson のエンティティプロバイダに対して 
ObjectMapperのインスタンスを提供したり、 JSON-B のエンティティプロバイダにJsonbのインスタンスを提供するプロバイダ - 
ContextResolverインタフェースを実装して作成する 
フィルター(Filters)
- フィルターを使うと、リクエストの送信前後にリクエストやレスポンスの情報を書き換えることができる
 - これにより、共通のヘッダーを設定したり、レスポンスの情報をログに出力するといったことができるようになる
 - フィルターは、以下のいずれかのインタフェースを実装して作成する
ClientRequestFilterClientResponseFilterContainerRequestFilterContainerResponseFilter
 - 
Container*Filterはサーバー側で使うものなのでここでは割愛 
実装
package sandbox.jaxrs.provider.filter;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.client.ClientResponseContext;
import jakarta.ws.rs.client.ClientResponseFilter;
import jakarta.ws.rs.core.MultivaluedMap;
public class MyFilter implements ClientRequestFilter, ClientResponseFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
        MultivaluedMap<String, Object> headers = requestContext.getHeaders();
        headers.add("X-Test-Header", "TestValue");
    }
    @Override
    public void filter(ClientRequestContext requestContext,
                       ClientResponseContext responseContext) {
        int length = responseContext.getLength();
        System.out.println("response length = " + length);
    }
}
- 
ClientRequestFilterおよびClientResponseFilterインタフェースを実装している - 
ClientRequestFilterのfilterメソッドでは、ヘッダーにX-Test-Headerというのを追加している - 
ClientResponseFilterのfilterメソッドでは、レスポンスボディの長さを取得して標準出力に出力している 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.filter.MyFilter;
public class JaxRsClientMain {
    public static void main(String[] args) {
        String response = ClientBuilder.newClient()
                .register(MyFilter.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.text("Hello World"), String.class);
        System.out.println(response);
    }
}
- 
registerメソッドでMyFilterを登録して実行している 
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
x-test-header: TestValue
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 11
===body===
Hello World
==========
- ヘッダーに 
x-test-headerが追加されているのが分かる 
response length = 11
Hello World
- 長さの情報が出力されている
 
説明
public class MyFilter implements ClientRequestFilter, ClientResponseFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
    ...
    @Override
    public void filter(ClientRequestContext requestContext,
                       ClientResponseContext responseContext) {
   ...
}
- 
ClientRequestFilterインタフェースを実装すると、リクエストが送信される前に処理を挟むことができる- 
ClientRequestContextだけを受け取るfilterメソッドを実装する - 
ClientRequestContextには送信しようとしているリクエストの情報が格納されており、このインスタンスの状態を書き換えることでリクエストの内容を変更できる 
 - 
 - 
ClientResponseFilterインタフェースを実装すると、レスポンスを受信した後に処理を挟むことができる- 
ClientRequestContextとClientResponseContextを受け取るfilterメソッドを実装する - 
ClientResponseContextからは、受信したレスポンスの情報を取得できる - レスポンスの内容の書き換えも可能
 
 - 
 
        String response = ClientBuilder.newClient()
                .register(MyFilter.class)
- 作成したフィルターは、 
ConfigurableのregisterメソッドにClassオブジェクトを渡すことで登録できる 
処理を中断する
package sandbox.jaxrs.provider.filter;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
public class MyAbortFilter implements ClientRequestFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
        Response response
            = Response.ok("abort!", MediaType.TEXT_PLAIN_TYPE).build();
        requestContext.abortWith(response);
    }
}
abort!
- 
ClientRequestContextのabortWithメソッドを呼ぶと、リクエストの処理が中断される - 
abortWithの引数には、クライアントに返すレスポンスを渡す必要がある - これは、例えばキャッシュがある場合は実際にはリクエストは投げずにキャッシュのレスポンスを返す、みたいな機能を実現したいときに利用できる
 
プロパティを受け取る
実装
package sandbox.jaxrs.provider.filter;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
public class MyPropertiesFilter implements ClientRequestFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
        for (String name : requestContext.getPropertyNames()) {
            System.out.println(name + ": " + requestContext.getProperty(name));
        }
    }
}
- 
ClientRequestContextからgetPropertyで取得できるプロパティの値を全て出力している 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import sandbox.jaxrs.provider.filter.MyPropertiesFilter;
public class JaxRsClientMain {
    public static void main(String[] args) {
        ClientBuilder.newClient()
                .property("foo", "FOO@Client")
                .register(MyPropertiesFilter.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .property("hoge", "HOGE@Invocation.Builder")
                .buildGet()
                .property("fuga", "FUGA@Invocation")
                .invoke()
                .close();
    }
}
- 
ClientとInvocation.Builder,Invocationでそれぞれプロパティを設定している 
実行結果
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
- 
Invocation.BuilderとInvocationで設定したプロパティだけが出力されている - 
Clientで設定したプロパティは出力されていない 
説明
    public void filter(ClientRequestContext requestContext) {
        for (String name : requestContext.getPropertyNames()) {
            System.out.println(name + ": " + requestContext.getProperty(name));
        }
    }
- 
ClientRequestContextからは、現在のリクエストコンテキストのプロパティにアクセスできる - リクエストコンテキストのプロパティは、 
Invocation.BuilderやInvocationのpropertyで設定できる - 
Clientのproperty(=Configurableで設定されたプロパティ)の値は参照できない- 
Configurableで設定したプロパティは、Configurationから参照できる - 詳しくは フィーチャー にて
 
 - 
 
インターセプター(Interceptors)
- インターセプターを使うと、エンティティの読み書きのときに処理をラップできる
 - インターセプタは、以下のいずれかのインタフェースを実装することで作成する
ReaderInterceptorWriterInterceptor
 
実装
package sandbox.jaxrs.provider.interceptor;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.ReaderInterceptorContext;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.ws.rs.ext.WriterInterceptorContext;
import java.io.IOException;
public class MyInterceptor implements ReaderInterceptor, WriterInterceptor {
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws IOException, WebApplicationException {
        try {
            System.out.println("[aroundReadFrom] before");
            return context.proceed();
        } finally {
            System.out.println("[aroundReadFrom] after");
        }
    }
    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        System.out.println("[aroundWriteTo] before");
        context.proceed();
        System.out.println("[aroundWriteTo] after");
    }
}
- 
ReaderInterceptorとWriterInterceptorを実装し、後続処理の呼び出し前後にログを出力している 
package sandbox.jaxrs.provider;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.Produces;
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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public class MySimpleEntityProvider implements
        MessageBodyReader<String>, MessageBodyWriter<String> {
    @Override
    public boolean isReadable(Class type,
                              Type genericType,
                              Annotation[] annotations,
                              MediaType mediaType) {
        return type == String.class;
    }
    @Override
    public String readFrom(Class type,
                           Type genericType,
                           Annotation[] annotations,
                           MediaType mediaType,
                           MultivaluedMap httpHeaders,
                           InputStream entityStream)
            throws IOException, WebApplicationException {
        System.out.println("readFrom");
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            entityStream.transferTo(out);
            return out.toString(StandardCharsets.UTF_8);
        }
    }
    @Override
    public boolean isWriteable(Class type,
                               Type genericType,
                               Annotation[] annotations,
                               MediaType mediaType) {
        return type == String.class;
    }
    @Override
    public void writeTo(String o,
                        Class type,
                        Type genericType,
                        Annotation[] annotations,
                        MediaType mediaType,
                        MultivaluedMap httpHeaders,
                        OutputStream entityStream)
            throws IOException, WebApplicationException {
        System.out.println("writeTo");
        try (OutputStreamWriter writer
                = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) {
            writer.write(o);
        }
    }
}
- 
Stringをそのまま書き出すだけの簡単なエンティティプロバイダ - 動作を見るために、各メソッド呼び出し時にログを出力している
 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.MySimpleEntityProvider;
import sandbox.jaxrs.provider.interceptor.MyInterceptor;
public class JaxRsClientMain {
    public static void main(String[] args) {
        String response = ClientBuilder.newClient()
                .register(MySimpleEntityProvider.class)
                .register(MyInterceptor.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.text("Hello World"), String.class);
        System.out.println(response);
    }
}
- 
MySimpleEntityProviderとMyInterceptorの両方を登録してリクエストを送信している 
実行結果
[aroundWriteTo] before
writeTo
[aroundWriteTo] after
[aroundReadFrom] before
readFrom
[aroundReadFrom] after
Hello World
- エンティティプロバイダのログの前後でインターセプターのログが出力されている
 
説明
public class MyInterceptor implements ReaderInterceptor, WriterInterceptor {
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws IOException, WebApplicationException {
        try {
            System.out.println("[aroundReadFrom] before");
            return context.proceed();
        } finally {
            System.out.println("[aroundReadFrom] after");
        }
    }
    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        System.out.println("[aroundWriteTo] before");
        context.proceed();
        System.out.println("[aroundWriteTo] after");
    }
}
- エンティティの読み取りの前後に処理を入れたい場合は 
ReaderInterceptorを実装する- 
aroundReadFromメソッドを実装する - 
ReaderInterceptorContextのproceedメソッドを呼ぶと後続処理が呼ばれるので、その前後で処理を実装する 
 - 
 - エンティティの書き出しの前後に処理を入れたい場合は 
WriterInterceptorを実装する- 
aroundWriteToメソッドを実装する - 
WriterInterceptorContextのproceedメソッドを呼ぶと後続処理が呼ばれるので、その前後で処理を実装する 
 - 
 
プロパティを受け取る
実装
package sandbox.jaxrs.provider.interceptor;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.ext.InterceptorContext;
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.ReaderInterceptorContext;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.ws.rs.ext.WriterInterceptorContext;
import java.io.IOException;
public class MyPropertiesInterceptor implements ReaderInterceptor, WriterInterceptor {
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws IOException, WebApplicationException {
        printProperties("aroundReadFrom", context);
        return context.proceed();
    }
    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        printProperties("aroundWriteTo", context);
        context.proceed();
    }
    private void printProperties(String method, InterceptorContext context) {
        System.out.println("[" + method + "]");
        for (String name : context.getPropertyNames()) {
            System.out.println(name + ": " + context.getProperty(name));
        }
    }
}
- 
aroundReadFrom,aroundWriteToのそれぞれで、コンテキストからプロパティの値を取得して出力している 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import sandbox.jaxrs.provider.interceptor.MyPropertiesInterceptor;
public class JaxRsClientMain {
    public static void main(String[] args) {
        ClientBuilder.newClient()
                .property("foo", "FOO@Client")
                .register(MyPropertiesInterceptor.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .property("hoge", "HOGE@Invocation.Builder")
                .buildPost(Entity.text("Hello"))
                .property("fuga", "FUGA@Invocation")
                .invoke()
                .readEntity(String.class);
    }
}
- 
ClientとInvocation.Builder,Invocationのそれぞれでプロパティを設定している 
実行結果
[aroundWriteTo]
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
[aroundReadFrom]
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
- 
Invocation.BuilderとInvocationで設定したプロパティのみが出力されている - 
Clientで設定したプロパティは出力されない 
説明
    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws IOException, WebApplicationException {
        printProperties("aroundReadFrom", context);
        return context.proceed();
    }
    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        printProperties("aroundWriteTo", context);
        context.proceed();
    }
    private void printProperties(String method, InterceptorContext context) {
        System.out.println("[" + method + "]");
        for (String name : context.getPropertyNames()) {
            System.out.println(name + ": " + context.getProperty(name));
        }
    }
- 
ReaderInterceptorContextおよびWriterInterceptorContextからは、リクエストコンテキストに設定されているプロパティを参照できる - リクエストコンテキストのプロパティは、 
Invocation.BuilderおよびInvocationのpropertyで設定できる - 
Clientのpropertyで設定したプロパティは、ここでは参照できない- 
Clientのpropertyで設定したプロパティは、Configurationから参照できる - 詳しくは フィーチャー を参照
 
 - 
 
フィーチャー(Features)
実装
package sandbox.jaxrs.provider.feature;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.FeatureContext;
import sandbox.jaxrs.provider.MySimpleEntityProvider;
import sandbox.jaxrs.provider.filter.MyFilter;
import sandbox.jaxrs.provider.interceptor.MyInterceptor;
public class MyFeature implements Feature {
    @Override
    public boolean configure(FeatureContext context) {
        context.register(MyFilter.class)
               .register(MyInterceptor.class)
               .register(MySimpleEntityProvider.class);
        return true;
    }
}
- 
Featureインタフェースを実装したクラスを作成 - 自作のフィルタ、インターセプター、エンティティプロバイダをそれぞれ登録している
- それぞれの実装は、ここまでのサンプルで使ってきた実装と同じ
 
 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.feature.MyFeature;
public class JaxRsClientMain {
    public static void main(String[] args) {
        ClientBuilder.newClient()
                .register(MyFeature.class)
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.text("Hello"), String.class);
    }
}
- 
MyFeatureをregisterで登録している 
実行結果
method=POST
requestURI=/debug/print
queryString=null
===headers===
content-type: text/plain
x-test-header: TestValue
user-agent: Jersey/3.1.8 (HttpUrlConnection 21.0.3)
host: localhost:8080
accept: */*
connection: keep-alive
content-length: 5
===body===
Hello
==========
- フィルタで設定した 
x-test-headerヘッダーが渡っている 
[aroundWriteTo] before
writeTo
[aroundWriteTo] after
response length = 5
[aroundReadFrom] before
readFrom
[aroundReadFrom] after
- フィルタ、インターセプター、エンティティプロバイダの処理が走っている
 
説明
public class MyFeature implements Feature {
    @Override
    public boolean configure(FeatureContext context) {
        context.register(MyFilter.class)
               .register(MyInterceptor.class)
               .register(MySimpleEntityProvider.class);
        return true;
    }
- フィーチャーは 
Featureインタフェースを実装することで作成できる - 
configureメソッドを実装する- 引数で受け取る 
FeatureContextを使ってプロバイダの登録など、任意の設定ができる - このフィーチャーが機能を有効にした場合は 
trueを返し、機能を有効にしない判断した場合はfalseを返す 
 - 引数で受け取る 
 - このように、フィーチャーを用いると複数のプロバイダの設定などを1つにまとめることができる
- Jackson のエンティティプロバイダなどは、実際はこのフィーチャーで提供されている
 - ただ、設定の有効化はサービス・ローダーの仕組みを使って裏で勝手に行われているので、表にはあまり出てこない
 
 
プロパティを受け取る
実装
package sandbox.jaxrs.provider.feature;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.FeatureContext;
public class MyPropertiesFeature implements Feature {
    @Override
    public boolean configure(FeatureContext context) {
        Configuration configuration = context.getConfiguration();
        for (String name : configuration.getPropertyNames()) {
            System.out.println(name + ": " + configuration.getProperty(name));
        }
        return true;
    }
}
- 
FeatureContextからConfigurationを取得し、プロパティを出力している 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import sandbox.jaxrs.provider.feature.MyPropertiesFeature;
public class JaxRsClientMain {
    public static void main(String[] args) {
        ClientBuilder.newBuilder()
                .property("hoge", "HOGE@ClientBuilder")
                .build()
                .register(MyPropertiesFeature.class)
                .property("fuga", "FUGA@Client")
                .target("http://localhost:8080/debug/print")
                .property("piyo", "PIYO@WebTarget")
                .request()
                .property("hoge", "HOGE@Invocation.Builder")
                .post(Entity.text("Hello"), String.class);
    }
}
- 
ClientBuilder,Client,WebTargetおよびInvocation.Builderのそれぞれでプロパティを設定している 
実行結果
hoge: HOGE@ClientBuilder
fuga: FUGA@Client
piyo: PIYO@WebTarget
- 
ClientBuilder,Client,WebTargetで設定したプロパティが出力されている - 
Invocation.Builderで設定したプロパティは出力されていない 
説明
    public boolean configure(FeatureContext context) {
        Configuration configuration = context.getConfiguration();
        for (String name : configuration.getPropertyNames()) {
            System.out.println(name + ": " + configuration.getProperty(name));
        }
        return true;
    }
- 
FeatureContextからは、Configurationに設定されたプロパティを参照できる - 
Configurationのプロパティには、Configurable経由で設定した値が反映されている - 大きく 
ConfigurableとInvocation.Builderの二か所でプロパティを設定できるが、それぞれは参照できる箇所が分かれている - 以下に整理する
 
- 
Configurableで設定したプロパティはConfigurationに反映される - 
Configurationに反映されたプロパティはFeatureを実装したクラスから参照できる 
- 
Invocation.BuilderおよびInvocationで設定したプロパティはリクエストコンテキストに保存される - リクエストコンテキストのプロパティは、 
InterceptorContextおよびClientRequestContextから参照できる - 
InterceptorContextはインターセプタから、ClientRequestContextはフィルターから参照できる 
UriBuilder を用いた URI の構築方法
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("/path")
                            .path("foo")
                            .path("bar")
                            .scheme("https")
                            .port(9090)
                            .host("localhost")
                            .queryParam("hoge", "HOGE")
                            .build();
        System.out.println(uri);
    }
}
https://localhost:9090/path/foo/bar?hoge=HOGE
- 
UriBuilderを用いることで、柔軟に URI を構築することができる - 
UriBuilderにはファクトリメソッドがいくつか用意されており、そこから構築をスタートする 
ファクトリメソッド
空のインスタンスを生成する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.newInstance()
                            .path("/hello")
                            .build();
        System.out.println(uri);
    }
}
/hello
- 
newInstanceは、何も URI の情報が設定されていない空の状態でインスタンスを生成する 
既存の URI 文字列をベースに生成する
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost:8080/test")
                            .path("foo")
                            .build();
        System.out.println(uri);
    }
}
http://localhost:8080/test/foo
- 
fromUriは、既存の URI 文字列をベースにしてビルダーを生成する 
既存の URI オブジェクトをベースに生成する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI baseUri = URI.create("http://localhost:8080/test");
        URI uri = UriBuilder.fromUri(baseUri)
                            .path("foo")
                            .build();
        System.out.println(uri);
    }
}
http://localhost:8080/test/foo
- 
fromUriは、既存のURIオブジェクトをベースにしてビルダーを生成する 
既存の相対パスをベースに生成する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("/test")
                            .path("foo")
                            .build();
        System.out.println(uri);
    }
}
/test/foo
- 
fromPathは、既存の相対パスをベースにしてビルダーを生成する 
既存の Link オブジェクトをベースに生成する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.Link;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        Link link = Link.fromUri("http://localhost:8080/hello").build();
        URI uri = UriBuilder.fromLink(link)
                            .path("foo")
                            .build();
        System.out.println(uri);
    }
}
http://localhost:8080/hello/foo
リソースクラスをベースに生成する
package sandbox.jaxrs.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/my-resource")
public class MyResource {
    @GET
    @Path("hoge")
    public String hogeMethod() {
        return "HOGE";
    }
    @GET
    @Path("fuga")
    public String fugaMethod() {
        return "FUGA";
    }
}
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import sandbox.jaxrs.resource.MyResource;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromResource(MyResource.class)
                            .path("foo")
                            .build();
        System.out.println(uri);
    }
}
/my-resource/foo
- 
fromResourceは、 JAX-RS のリソースクラスに設定された@Pathの値をベースにしたビルダーを生成する - 引数にはリソースクラスの 
Classオブジェクトを渡す 
リソースメソッドをベースに生成する
package sandbox.jaxrs.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/my-resource")
public class MyResource {
    @GET
    @Path("hoge")
    public String hogeMethod() {
        return "HOGE";
    }
    @GET
    @Path("fuga")
    public String fugaMethod() {
        return "FUGA";
    }
}
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import sandbox.jaxrs.resource.MyResource;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromMethod(MyResource.class, "hogeMethod")
                            .path("foo")
                            .build();
        System.out.println(uri);
    }
}
hoge/foo
- 
fromMethodは、 JAX-RS のリソースクラスのメソッドに設定された@Pathの値をベースにしたビルダーを生成する - 引数にはリソースクラスの 
Classオブジェクトとメソッドの名前を渡す - 指定された名前で 
@Pathのつけられたメソッドが1つしか存在しないことが前提- 複数存在する場合はエラーになる
 
 
スキーム・ホスト・ポートを設定する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .scheme("https")
                            .host("localhost")
                            .port(8080)
                            .build();
        System.out.println(uri);
    }
}
https://localhost:8080/test
- 
schemeでスキーム
hostでホスト
portでポートを設定できる 
パスを追加する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .path("foo")
                            .path("/bar")
                            .path("fizz/fizz buzz")
                            .build();
        System.out.println(uri);
    }
}
test/foo/bar/fizz/fizz%20buzz
- 
pathでパスを追加できる - パスとパスの間のスラッシュ (
/) の要・不要は自動的に判断される - スラッシュ (
/) はそのまま追加される - スラッシュ以外の URL エンコードが必要な文字はエンコードされる
 
リソースクラスのパスを追加する
package sandbox.jaxrs.resource;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/my-resource")
public class MyResource {
    @GET
    @Path("hoge")
    public String hogeMethod() {
        return "HOGE";
    }
    @GET
    @Path("fuga")
    public String fugaMethod() {
        return "FUGA";
    }
}
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import sandbox.jaxrs.resource.MyResource;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .path(MyResource.class)
                            .build();
        System.out.println(uri);
    }
}
test/my-resource
- 
path(Class)を使うと、指定したリソースクラスの@Pathに設定されたパスを追加できる 
リソースクラスのメソッドのパスを追加する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import sandbox.jaxrs.resource.MyResource;
import java.lang.reflect.Method;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) throws Exception {
        Method fugaMethod = MyResource.class.getMethod("fugaMethod");
        URI uri = UriBuilder.fromPath("test")
                            .path(MyResource.class, "hogeMethod")
                            .path(fugaMethod)
                            .build();
        System.out.println(uri);
    }
}
test/hoge/fuga
- 
path(Class, String)およびpath(Method)を使うと、指定したリソースクラスのメソッドに設定された@Pathの値を追加できる 
パスを置き換える
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost/foo/bar")
                            .replacePath("fizz/buzz")
                            .build();
        System.out.println(uri);
    }
}
http://localhost/fizz/buzz
- 
replacePathを使うと、 URI のパスの部分を置き換えることができる 
クエリパラメータを設定する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .queryParam("hoge", "HOGE")
                            .queryParam("fuga", "FUGA", "FUGA", "FUGA")
                            .queryParam("piyo", "PIYO/PIYO%2FPIYO")
                            .build();
        System.out.println(uri);
    }
}
test?hoge=HOGE&fuga=FUGA&fuga=FUGA&fuga=FUGA&piyo=PIYO%2FPIYO%2FPIYO
- 
queryParamメソッドでクエリパラメータを追加できる- 値は可変長引数で複数指定可能
 
 - URLエンコードが必要な部分は自動的にエンコードされる
- 既にエンコード済みの部分はそのまま埋め込まれる
 
 
クエリパラメータを文字列で直接設定する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost/test?foo=FOO")
                            .replaceQuery("hoge=HOGE&fuga=FUGA FUGA&piyo=PIYO%20PIYO")
                            .build();
        System.out.println(uri);
    }
}
http://localhost/test?hoge=HOGE&fuga=FUGA%20FUGA&piyo=PIYO%20PIYO
- 
replaceQuery(String)を使うと、クエリパラメータを指定した文字列で置き換えることができる - URL エンコードが必要な部分は自動的にエンコードされる
- 既にエンコード済みの部分はそのまま埋め込まれる
 
 
フラグメントを設定する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromPath("test")
                            .fragment("hoge")
                            .build();
        System.out.println(uri);
    }
}
test#hoge
- 
fragmentでフラグメントを設定できる 
テンプレート
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost/{foo}/{bar}/{foo}")
                            .build("FOO", "BAR");
        System.out.println(uri);
    }
}
http://localhost/FOO/BAR/FOO
- 
{名前}のように埋め込みパラメータを宣言することで、 URI をテンプレート化できる - 
buildでパラメータに埋め込む値を指定する場合、パラメータが出現した順序で埋め込みが行われる 
Map でパラメータを指定する
package sandbox.jaxrs.client;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.Map;
public class UriBuilderMain {
    public static void main(String[] args) {
        URI uri = UriBuilder.fromUri("http://localhost/{foo}/{bar}/{foo}")
                            .buildFromMap(Map.of("foo", "FOO", "bar", "BAR"));
        System.out.println(uri);
    }
}
http://localhost/FOO/BAR/FOO
- 
buildFromMapを使えばMapでパラメータを埋め込むことも可能 - その他、細かい使い方は 
Client構築時のテンプレートと同じなので そちら を参照 
コネクタの変更
- Jersey では、実際に HTTP 通信を行う部分の実装にデフォルトでは 
HttpUrlConnectionを利用している - この実際に HTTP 通信を行うコンポーネントのことをコネクタと呼ぶ
 - コネクタには、例えば Apache HTTP Client を使うものや java.net.http の 
HttpClientを使うものが用意されている 
HttpUrlConnection の制限
- デフォルトで利用される 
HttpUrlConnectionでも多くのケースでは問題なく利用できる - ただし、 
HttpUrlConnectionには以下に挙げるような制限がある- PATCH メソッドが利用できない
- 
HttpUrlConnectionは HTTP 1.1 を実装しているため、 PATCH メソッドが利用できない 
 - 
 - デフォルトでは利用できないヘッダーがある
- 
HttpUrlConnectionでは、以下のリクエストヘッダーはデフォルトでは設定できないように制限されているAccess-Control-Request-HeadersAccess-Control-Request-Method- 
Connection- ただし、値が 
Closedの場合は許可される 
 - ただし、値が 
 Content-LengthContent-Transfer-Encoding-HostKeep-AliveOriginTrailerTransfer-EncodingUpgradeVia- 
Sec-で始まるすべてのヘッダー 
 
 - 
 
 - PATCH メソッドが利用できない
 - 一応、それぞれ 
HttpUrlConnectionのまま回避する方法が用意されているが、それよりは Apache HTTP Client のコネクタなどの他のコネクタに切り替えることが推奨されている 
コネクタを変更する
PATCHメソッドのリクエストを投げる場合を例にして、コネクタを切り替える方法を説明する。
デフォルトコネクタの場合
実装
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
public class JaxRsClientMain {
    public static void main(String[] args) 
        ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .method("PATCH", Entity.text("Hello"))
                .close();
    }
}
実行結果
Exception in thread "main" jakarta.ws.rs.ProcessingException: java.net.ProtocolException: Invalid HTTP method: PATCH
	at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:288)
	...
Caused by: java.net.ProtocolException: Invalid HTTP method: PATCH
	at java.base/java.net.HttpURLConnection.setRequestMethod(HttpURLConnection.java:491)
	...
	... 12 more
- デフォルトのままだと、 PATCH メソッドのリクエストは送信できない
 
Java java.net.http client の場合
実装
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓以下を追加
    implementation "org.glassfish.jersey.connectors:jersey-jnh-connector:3.1.8"
}
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jnh.connector.JavaNetHttpConnectorProvider;
public class JaxRsClientMain {
    public static void main(String[] args) {
        ClientConfig config = new ClientConfig();
        config.connectorProvider(new JavaNetHttpConnectorProvider());
        ClientBuilder.newClient(config)
                .target("http://localhost:8080/debug/print")
                .request()
                .method("PATCH", Entity.text("Hello"))
                .close();
    }
}
実行結果
method=PATCH
requestURI=/debug/print
queryString=null
===headers===
content-length: 5
host: localhost:8080
content-type: text/plain
user-agent: Jersey/3.1.8 (Java HttpClient Connector 3.1.8)
===body===
Hello
==========
- PATCH メソッドでリクエストが送信できている
 
説明
dependencies {
    ...
    implementation "org.glassfish.jersey.connectors:jersey-jnh-connector:3.1.8"
}
- コネクタに Java java.net.http client を使用する場合は、 org.glassfish.jersey.connectors:jersey-jnh-connector を依存関係に追加する
 
        ClientConfig config = new ClientConfig();
        config.connectorProvider(new JavaNetHttpConnectorProvider());
        ClientBuilder.newClient(config)
- 各コネクタは 
ConnectorProviderインタフェースを実装したクラスを提供している- Java java.net.http client の場合は 
JavaNetHttpConnectorProvider 
 - Java java.net.http client の場合は 
 - この 
ConnectorProviderのインスタンスをClientConfigのconnectorProviderメソッドで設定し、ClientConfigを使ってClientに生成することでコネクタを変更できる - 他のコネクタの 
ConnectorProviderや Maven の依存関係については下記公式サイトの表を参照 
プロキシの設定方法
- Jersey でのプロキシの設定は、プロパティで行う
 - キーは ClientProperties に定義されている
PROXY_URIPROXY_USERNAMEPROXY_PASSWORD
 - なお、 
PROXY_URIをサポートしているのは以下のコネクタのみApacheConnectorProviderApache5ConnectorProviderGrizzlyConnectorProviderHelidonConnectorProviderNettyConnectorProviderJetty11ConnectorProviderJettyConnectorProvider
 - また、 
PROXY_USERNAMEとPROXY_PASSWORDをサポートしているのは以下のコネクタのみApacheConnectorProviderJettyConnectorProvider
 - プロパティに関する情報は A.5. Client configuration properties を参照
 
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
public class JaxRsClientMain {
    public static void main(String[] args) {
        ClientConfig config = new ClientConfig();
        config.connectorProvider(new ApacheConnectorProvider())
              .property(ClientProperties.PROXY_URI, "http://proxy.host.name:8080")
              .property(ClientProperties.PROXY_USERNAME, "username")
              .property(ClientProperties.PROXY_PASSWORD, "password");
        ClientBuilder.newClient(config)
                .target("http://localhost:8080/debug/print")
                .request()
                .method("PATCH", Entity.text("Hello"))
                .close();
    }
}
非同期実行
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import java.util.concurrent.Future;
public class JaxRsClientMain {
    public static void main(String[] args) throws Exception {
        try (Client client = ClientBuilder.newClient()) {
            Future<String> future = client
                    .target("http://localhost:8080/debug/print")
                    .request()
                    .async()
                    .post(Entity.text("Hello"), String.class);
            String response = future.get();
            System.out.println(response);
        }
    }
}
- 
Invocation.Builderのasyncメソッドを使用すると、リクエストの実行を非同期(別スレッド)で行うAsyncInvokerを作成できる - 
AsyncInvokerにはgetやpostなどの HTTP メソッドに対応する実行メソッドがある- 任意のメソッドを指定する場合は 
methodメソッドを使用する 
 - 任意のメソッドを指定する場合は 
 - 実行メソッドの戻り値は 
Futureとなっており、getメソッドで非同期処理の結果を待つことができる 
Invocation を非同期で実行する
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import java.util.concurrent.Future;
public class JaxRsClientMain {
    public static void main(String[] args) throws Exception {
        try (Client client = ClientBuilder.newClient()) {
            Invocation invocation = client
                    .target("http://localhost:8080/debug/print")
                    .request()
                    .buildPost(Entity.text("Hello"));
            Future<String> future = invocation.submit(String.class);
            String response = future.get();
            System.out.println(response);
        }
    }
}
- 
Invocationのsubmitメソッドを使うと、リクエストを非同期で実行できる - 
submitの戻り値はFutureになっているので、非同期の処理結果を待機できる 
ExecutorService を指定する
package sandbox.jaxrs.client;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class JaxRsClientMain {
    public static void main(String[] args) throws Exception {
        try (ExecutorService executorService = Executors.newSingleThreadExecutor()) {
            ClientBuilder builder = ClientBuilder.newBuilder()
                                                 .executorService(executorService);
            try (Client client = builder.build()) {
                Invocation invocation = client
                        .target("http://localhost:8080/debug/print")
                        .request()
                        .buildPost(Entity.text("Hello"));
                Future<String> future = invocation.submit(String.class);
                String response = future.get();
                System.out.println(response);
            }
        }
    }
}
- 
ClientBuilderのexecutorServiceメソッドにExecutorServiceを渡すことができる - これにより非同期でリクエストを実行するときは、ここで設定された 
ExecutorServiceが作成したスレッドが使われるようになる