RESTEasy Clientを使う時、HTTPクライアントの実装として次の2つから選択することができます。
- Apache HttpComponents/Client
- java.net.HttpURLConnection
デフォルトは、Apache HttpComponents/Clientです。
Apache HTTP Client 4.x and other backends
今回は、この切り替えとHTTPクライアントの実装をOkHttp3にするサンプルを書いてみたいと思います。
準備
依存ライブラリは、次のようにしました。
libraryDependencies ++= Seq( // for RESTEasy Client "org.jboss.resteasy" % "resteasy-client" % "3.1.4.Final" % Compile exclude("junit", "junit"), "org.jboss.resteasy" % "resteasy-jackson2-provider" % "3.1.4.Final" % Compile, "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.8.9" % Compile, // dependency retrieve "org.jboss.resteasy" % "resteasy-jaxrs" % "3.1.4.Final" % Compile, "org.jboss.logging" % "jboss-logging" % "3.3.0.Final" % Compile, "org.jboss.logging" % "jboss-logging-annotations" % "2.0.1.Final" % Compile, "org.jboss.logging" % "jboss-logging-processor" % "2.0.1.Final" % Compile, "org.apache.httpcomponents" % "httpclient" % "4.5.2" % Compile, "com.fasterxml.jackson.core" % "jackson-core" % "2.8.9" % Compile, "com.fasterxml.jackson.core" % "jackson-annotations" % "2.8.9" % Compile, "com.fasterxml.jackson.core" % "jackson-databind" % "2.8.9" % Compile, "com.fasterxml.jackson.jaxrs" % "jackson-jaxrs-json-provider" % "2.8.9" % Compile, "javax.activation" % "activation" % "1.1.1" % Compile, "org.jboss.spec.javax.servlet" % "jboss-servlet-api_3.1_spec" % "1.0.0.Final" % Compile, "org.jboss.spec.javax.annotation" % "jboss-annotations-api_1.2_spec" % "1.0.0.Final" % Compile, "org.jboss.spec.javax.ws.rs" % "jboss-jaxrs-api_2.0_spec" % "1.0.1.Beta1" % Compile, "commons-io" % "commons-io" % "2.5" % Compile, "net.jcip" % "jcip-annotations" % "1.0" % Compile, // for Customize HttpEngine "com.squareup.okhttp3" % "okhttp" % "3.9.0" % Compile, // for test "org.jboss.resteasy" % "resteasy-netty4" % "3.1.4.Final" % Test, "io.netty" % "netty-all" % "4.1.15.Final" % Test, "org.scalatest" %% "scalatest" % "3.0.4" % Test )
えらい長いんですけど、これはsbtがbomをうまく理解できないからみたいです…。
依存している各種ライブラリのバージョンは、こちらを参照…。
https://github.com/resteasy/Resteasy/blob/3.1.4.Final/resteasy-dependencies-bom/pom.xml
ホントは、これくらいでいいのですが。「jackson-module-scala」があるのは、Scalaを使っている関係からですけどね。
// for RESTEasy Client "org.jboss.resteasy" % "resteasy-client" % "3.1.4.Final" % Compile, "org.jboss.resteasy" % "resteasy-jackson2-provider" % "3.1.4.Final" % Compile, "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.8.9" % Compile,
こちらは、テスト用です。
// for test "org.jboss.resteasy" % "resteasy-netty4" % "3.1.4.Final" % Test, "io.netty" % "netty-all" % "4.1.15.Final" % Test, "org.scalatest" %% "scalatest" % "3.0.4" % Test
テスト用のJAX-RSサーバーは、Netty4+RESTEasyで作ることにします。
あと、Jackson2用モジュールを有効化しておきましょう。こういうクラスを作成。
src/test/scala/org/littlewings/javaee7/resteasy/ScalaObjectMapperProvider.scala
package org.littlewings.javaee7.resteasy import javax.ws.rs.{Consumes, Produces} import javax.ws.rs.core.MediaType import javax.ws.rs.ext.{ContextResolver, Provider} import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule @Provider @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) class ScalaObjectMapperProvider extends ContextResolver[ObjectMapper] { override def getContext(typ: Class[_]): ObjectMapper = { val objectMapper = new ObjectMapper objectMapper.registerModule(DefaultScalaModule) objectMapper } }
このクラスが有効になるように、META-INF/servicesに仕込んでおきます。
src/test/resources/META-INF/services/javax.ws.rs.ext.Providers
org.littlewings.javaee7.resteasy.ScalaObjectMapperProvider
リクエスト/レスポンスで使うEntityクラス
単純に、書籍で。
src/test/scala/org/littlewings/javaee7/resteasy/Book.scala
package org.littlewings.javaee7.resteasy case class Book(isbn: String, title: String, price: Int)
テスト用JAX-RSサーバー
テスト用のJAX-RSサーバーは、Netty4+RESTEasyを使ってこんな感じで。
src/test/scala/org/littlewings/javaee7/resteasy/TestRestServer.scala
package org.littlewings.javaee7.resteasy import javax.ws.rs.core.MediaType import javax.ws.rs._ import org.jboss.resteasy.plugins.server.netty.NettyJaxrsServer object TestRestServer { def withServer(fun: => Unit): Unit = { val netty = new NettyJaxrsServer val deployment = netty.getDeployment deployment.setResourceClasses(java.util.Arrays.asList(classOf[TestResource].getName)) netty.setRootResourcePath("") netty.setPort(8080) netty.setDeployment(deployment) netty.start() try { fun } finally { netty.stop() } } } @Path("test") class TestResource { @GET @Path("get") @Produces(Array(MediaType.APPLICATION_JSON)) def get: Book = Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104) @POST @Path("post") @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) def post(book: Book): Book = book.copy(price = book.price * 2) @PUT @Path("put") @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) def put(book: Book): Book = book.copy(price = book.price * 2) @DELETE @Path("delete") @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) def delete(book: Book): Unit = () }
ちょっと例としては微妙ですが、POST/PUTは送信されてきた書籍情報の価格を2倍にして返すことにしました。
@POST @Path("post") @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) def post(book: Book): Book = book.copy(price = book.price * 2) @PUT @Path("put") @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) def put(book: Book): Book = book.copy(price = book.price * 2)
こんな感じで使います。
TestRestServer.withServer { .... }
テストコードの雛形
テストコードの雛形は、このように。
src/test/scala/org/littlewings/javaee7/resteasy/RestEasyHttpEngineSpec.scala
package org.littlewings.javaee7.resteasy import javax.ws.rs.client.{ClientBuilder, Entity} import javax.ws.rs.core.Response import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder import org.jboss.resteasy.client.jaxrs.engines.URLConnectionEngine import org.scalatest.{FunSuite, Matchers} class RestEasyHttpEngineSpec extends FunSuite with Matchers { // ここに、テストを書く! }
では、順次書いていってみましょう。
標準(Apache HttpComponents/Client)
まずは、Apache HttpCompontents/Clientを使ったパターンから。
といっても、こちらはふつうに書けばOKです。
test("use standard engine") { TestRestServer.withServer { val client = ClientBuilder.newBuilder.build try { val response = client .target("http://localhost:8080/test/get") .request .get() response.getStatus should be(Response.Status.OK.getStatusCode) response.readEntity(classOf[Book]) should be( Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104) ) response.close() } finally { client.close() } } }
java.net.HttpURLConnectionを使う
では、これをjava.net.HttpURLConnectionを使うように切り替えてみましょう。
こんな感じになります。
test("use java.net.HttpConnection engine") { TestRestServer.withServer { val client = new ResteasyClientBuilder().httpEngine(new URLConnectionEngine).build() try { val response = client .target("http://localhost:8080/test/get") .request .get() response.getStatus should be(Response.Status.OK.getStatusCode) response.readEntity(classOf[Book]) should be( Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104) ) response.close() } finally { client.close() } } }
ポイントは、ここだけです。
val client = new ResteasyClientBuilder().httpEngine(new URLConnectionEngine).build()
JAX-RSのClient.BuilderからResteasyClientBuilderに変え、httpEngineメソッドでURLConnectionEngineのインスタンスを設定します。
これだけで、RESTEasy Clientの内部で使われるHTTPクライアントの実装を切り替えることができます。
RESTEasy ClientのHTTPクライアントの実装を、OkHttp3に替えてみる
では、今度はRESTEasy Clientが使うHTTPクライアントの実装を、自前で用意したものに差し替えてみましょう。
今回のお題としては、OkHttp3を使うことにします。
先ほどのjava.net.HttpURLConnectionで差し替えた例である程度わかるかもしれませんが、ResteasyClientBuilder#httpEngineに渡すクラスを
作成すればOKです。
つまり、ClientHttpEngineインターフェースの実装クラスを作成します。
http://docs.jboss.org/resteasy/docs/3.1.4.Final/javadocs/org/jboss/resteasy/client/jaxrs/ClientHttpEngine.html
実装しなくてはいけないメソッドは、次の4つです。
- close
- getHostnameVerifier
- getSslContext
- invoke
参考にするのは、既存の実装でしょう。つまり、この2つ。
https://github.com/resteasy/Resteasy/blob/3.1.4.Final/resteasy-client/src/main/java/org/jboss/resteasy/client/jaxrs/engines/ApacheHttpClient4Engine.java
https://github.com/resteasy/Resteasy/blob/3.1.4.Final/resteasy-client/src/main/java/org/jboss/resteasy/client/jaxrs/engines/URLConnectionEngine.java
で、実装した結果はこちら。
src/main/scala/org/littlewings/javaee7/resteasy/OkHttpEngine.scala
package org.littlewings.javaee7.resteasy import java.io.{ByteArrayOutputStream, InputStream} import javax.net.ssl.{HostnameVerifier, SSLContext} import javax.ws.rs.core.MultivaluedMap import okhttp3.{MediaType, OkHttpClient, Request, RequestBody} import org.jboss.resteasy.client.jaxrs.ClientHttpEngine import org.jboss.resteasy.client.jaxrs.internal.{ClientInvocation, ClientResponse} import org.jboss.resteasy.util.CaseInsensitiveMap import scala.beans.BeanProperty class OkHttpEngine extends ClientHttpEngine { val okHttpClient: OkHttpClient = new OkHttpClient @BeanProperty var sslContext: SSLContext = _ @BeanProperty var hostnameVerifier: HostnameVerifier = _ override def invoke(request: ClientInvocation): ClientResponse = { val okHttpRequestBuilder = new Request.Builder() .url(request.getUri.toURL) val body = Option(request.getEntity).map { _ => val baos = new ByteArrayOutputStream request.getDelegatingOutputStream.setDelegate(baos) request.writeRequestBody(baos) baos.close() baos.toByteArray } val headers = request.getHeaders.asMap headers.entrySet.forEach(header => header.getValue.forEach(okHttpRequestBuilder.addHeader(header.getKey, _))) okHttpRequestBuilder.method( request.getMethod, body.map(RequestBody.create(MediaType.parse(request.getHeaders.getMediaType.toString), _)).getOrElse(null) ) val okHttpRequest = okHttpRequestBuilder.build val okHttpResponse = okHttpClient.newCall(okHttpRequest).execute() val response = new ClientResponse(request.getClientConfiguration) { var inputStream: InputStream = _ override def releaseConnection(): Unit = okHttpResponse.close() override def setInputStream(is: InputStream): Unit = inputStream = is override def getInputStream = { if (inputStream == null) { inputStream = okHttpResponse.body().byteStream() } inputStream } } response.setStatus(okHttpResponse.code()) val responseHeaders: MultivaluedMap[String, String] = new CaseInsensitiveMap val headerNames = okHttpResponse.headers.names headerNames.forEach(name => okHttpResponse.headers(name).forEach(value => responseHeaders.add(name, value))) response.setHeaders(responseHeaders) response } override def close(): Unit = () }
OkHttpClientは特にcloseするものがないので、実装が必要な次の3つは簡単にまとめました。
@BeanProperty var sslContext: SSLContext = _ @BeanProperty var hostnameVerifier: HostnameVerifier = _ override def close(): Unit = ()
あとは、invokeを実装するだけです。
invokeメソッドでは、RESTEasy側から渡されたClientInvocationを元にしって実際にHTTPリクエストを投げて、その結果を元に
レスポンスを作成して、RESTEasyに戻す処理を実装することになります。
OkHttp側へリクエストを設定。
override def invoke(request: ClientInvocation): ClientResponse = { val okHttpRequestBuilder = new Request.Builder() .url(request.getUri.toURL) val body = Option(request.getEntity).map { _ => val baos = new ByteArrayOutputStream request.getDelegatingOutputStream.setDelegate(baos) request.writeRequestBody(baos) baos.close() baos.toByteArray } val headers = request.getHeaders.asMap headers.entrySet.forEach(header => header.getValue.forEach(okHttpRequestBuilder.addHeader(header.getKey, _))) okHttpRequestBuilder.method( request.getMethod, body.map(RequestBody.create(MediaType.parse(request.getHeaders.getMediaType.toString), _)).getOrElse(null) ) val okHttpRequest = okHttpRequestBuilder.build val okHttpResponse = okHttpClient.newCall(okHttpRequest).execute()
レスポンスは、ClientResponseのサブクラスとして返却します。
val response = new ClientResponse(request.getClientConfiguration) { var inputStream: InputStream = _ override def releaseConnection(): Unit = okHttpResponse.close() override def setInputStream(is: InputStream): Unit = inputStream = is override def getInputStream = { if (inputStream == null) { inputStream = okHttpResponse.body().byteStream() } inputStream } }
また、このレスポンスにOkHttpのレスポンスが持つステータスコードやヘッダーなどを設定します。
response.setStatus(okHttpResponse.code()) val responseHeaders: MultivaluedMap[String, String] = new CaseInsensitiveMap val headerNames = okHttpResponse.headers.names headerNames.forEach(name => okHttpResponse.headers(name).forEach(value => responseHeaders.add(name, value))) response.setHeaders(responseHeaders) response }
これで完成です。
では、使ってみましょう。
GET
test("use OkHttp engine") { TestRestServer.withServer { val client = new ResteasyClientBuilder().httpEngine(new OkHttpEngine).build() try { val response = client .target("http://localhost:8080/test/get") .request .get() response.getStatus should be(Response.Status.OK.getStatusCode) response.readEntity(classOf[Book]) should be( Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104) ) response.close() } finally { client.close() } } }
POST
test("use OkHttp engine, post") { TestRestServer.withServer { val client = new ResteasyClientBuilder().httpEngine(new OkHttpEngine).build() try { val response = client .target("http://localhost:8080/test/post") .request .post(Entity.json(Book("978-4774183169", "パーフェクト Java EE", 3456))) response.getStatus should be(Response.Status.OK.getStatusCode) response.readEntity(classOf[Book]) should be( Book("978-4774183169", "パーフェクト Java EE", 6912) ) response.close() } finally { client.close() } } }
コードは省略しますが、PUT/DELETEでも特に問題なく。
まとめ
RESTEasy Clientで、内部的に使用するHTTPクライアントの実装の変更、自作をやってみました。
あんまり情報がないので、ClientHttpEngineを作るところは若干てこずりましたが、まあ最低限はこんなところかなぁと。とりあえず動かせたので
良しとしましょう。
今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/resteasy-customize-http-engine
あと、こちらも参考にするとよいかも?
https://github.com/tbroyer/jaxrs-utils/tree/master/resteasy-client-okhttp3