CLOVER🍀

That was when it all began.

RESTEasy Spring Boot Starterを試す

これは、なにをしたくて書いたもの?

そういう、単純な動機です。

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-RSAPIを足す感じで。

サンプルも、そうなっていました。

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はふつうに使えそうですね、と。

https://github.com/resteasy/resteasy-spring-boot/blob/2.0.1.Final/resteasy-spring-boot-starter/pom.xml#L90-L105

あと、他の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>

Maven Surefire Pluginも。

            <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クラスのサブクラスを検出するまで順次実行していく流れとなります。

https://github.com/resteasy/resteasy-spring-boot/blob/2.0.1.Final/resteasy-spring-boot-starter/src/main/java/org/jboss/resteasy/springboot/ResteasyEmbeddedServletInitializer.java#L81-L103

リソースクラスや、Providerは、アノテーションを使って検索します。

https://github.com/resteasy/resteasy-spring-boot/blob/2.0.1.Final/resteasy-spring-boot-starter/src/main/java/org/jboss/resteasy/springboot/ResteasyEmbeddedServletInitializer.java#L219-L220

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からは使えなくなったパラメーターや、その理由の説明も書かれています。

ソースコードも、それほど多くないのでざっと眺めてみてもよいでしょう。

https://github.com/resteasy/resteasy-spring-boot/tree/2.0.1.Final/resteasy-spring-boot-starter/src/main/java/org/jboss/resteasy/springboot

ServletContainerにデプロイされていることが前提となっているので、RESTEasy単体のようにNettyに載せたりするのは
難しそうですね。

https://github.com/resteasy/resteasy-spring-boot/blob/2.0.1.Final/resteasy-spring-boot-starter/src/main/java/org/jboss/resteasy/springboot/ResteasyAutoConfiguration.java

そういえばこのリポジトリ、Spring BootのStarter作成のルールに則った形(autoconfigureとstarterに分かれている)に
なっていないんですねぇ…。

Creating Your Own Starter

まとめ

RESTEasy Spring Boot Starterを、軽く試してみました。

Spring Boot、Springに載せつつ、でも世界観はRESTEasy(JAX-RS)な感じですね。

まあ、通常はSpring MVCとかSpring WebFluxとかを使うのでしょうが、ちょっと覚えておこうかな、と。