1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JAX-RS Client (Eclipse Jersey)使い方メモ

Posted at

はじめに

  • 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

検証用のサーバー

PrintServlet.java
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 もリクエストと同じものをレスポンスに設定するようにしている

実装

build.gradle
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"
}
JaxRsClientMain.java
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

説明

build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
}
JaxRsClientMain.java
        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 インスタンスを使用する
  • ClientBuilderClient インスタンスを生成し、各種リクエストの情報を設定していく
  • HTTPレスポンスは Response で取得でき、そこからレスポンスの情報にアクセスできる

リクエスト送信の流れ

Client の生成

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
...

Client client = ClientBuilder.newClient();
  • HTTPリクエストを送信するには、まず Client のインスタンスを用意する
  • Client インスタンスは、 ClientBuildernewClient メソッドで取得できる
  • 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

  • ClientAutoCloseable を継承しているので、不要になったらクローズが必要

WebTarget の構築

import jakarta.ws.rs.client.WebTarget;
...

WebTarget target = client.target("http://localhost:8080/debug/print");
  • Clienttarget メソッドで 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 を使用する
URLエンコード済みの値を埋め込みたい場合
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 }
  • pathqueryParam, 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
  • propertyregister などの設定メソッドは、インスタンスの状態を変更して同じインスタンスを返す
  • 設定についての説明は後述

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");
  • WebTargetrequest メソッドを実行すると、 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 で指定する
    • postput など、ボディを持つことができる HTTP メソッドに対応するメソッドであれば引数に Entity を受け取れるようになっている
  • Entity のインスタンスは Entity にある static なファクトリメソッドで生成できる
    • text メソッドを使って Entity を作成すると、ヘッダーに Content-Type: text/plain が自動的に付与される
    • 同様に jsonxml など、 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();
  • BuilderbuildGet などのビルドメソッドを実行すると、そこまでに構築した HTTP リクエストの情報を保持した Invocation のインスタンスを取得できる
    • 他に HTTP メソッドを指定できるビルドメソッドとしては、 buildPost, buildPut, buildDelete がある
    • それ以外の HTTP メソッドについては build メソッドを使用する(引数で任意の 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 の型を指定する
    • ボディのデシリアライズに関しての詳細は後述
  • ResponseAutoCloseable を継承している
    • 一応 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 などのメソッドや Invocationinvoke などのメソッドには、レスポンスのエンティティの型を引数で指定できるようになっている
  • これを指定すると、戻り値の型は Response ではなく、引数で指定したエンティティの型になり直接エンティティだけを受け取ることができるようになる
    • 要するに、いきなり ResponsereadEntity を呼んでいる感じになる
  • レスポンスのステータスコードが 2xx 系でない場合は例外がスローされる

ボディ(エンティティ)のシリアライズ・デシリアライズ

ここでは HTTP メッセージのボディ(エンティティ)のシリアライズとデシリアライズがどのような仕組みで制御されているのか、どう調整できるのかについてまとめる。

エンティティプロバイダ

  • リクエストおよびレスポンスボディ(エンティティ)のシリアライズ・デシリアライズは、エンティティプロバイダによって行われる
  • エンティティプロバイダは MessageBodyWriter および MessageBodyReader インタフェースを実装することで作成できる
  • それぞれのインタフェースの定義は以下のようになっている
MessageBodyWriter
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;
}
MessageBodyReader
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 のランタイムは、エンティティを書き出すときにまずは登録されている各 MessageBodyWriterisWritable を呼び出す
    • isWritable には、書き出そうとしているエンティティの型(Class)やメディアタイプなどの情報が渡される
    • isWritable は渡された情報をもとに、そのエンティティのシリアライズに対応しているかどうかを判定し boolean で結果を返す(対応している場合は true)
    • JAX-RS のランタイムは、最初に true を返した MessageBodyWriterwriteTo メソッドを使ってエンティティの書き出しを行う
  • 読み取りの場合も同様で、登録されている各 MessageBodyReaderisReadable が呼ばれる
    • isReadable にはデシリアライズ後のエンティティの型やメディアタイプなどが渡されるので、デシリアライズに対応している場合は true を返すように実装する
    • JAX-RS ランタイムは最初に true を返した MessageBodyReaderreadFrom メソッドを使ってエンティティのデシリアライズを行う

