これは、なにをしたくて書いたもの?
- RESTEasy Spring Boot Starterというリポジトリを見つけたので、試してみようかと
- そういえば、Spring BootにはJAX-RS and Jerseyがあったので、RESTEasyで1度試してみるのもいいかもと
そういう、単純な動機です。
RESTEasy Spring Boot Starter?
文字通り、RESTEasyをSpring Boot上で動かすためのStarterです。
ドキュメントにも、記載があります。
Spring Integration / Spring Boot starter
どうやらオリジナルはPayPalが作ったものらしく、それがRESTEasyの開発チームに寄贈されたもののようです。
This project has been kindly donated by PayPal. Please refer to https://github.com/paypal/resteasy-spring-boot for old versions.
PayPal側のリポジトリは、すでにRead Onlyになっています。
GitHub - paypal/resteasy-spring-boot: RESTEasy Spring Boot Starter
対応しているSpring Bootのバージョンは、2.0.x(RESTEasy Spring Boot Starter 2.0.x)と
1.5.x(RESTEasy Spring Boot Starter 1.0.x)となっているようです。
使い方は、ざっくりREADME.mdおよびUSAGE.mdを見ると、なんとなくわかります。
https://github.com/resteasy/resteasy-spring-boot/blob/master/README.md
https://github.com/resteasy/resteasy-spring-boot/blob/master/mds/USAGE.md
Applicationクラスのサブクラスや、JAX-RSリソースクラス、ProviderクラスをSpringのBeanとして定義すると、うまく
統合してくれそうな感じですね。
では、試してみるとしましょう。
環境
今回の環境は、こちらです。
$ java -version openjdk version "1.8.0_181" OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-0ubuntu0.18.04.1-b13) OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode) $ mvn -version Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T03:33:14+09:00) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.15.0-34-generic", arch: "amd64", family: "unix"
準備
Maven依存関係の定義から。
まずは、dependencyManagement。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.0.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Spring Bootのバージョンは、2.0.5.RELEASEとしました。
Spring Boot上でRESTEasyを使うための依存関係は、こちらになります。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>javax.ws.rs</groupId> <artifactId>javax.ws.rs-api</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-spring-boot-starter</artifactId> <version>2.0.1.Final</version> <scope>runtime</scope> </dependency>
RESTEasy Spring Boot Starterのドキュメントを読むと、scopeをruntimeで使って欲しそうな感じです。
あと、spring-boot-starter-webと、JAX-RSのAPIを足す感じで。
サンプルも、そうなっていました。
https://github.com/resteasy/resteasy-spring-boot/blob/2.0.1.Final/sample-app/pom.xml#L35-L59
Spring Boot用のMaven Plugin。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.0.5.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
準備は、こんなところで。
サンプルアプリケーションを作る
それでは、RESTEasy Spring Boot Starterを使った、簡単なアプリケーションを作成しましょう。
まずは、JAX-RSの有効化ということで、Applicationクラスのサブクラスを作成します。
src/main/java/org/littlewings/resteasy/spring/rest/JaxrsActivator.java
package org.littlewings.resteasy.spring.rest; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; import org.springframework.stereotype.Component; @Component @ApplicationPath("rest") public class JaxrsActivator extends Application { }
単純にJAX-RSとして使っていた時に比べると、@Componentアノテーションを付与するところが異なります。
リソースクラスも作成していってみましょう。
Stringと、JSONでのデータを返すリソースクラス。
src/main/java/org/littlewings/resteasy/spring/rest/HelloResource.java
package org.littlewings.resteasy.spring.rest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.springframework.stereotype.Component; @Component @Path("hello") public class HelloResource { @GET @Path("text") @Produces(MediaType.TEXT_PLAIN) public String text() { return "Hello RESTEasy!!"; } @GET @Path("json") @Produces(MediaType.APPLICATION_JSON) public Book json() { return Book.create( "978-4873114675", "JavaによるRESTfulシステム構築", 3456 ); } public static class Book { String isbn; String title; int price; public static Book create(String isbn, String title, int price) { Book book = new Book(); book.isbn = isbn; book.title = title; book.price = price; return book; } public String getIsbn() { return isbn; } public String getTitle() { return title; } public int getPrice() { return price; } } }
リソースクラス自体は、パッと見、@Componentアノテーションが付いている以外は、いたってふつうのJAX-RSリソースクラスです。
@Component @Path("hello") public class HelloResource {
RESTEasy Spring Boot Starterの依存関係を見ていると、RESTEasy関係の追加モジュールは、Spring向けのものと
Jackson2向けのものが入っているので、JSONはふつうに使えそうですね、と。
あと、他のBeanをインジェクションするリソースクラスも作成してみましょう。
まずは、依存先のクラスを作成。
src/main/java/org/littlewings/resteasy/spring/service/MessageService.java
package org.littlewings.resteasy.spring.service; import org.springframework.stereotype.Component; @Component public class MessageService { public String get() { return "Hello RESTEasy with Spring Boot!!"; } }
で、このServiceクラスを使うリソースクラスを作成。
src/main/java/org/littlewings/resteasy/spring/rest/MessageResource.java
package org.littlewings.resteasy.spring.rest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.littlewings.resteasy.spring.service.MessageService; import org.springframework.stereotype.Component; @Path("message") @Component public class MessageResource { MessageService messageService; public MessageResource(MessageService messageService) { this.messageService = messageService; } @GET @Produces(MediaType.TEXT_PLAIN) public String get() { return messageService.get(); } }
最後に、アプリケーションの起動クラスを作成します。
src/main/java/org/littlewings/resteasy/spring/App.java
package org.littlewings.resteasy.spring; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
こんなところでしょう。
確認
それでは、動作確認してみましょう。
パッケージングと起動。
$ mvn package $ java -jar target/resteasy-spring-boot-example-0.0.1-SNAPSHOT.jar
確認。
## Text $ curl -i localhost:8080/rest/hello/text HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 16 Date: Sat, 29 Sep 2018 12:17:57 GMT Hello RESTEasy!! ## JSON $ curl -i localhost:8080/rest/hello/json HTTP/1.1 200 Content-Type: application/json Content-Length: 87 Date: Sat, 29 Sep 2018 12:18:00 GMT {"isbn":"978-4873114675","title":"JavaによるRESTfulシステム構築","price":3456} ## with Injection $ curl -i localhost:8080/rest/message HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 33 Date: Sat, 29 Sep 2018 12:18:36 GMT Hello RESTEasy with Spring Boot!!
どれもOKそうですね。
ところで、JSONでの出力がPretty Printされていないので、目視ではちょっと読みづらいです。
Spring Bootを使っている場合、通常は以下のプロパティで設定を行います。
spring.jackson.serialization.indent_output = true
ですが、これでは効果がありません。
{"isbn":"978-4873114675","title":"JavaによるRESTfulシステム構築","price":3456}
Spring Boot上のRESTEasyでこれを行う場合、ObjectMapperのProviderを作成することになります。
src/main/java/org/littlewings/resteasy/spring/rest/ObjectMapperProvider.java
package org.littlewings.resteasy.spring.rest; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.stereotype.Component; @Provider @Component public class ObjectMapperProvider implements ContextResolver<ObjectMapper> { ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); @Override public ObjectMapper getContext(Class<?> type) { return objectMapper; } }
このProviderも、@Componentアノテーション付きのBeanとして定義します。
こうすると、Providerで提供したObjectMapperが使われるようになります。
$ curl -i localhost:8080/rest/hello/json HTTP/1.1 200 Content-Type: application/json Content-Length: 103 Date: Sat, 29 Sep 2018 12:23:35 GMT { "isbn" : "978-4873114675", "title" : "JavaによるRESTfulシステム構築", "price" : 3456 }
全体的に、SpringのBean定義に乗っていく感じですね。
テストを書く
最後に、テストコードを書いて確認してみましょう。
WebTestClientを使って、テストコードを書くことにします。Maven依存関係に、以下を追加。JUnit 5も使いましょう。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>junit</groupId> <artifactId>junit</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <scope>test</scope> </dependency>
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.19.1</version> <dependencies> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-surefire-provider</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.1.1</version> </dependency> </dependencies> </plugin>
作成した、テストコードはこちら。
src/test/java/org/littlewings/resteasy/spring/RestEasySpringTest.java
package org.littlewings.resteasy.spring; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.littlewings.resteasy.spring.rest.HelloResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.reactive.server.WebTestClient; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class RestEasySpringTest { @Autowired WebTestClient client; @Test public void text() { client .get() .uri("/rest/hello/text") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello RESTEasy!!"); } @Test public void json() { client .get() .uri("/rest/hello/json") .accept(MediaType.APPLICATION_JSON_UTF8) .exchange() .expectStatus() .isOk() .expectBody(HelloResource.Book.class) .consumeWith(response -> { HelloResource.Book book = response.getResponseBody(); assertThat(book.getIsbn()).isEqualTo("978-4873114675"); assertThat(book.getTitle()).isEqualTo("JavaによるRESTfulシステム構築"); assertThat(book.getPrice()).isEqualTo(3456); }); } @Test public void withInject() { client .get() .uri("/rest/message") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello RESTEasy with Spring Boot!!"); } }
Spring MVCもSpring WebFluxも使っていないからか、SpringBootTest.WebEnvironment.MOCKではどうにもうまくいかなかったので
SpringBootTest.WebEnvironment.RANDOM_PORTで確認することにしました。
設定などなど
基本的な使い方は、こんな感じでJAX-RSのリソースクラスやProvider、ApplicationクラスのサブクラスをSpringのBeanとして
定義して使っていく感じになります。
ですが、こちらを見ていると、設定自体はもう少しありそうなので見ていきましょう。
https://github.com/resteasy/resteasy-spring-boot/blob/master/mds/USAGE.md
JAX-RSでのApplicationクラスのサブクラスの検出方法を、プロパティで指定する方法が記載されています。
Advanced topics / JAX-RS application registration methods
Applicationクラスのサブクラスを「resteasy.jaxrs.app.classes」で「,」区切りで定義したり、「resteasy.jaxrs.app.registration」で
指定する検出方法で探索することができます。
「resteasy.jaxrs.app.registration」はデフォルトは「auto」で、「bean」、「property」、「scanning」と同義の内容を、
Applicationクラスのサブクラスを検出するまで順次実行していく流れとなります。
リソースクラスや、Providerは、アノテーションを使って検索します。
Applicationクラスのサブクラスは、ない場合は自動的に「/*」にマッピングするようですが
If after that still no JAX-RS application class could be registered, then a default one will be automatically created mapping to /* (according to section 2.3.2 in the JAX-RS 2.0 specification).
RESTEasy Spring Boot Starterとしては、少なくともひとつはApplicationクラスのサブクラスを作るべきだし、
またJAX-RSのベースとなるパスを「/」にマッピングすることは推奨していないようです。
It is recommended to always have at least one JAX-RS application class.
Avoid setting the JAX-RS application base URI to simply / to prevent URI conflicts, as explained in item 1
その他、ContextParamやセキュリティ関連などの、RESTEasy自体に関する設定方法は、こちら。
Advanced topics / RESTEasy configuration
RESTEasyからは使えなくなったパラメーターや、その理由の説明も書かれています。
ソースコードも、それほど多くないのでざっと眺めてみてもよいでしょう。
ServletContainerにデプロイされていることが前提となっているので、RESTEasy単体のようにNettyに載せたりするのは
難しそうですね。
そういえばこのリポジトリ、Spring BootのStarter作成のルールに則った形(autoconfigureとstarterに分かれている)に
なっていないんですねぇ…。
まとめ
RESTEasy Spring Boot Starterを、軽く試してみました。
Spring Boot、Springに載せつつ、でも世界観はRESTEasy(JAX-RS)な感じですね。
まあ、通常はSpring MVCとかSpring WebFluxとかを使うのでしょうが、ちょっと覚えておこうかな、と。