はじめに
- 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/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)
実装
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)
- フィルターを使うと、リクエストの送信前後にリクエストやレスポンスの情報を書き換えることができる
- これにより、共通のヘッダーを設定したり、レスポンスの情報をログに出力するといったことができるようになる
- フィルターは、以下のいずれかのインタフェースを実装して作成する
ClientRequestFilter
ClientResponseFilter
ContainerRequestFilter
ContainerResponseFilter
-
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)
- インターセプターを使うと、エンティティの読み書きのときに処理をラップできる
- インターセプタは、以下のいずれかのインタフェースを実装することで作成する
ReaderInterceptor
WriterInterceptor
実装
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-Headers
Access-Control-Request-Method
-
Connection
- ただし、値が
Closed
の場合は許可される
- ただし、値が
Content-Length
Content-Transfer-Encoding-
Host
Keep-Alive
Origin
Trailer
Transfer-Encoding
Upgrade
Via
-
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_URI
PROXY_USERNAME
PROXY_PASSWORD
- なお、
PROXY_URI
をサポートしているのは以下のコネクタのみApacheConnectorProvider
Apache5ConnectorProvider
GrizzlyConnectorProvider
HelidonConnectorProvider
NettyConnectorProvider
Jetty11ConnectorProvider
JettyConnectorProvider
- また、
PROXY_USERNAME
とPROXY_PASSWORD
をサポートしているのは以下のコネクタのみApacheConnectorProvider
JettyConnectorProvider
- プロパティに関する情報は 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
が作成したスレッドが使われるようになる