MessageBodyWritergetSize メソッドは実装不要

MessageBodyWritergetSize メソッドは、 JAX-RS 2.0 では無視されるようになっている。
Content-Length ヘッダーに設定する値の計算は JAX-RS のランタイムが行うようになっている。

エンティティプロバイダの登録方法

実装

MyLocalDateEntityProvider
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);
    }
}
  • LocalDateyyyy/MM/dd 形式の文字列にシリアライズ・デシリアライズするエンティティプロバイダ
JaxRsClientMain
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

説明

JaxRsClientMain
        LocalDate result = ClientBuilder.newClient()
                            .register(MyLocalDateEntityProvider.class)
  • エンティティプロバイダの登録は、 Client などが提供している register メソッドを使用して行う
  • 引数にプロバイダの Class オブジェクトを渡すことで登録できる
  • register メソッドは、実際には Configurable インタフェースで定義されており、 Client はこれを継承している関係になっている
  • 他にも WebTargetConfigurable を継承しているので、こちらでもプロバイダの登録ができる

メディアタイプによる候補の絞り込み

実装

MyLocalDateJsonEntityProvider.java
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 が呼ばれた標準出力にメッセージを出力するようにしている
JaxRsClientMain.java
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()));
    }
}
  • エンティティプロバイダとして MyLocalDateEntityProviderMyLocalDateJsonEntityProvider の2つを登録
  • 最初のリクエストは Entity.json() を使って Content-Typeapplication/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
==========

説明

MyLocalDateJsonEntityProvider.java
@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/xml
application/xml
applicaiton/*+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)

実装

build.gradle
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"
}
MyEntity.java
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 でクラスを注釈しておく
JaxRsClientMain.java
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 もデシリアライズされて取得できている

説明

MyEntity.java
@XmlRootElement
public class MyEntity {
  • JAXB で処理されるには、エンティティが @XmlRootElement で注釈されている必要がある

XmlRootElement でエンティティを注釈できない場合

  • 変換したいエンティティを @XmlRootElement で注釈できない場合は、 JAXBElement を使用する

実装

JaxRsClientMain.java
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}

説明

JaxRsClientMain
        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 オブジェクトを渡す
    • 第三引数には、変換対象のエンティティ自身を渡す
  • JAXBElementEntity に詰めてボディに設定する
JaxRsClientMain.java
        JAXBElement<MyEntity> responseElement = ClientBuilder.newClient()
                .target("http://localhost:8080/debug/print")
                .request()
                .post(Entity.xml(jaxbEntity), new GenericType<>() {});
  • レスポンスは GenericType を使用し、同じく JAXBElement で受け取る

JSON-B (JSON)

実装

build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-binding:3.1.8"
}
MyEntity.java
package sandbox.jaxrs.client;

public class MyEntity {
    private int id;

    public MyEntity() {}

    // getter, setter, toString は省略
}
JaxRsClientMain.java
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}

説明

build.gradle
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 を調整したい場合は以下のようにする

実装

MyJsonbConfigProvider.java
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);
    }
}
JaxRsClientMain.java
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
}
==========

説明

MyJsonbConfigProvider.java
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 を返すように実装する
JaxRsClientMain.java
        MyEntity response = ClientBuilder.newClient()
                .register(MyJsonbConfigProvider.class)
  • 作成した自作の ContextResolverregister で登録することで有効化できる

Jackson (JSON)

実装

build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓を追加
    implementation "org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8"
}
MyEntity.java
package sandbox.jaxrs.client;

public class MyEntity {
    private int id;

    public MyEntity() {}

    // getter, setter, toString は省略
}
JaxRsClientMain.java
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 にデシリアライズできている

説明

build.gradle
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 を調整したい場合は以下のようにする

実装

MyObjectMapperProvider.java
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 を返すように実装
JaxRsClientMain.java
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);
    }
}
  • registerMyObjectMapperProvider を登録するように設定

実行結果

実行結果(サーバー側)
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 がフォーマットされている

説明

MyObjectMapperProvider.java
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 を返すように実装する
JaxRsClientMain.java
        MyEntity response = ClientBuilder.newClient()
                .register(MyObjectMapperProvider.class)
  • 作成した自作の ContextResolverregister で登録することで有効化できる

Jakarta EE 環境だと標準でサポートされているエンティティプロバイダ

  • Jakarta EE 環境だと、 JSON や XML のエンティティプロバイダがデフォルトでサポートされるようになっている

実装

フォルダ構成
|-build.gradle
`-src/main/java/
  `-sandbox/jaxrs/
    |-MyApplication.java
    |-HelloResource.java
    `-MyEntity.java
build.gradle
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 が生成されるように設定
MyApplication.java
package sandbox.jaxrs;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")
public class MyApplication extends Application {
}
  • @ApplicationPath の設定
  • パスのルートを /api で定義
MyEntity.java
package sandbox.jaxrs;

import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class MyEntity {
    public int id;
}
  • ボディに使用するエンティティ
  • @XmlRootElement で注釈して JAXB のエンティティプロバイダの処理対象となるように定義
HelloResource.java
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 にして送信
  • 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 というインタフェースが用意されている
  • ClientBuilderClient, WebTarget などはこの Configurable を継承または実装しているため、同じ方法で設定が可能となっている
  • Configurable を介して設定できるモノには以下の3つがある
    1. プロパティ(Properties)
      • キー・バリューの設定
    2. フィーチャー(Features)
      • 複数のプロバイダやプロパティをまとめて設定するための仕組み
      • Feature インタフェースを実装して作成する
    3. プロバイダ(Providers)
      • 様々な機能追加や拡張を提供するものの総称
      • 前述のエンティティとのマッピング機能を提供するエンティティプロバイダもこの1つ

プロバイダ(Providers)

エンティティプロバイダ(Entity Providers)

エンティティプロバイダ を参照。

コンテキストプロバイダ(Context Providers)

  • 他のプロバイダに対してコンテキストを提供するプロバイダ
  • 例えば Jackson のエンティティプロバイダに対して ObjectMapper のインスタンスを提供したり、 JSON-B のエンティティプロバイダに Jsonb のインスタンスを提供するプロバイダ
  • ContextResolver インタフェースを実装して作成する

フィルター(Filters)

  • フィルターを使うと、リクエストの送信前後にリクエストやレスポンスの情報を書き換えることができる
  • これにより、共通のヘッダーを設定したり、レスポンスの情報をログに出力するといったことができるようになる
  • フィルターは、以下のいずれかのインタフェースを実装して作成する
    • ClientRequestFilter
    • ClientResponseFilter
    • ContainerRequestFilter
    • ContainerResponseFilter
  • Container*Filter はサーバー側で使うものなのでここでは割愛

実装

MyFilter.java
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 インタフェースを実装している
  • ClientRequestFilterfilter メソッドでは、ヘッダーに X-Test-Header というのを追加している
  • ClientResponseFilterfilter メソッドでは、レスポンスボディの長さを取得して標準出力に出力している
JaxRsClientMain.java
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
  • 長さの情報が出力されている

説明

MyFilter.java
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 インタフェースを実装すると、レスポンスを受信した後に処理を挟むことができる
    • ClientRequestContextClientResponseContext を受け取る filter メソッドを実装する
    • ClientResponseContext からは、受信したレスポンスの情報を取得できる
    • レスポンスの内容の書き換えも可能
JaxRsClientMain.java
        String response = ClientBuilder.newClient()
                .register(MyFilter.class)
  • 作成したフィルターは、 Configurableregister メソッドに Class オブジェクトを渡すことで登録できる

処理を中断する

MyAbortFilter.java
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!
  • ClientRequestContextabortWith メソッドを呼ぶと、リクエストの処理が中断される
  • abortWith の引数には、クライアントに返すレスポンスを渡す必要がある
  • これは、例えばキャッシュがある場合は実際にはリクエストは投げずにキャッシュのレスポンスを返す、みたいな機能を実現したいときに利用できる

プロパティを受け取る

実装

MyPropertiesFilter.java
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 で取得できるプロパティの値を全て出力している
JaxRsClientMain.java
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();
    }
}
  • ClientInvocation.Builder, Invocation でそれぞれプロパティを設定している

実行結果

実行結果(クライアント側出力)
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
  • Invocation.BuilderInvocation で設定したプロパティだけが出力されている
  • Client で設定したプロパティは出力されていない

説明

MyPropertiesFilter.java
    public void filter(ClientRequestContext requestContext) {
        for (String name : requestContext.getPropertyNames()) {
            System.out.println(name + ": " + requestContext.getProperty(name));
        }
    }
  • ClientRequestContext からは、現在のリクエストコンテキストのプロパティにアクセスできる
  • リクエストコンテキストのプロパティは、 Invocation.BuilderInvocation property で設定できる
  • Clientproperty (= Configurable で設定されたプロパティ)の値は参照できない
    • Configurable で設定したプロパティは、 Configuration から参照できる
    • 詳しくは フィーチャー にて

インターセプター(Interceptors)

  • インターセプターを使うと、エンティティの読み書きのときに処理をラップできる
  • インターセプタは、以下のいずれかのインタフェースを実装することで作成する
    • ReaderInterceptor
    • WriterInterceptor

実装

MyInterceptor.java
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");
    }
}
  • ReaderInterceptorWriterInterceptor を実装し、後続処理の呼び出し前後にログを出力している
MySimpleEntityProvider.java
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 をそのまま書き出すだけの簡単なエンティティプロバイダ
  • 動作を見るために、各メソッド呼び出し時にログを出力している
JaxRsClientMain.java
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);
    }
}
  • MySimpleEntityProviderMyInterceptor の両方を登録してリクエストを送信している

実行結果

実行結果(クライアント側出力)
[aroundWriteTo] before
writeTo
[aroundWriteTo] after
[aroundReadFrom] before
readFrom
[aroundReadFrom] after
Hello World
  • エンティティプロバイダのログの前後でインターセプターのログが出力されている

説明

MyInterceptor.java
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 メソッドを実装する
    • ReaderInterceptorContextproceed メソッドを呼ぶと後続処理が呼ばれるので、その前後で処理を実装する
  • エンティティの書き出しの前後に処理を入れたい場合は WriterInterceptor を実装する
    • aroundWriteTo メソッドを実装する
    • WriterInterceptorContextproceed メソッドを呼ぶと後続処理が呼ばれるので、その前後で処理を実装する

プロパティを受け取る

実装

MyPropertiesInterceptor.java
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 のそれぞれで、コンテキストからプロパティの値を取得して出力している
JaxRsClientMain.java
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);
    }
}
  • ClientInvocation.Builder, Invocation のそれぞれでプロパティを設定している

実行結果

実行結果
[aroundWriteTo]
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
[aroundReadFrom]
hoge: HOGE@Invocation.Builder
fuga: FUGA@Invocation
  • Invocation.BuilderInvocation で設定したプロパティのみが出力されている
  • Client で設定したプロパティは出力されない

説明

MyPropertiesInterceptor.java
    @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 および Invocationproperty で設定できる
  • Clientproperty で設定したプロパティは、ここでは参照できない
    • Clientproperty で設定したプロパティは、 Configuration から参照できる
    • 詳しくは フィーチャー を参照

フィーチャー(Features)

実装

MyFeature.java
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 インタフェースを実装したクラスを作成
  • 自作のフィルタ、インターセプター、エンティティプロバイダをそれぞれ登録している
    • それぞれの実装は、ここまでのサンプルで使ってきた実装と同じ
JaxRsClientMain.java
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);
    }
}
  • MyFeatureregister で登録している

実行結果

実行結果(サーバー側)
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
  • フィルタ、インターセプター、エンティティプロバイダの処理が走っている

説明

MyFeature.java
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 のエンティティプロバイダなどは、実際はこのフィーチャーで提供されている
    • ただ、設定の有効化はサービス・ローダーの仕組みを使って裏で勝手に行われているので、表にはあまり出てこない

プロパティを受け取る

実装

MyPropertiesFeature.java
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 を取得し、プロパティを出力している
JaxRsClientMain.java
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 で設定したプロパティは出力されていない

説明

MyPropertiesFeature.java
    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 経由で設定した値が反映されている
  • 大きく ConfigurableInvocation.Builder の二か所でプロパティを設定できるが、それぞれは参照できる箇所が分かれている
  • 以下に整理する
  • Configurable で設定したプロパティは Configuration に反映される
  • Configuration に反映されたプロパティは Feature を実装したクラスから参照できる
  • Invocation.Builder および Invocation で設定したプロパティはリクエストコンテキストに保存される
  • リクエストコンテキストのプロパティは、 InterceptorContext および ClientRequestContext から参照できる
  • InterceptorContext はインターセプタから、 ClientRequestContext はフィルターから参照できる

UriBuilder を用いた URI の構築方法

UriBuilderMain.java
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 にはファクトリメソッドがいくつか用意されており、そこから構築をスタートする

ファクトリメソッド

空のインスタンスを生成する

UriBuilderMain.java
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 文字列をベースに生成する

UriBuilderMain.javapackage 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:8080/test")
                            .path("foo")
                            .build();

        System.out.println(uri);
    }
}
実行結果
http://localhost:8080/test/foo
  • fromUri は、既存の URI 文字列をベースにしてビルダーを生成する

既存の URI オブジェクトをベースに生成する

UriBuilderMain.java
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 オブジェクトをベースにしてビルダーを生成する

既存の相対パスをベースに生成する

UriBuilderMain.java
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 オブジェクトをベースに生成する

UriBuilderMain.java
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

リソースクラスをベースに生成する

MyResource.java
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";
    }
}
UriBuilderMain.java
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 オブジェクトを渡す

リソースメソッドをベースに生成する

MyResource.java
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";
    }
}
UriBuilderMain.java
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つしか存在しないことが前提
    • 複数存在する場合はエラーになる

スキーム・ホスト・ポートを設定する

UriBuilderMain.java
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 でポートを設定できる

パスを追加する

UriBuilderMain.java
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 エンコードが必要な文字はエンコードされる

リソースクラスのパスを追加する

MyResource.java
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";
    }
}
UriBuilderMain.java
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 に設定されたパスを追加できる

リソースクラスのメソッドのパスを追加する

UriBuilderMain.java
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 の値を追加できる

パスを置き換える

UriBuilderMain.java
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 のパスの部分を置き換えることができる

クエリパラメータを設定する

UriBuilderMain.java
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エンコードが必要な部分は自動的にエンコードされる
    • 既にエンコード済みの部分はそのまま埋め込まれる

クエリパラメータを文字列で直接設定する

UriBuilderMain.java
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 エンコードが必要な部分は自動的にエンコードされる
    • 既にエンコード済みの部分はそのまま埋め込まれる

フラグメントを設定する

UriBuilderMain.java
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 でフラグメントを設定できる

テンプレート

UriBuilderMain.java
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 でパラメータを指定する

UriBuilderMain.java
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-Headers
        • Access-Control-Request-Method
        • Connection
          • ただし、値が Closed の場合は許可される
        • Content-Length
        • Content-Transfer-Encoding-
        • Host
        • Keep-Alive
        • Origin
        • Trailer
        • Transfer-Encoding
        • Upgrade
        • Via
        • Sec- で始まるすべてのヘッダー
  • 一応、それぞれ HttpUrlConnection のまま回避する方法が用意されているが、それよりは Apache HTTP Client のコネクタなどの他のコネクタに切り替えることが推奨されている

コネクタを変更する

PATCHメソッドのリクエストを投げる場合を例にして、コネクタを切り替える方法を説明する。

デフォルトコネクタの場合

実装

JaxRsClientMain.java
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 の場合

実装

build.gradle
dependencies {
    implementation "org.glassfish.jersey.core:jersey-client:3.1.8"
    // ↓以下を追加
    implementation "org.glassfish.jersey.connectors:jersey-jnh-connector:3.1.8"
}
JaxRsClientMain.java
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 メソッドでリクエストが送信できている

説明

build.gradle
dependencies {
    ...
    implementation "org.glassfish.jersey.connectors:jersey-jnh-connector:3.1.8"
}
JaxRsClientMain.java
        ClientConfig config = new ClientConfig();
        config.connectorProvider(new JavaNetHttpConnectorProvider());

        ClientBuilder.newClient(config)
  • 各コネクタは ConnectorProvider インタフェースを実装したクラスを提供している
    • Java java.net.http client の場合は JavaNetHttpConnectorProvider
  • この ConnectorProvider のインスタンスを ClientConfigconnectorProvider メソッドで設定し、 ClientConfig を使って Client に生成することでコネクタを変更できる
  • 他のコネクタの ConnectorProvider や Maven の依存関係については下記公式サイトの表を参照

プロキシの設定方法

  • Jersey でのプロキシの設定は、プロパティで行う
  • キーは ClientProperties に定義されている
    • PROXY_URI
    • PROXY_USERNAME
    • PROXY_PASSWORD
  • なお、 PROXY_URI をサポートしているのは以下のコネクタのみ
    • ApacheConnectorProvider
    • Apache5ConnectorProvider
    • GrizzlyConnectorProvider
    • HelidonConnectorProvider
    • NettyConnectorProvider
    • Jetty11ConnectorProvider
    • JettyConnectorProvider
  • また、 PROXY_USERNAMEPROXY_PASSWORD をサポートしているのは以下のコネクタのみ
    • ApacheConnectorProvider
    • JettyConnectorProvider
  • プロパティに関する情報は A.5. Client configuration properties を参照
JaxRsClientMain.java
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();
    }
}

非同期実行

JaxRsClientMain.java
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.Builderasync メソッドを使用すると、リクエストの実行を非同期(別スレッド)で行う AsyncInvoker を作成できる
  • AsyncInvoker には getpost などの HTTP メソッドに対応する実行メソッドがある
    • 任意のメソッドを指定する場合は method メソッドを使用する
  • 実行メソッドの戻り値は Future となっており、 get メソッドで非同期処理の結果を待つことができる

Invocation を非同期で実行する

JaxRsClientMain.java
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);
        }
    }
}
  • Invocationsubmit メソッドを使うと、リクエストを非同期で実行できる
  • submit の戻り値は Future になっているので、非同期の処理結果を待機できる

ExecutorService を指定する

JaxRsClientMain.java
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);
            }
        }
    }
}
  • ClientBuilderexecutorService メソッドに ExecutorService を渡すことができる
  • これにより非同期でリクエストを実行するときは、ここで設定された ExecutorService が作成したスレッドが使われるようになる

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?