こちらの記事の続きです。
Mavenでマルチモジュール構成にする(Jersey RESTful)
https://qiita.com/kasa_le/items/db0d84e3e868ff14bc2b
目的
JserseyとSpring Framework(not Boot)でRESTfulAPIを実装する。
Java Spring
で調べるとSpring Bootの例ばかりで、多分Boot使ったほうがいろいろ楽なんでしょうが、大人の事情で使えない(かもしれない)ので、Spring Frameworkの方を使ったサンプルを目指します。
ゴール
Jersey+Spring Frameworkの最小構成のプログラムでHello World的なものが動く。
環境など
ツールなど | バージョンなど |
---|---|
MacbookPro | macOS Mojave 10.14.5 |
IntelliJ IDEA | Ultimate 2019.3.3 |
Java | AdoptOpenJDK 11 |
apache maven | 3.6.3 |
Jersey | 2.30.1 |
JUnit | 5.6.0 |
Tomcat | apache-tomcat-8.5.51 |
Postman | 7.19.1 |
Spring Framework | 5.2.4-RELEASE |
参考サイト
自分が作ったJerseyのプロジェクトや、Spring MVCのHelloWorldプロジェクトに追加しようとしたのですが、情報を探すとどうしてもSpringBootを使っているものが多く、なかなか難儀していました。
やっと見つけたのがこちらのサイトです。
Jersey + Spring integration example
https://mkyong.com/webservices/jax-rs/jersey-spring-integration-example/
十年近く前の情報で、かなり古いですが、上がっているプロジェクトは一応そのまま動きました。(※ただしJDK9以降の環境では、JAXB関連の依存関係の追加が必要)
なので、いったんそこから最新版に上げることを目指し、成功したので、その最終形態をメモしておきます。
プロジェクト設定手順
1.Mavenプロジェクトを新規作成
IntelliJ IDEAで新規にMavenプロジェクトを作成します。
手順はこちらなどを参考にしてください。
2.依存関係を設定
pom.xml
は次のようになりました。
参考サイトと変わっているのは、各バージョンが最新版になっていることと、それに伴いパッケージ移動したものを変更しています。また、JAXB関連がJDK9以降削除されているのでその依存関係も追加しています。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>my.example.jerseyspring</groupId>
<artifactId>RESTfulExample</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>RESTfulExample Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<!-- Jersey -->
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-server -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-binding</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- Spring dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Jersey + Spring -->
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.ext/jersey-spring5 -->
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-spring5</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- JAXBはJDK9から外されました -->
<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.activation/activation -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-runtime -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
<build>
<finalName>RESTfulExample</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<inherited>true</inherited>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<spring.version>5.2.4.RELEASE</spring.version>
<jersey.version>2.30.1</jersey.version>
<junit.jupiter.version>5.6.0</junit.jupiter.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
3.トランザクションクラスを作成
Beanでインジェクトするクラスとなります。
インターフェースを作ってImplementするのが作法みたいですが、そうなっていなくても問題はないです。Springの売りの1つがDI(依存成注入)なので、後でテストするときにモックすることなどを考えたら、最初からやっておいたほうがいいでしょうね。
インターフェースクラス
public interface TransactionBo {
String save();
}
実装クラス
public class TransactionBoImpl implements TransactionBo {
public String save() {
return "Jersey + Spring example";
}
}
4.サービスクラスを作成
TransactionBoImpl
をSpringにDIしてもらいますが、@Autowired
アノテーションは使いません。
コンストラクタインジェクションを使います。
(コンストラクタインジェクションが推奨のようです。詳しくは末尾の参考サイトから)
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import org.springframework.stereotype.Component;
import my.example.jerseyspring.transaction.TransactionBo;
@Component
@Path("/payment")
public class PaymentService {
final TransactionBo transactionBo;
public PaymentService(TransactionBo transactionBo) {
this.transactionBo = transactionBo;
}
@GET
@Path("/mkyong")
public Response savePayment() {
String result = transactionBo.save();
return Response.status(200).entity(result).build();
}
}
5.applicationContext.xml
Beanのクラスをインジェクトをしてもらうために設定するファイルです。
src/main/resources
フォルダ下に置きます。
パッケージ名とBeanのクラスは自分が作成したものに適宜変更して下さい。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="my.example.jerseyspring" />
<bean id="transactionBo" class="my.example.jerseyspring.transaction.impl.TransactionBoImpl" />
</beans>
5. web.xml
src/main/webapp/WEB-INF/
下にweb.xml
を作成して以下のようにします。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Restful Web Application</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>jersey-serlvet</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>my.example.jerseyspring</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jersey-serlvet</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
</web-app>
参考サイトと違うのは、<servlet-class>
がJerseyのクラスになっている点くらいです。
あとパッケージ名も自分が作成したものに変えてくださいね。
6.index.jsp
webapp
フォルダ下に置きます。
なくてもいいんですが、ないとTomcatにデプロイして起動したとき404ページが表示されて気持ち悪いので(^^;
<html>
<body>
<h2>Jersey + Spring RESTful Web Application!</h2>
<p><a href="rest/payment/mkyong">Jersey resource</a>
</body>
</html>
実行
Tomcatで実行設定をして起動すれば、次のような画面が表示されるはずです。
Jersey Resource
のリンクをクリックすると、rest/payment/mkyong
のGET
メソッドが叩かれ、戻り値が表示されます。
curlやPostmanでも成功するはずです。
テスト
せっかくなのでテストも書いてみます。
1.単純なテスト
(1)依存関係の追加
テスト用の依存関係を追加します。
この記事の時と同じにしておきました。
まず<build>/<plugins>
下にプラグインを追加します。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<additionalClasspathElements>
<additionalClasspathElement>src/test/java/</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.0.0-M4</version>
</plugin>
続いて、<dependencies>
に追加します。
<!-- テスト -->
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.test-framework.providers/jersey-test-framework-provider-grizzly2 -->
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>2.30.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.15.0</version>
<scope>test</scope>
</dependency>
(2)テストクラスの実装
基本的にはJerseyの基本サンプルでやったをそのまま踏襲するだけです。
package my.example.jerseyspring.rest;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Response;
import static org.assertj.core.api.Assertions.assertThat;
class PaymentServiceTest extends JerseyTest {
@Override
protected Application configure() {
return new ResourceConfig(PaymentService.class);
}
@BeforeEach
@Override
public void setUp() throws Exception {
super.setUp();
}
@AfterEach
@Override
public void tearDown() throws Exception {
super.tearDown();
}
@Test
public void get(){
final Response response = target("/payment/mkyong").request().get();
String content = response.readEntity(String.class);
assertThat(content).isEqualTo("Jersey + Spring example");
}
}
2.TransactionBoの実装を変えてみる
せっかくなのでSpringのDIを使ってモックに入れ替えられないか試してみました。
(1)依存関係の追加
SpringのDIを動かすために、SpringのTestフレームワークを入れます。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
(2)モックBeanクラスを作成
src/test/java/my/example/transaction/impl
下に、下記のクラスを作ります。
public class TransactionBoMock implements TransactionBo {
public String save() {
return "This is mock.";
}
}
これをBean定義しますが、テスト用なのでテスト用のapplicationContext.xml
を作ります。
src/test/resources
下に置きます。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="my.example.jerseyspring"/>
<bean id="transactionBo" class="my.example.jerseyspring.transaction.impl.TransactionBoMock"/>
</beans>
実装クラスをTransactionBoMock
に変更しています。
(3)テストクラスの修正
Junit5
に対応した書き方にしています。
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:/applicationContext.xml")
class PaymentServiceTest extends JerseyTest {
2つのアノテーションを追加するだけ!
実行すると、まだ比較文字列を変えていないので、エラーになると思います。
org.opentest4j.AssertionFailedError:
Expecting:
<"This is mock.">
to be equal to:
<"Jersey + Spring example">
but was not.
Expected :Jersey + Spring example
Actual :This is mock.
assertThat(content).isEqualTo("Jersey + Spring example");
上記の文字列を"This is mock."
に変えてやれば完了ですね。
感想
ここにたどり着くまでにかなり時間かかっていますが・・・
出来上がってみれば、割と単純です。
ここまでシンプルになると、DIしてもらうのにapplicationContext.xml
での定義が必要なんだなとか、そういうことが分かりやすくなりますね。
Spring MVCを入れるとなると、Controllerとかdispatcherとかの設定がまた大変そうですが、RESTfulAPIだけの予定なので、いったんはこれでよいでしょう。(あまりSpring MVCは勉強する気がなかったりw)
次は、もうちょっとちゃんとしたAPIを作って、できればJerseyのみでマルチモジュール化したプロジェクトとマージしていきたいですね。
ここまでのプロジェクトは、以下にアップしてあります。
その他の参考サイト
情報が古かったりBoot向けだったりで、直接の参考にはならなかったけど、細かいところでは色々とヒントを貰ったサイトです。
REST API with Jersey and Spring
https://www.baeldung.com/jersey-rest-api-with-spring
Programmers: Jersey with a Side of Spring
http://pilotprogrammer.com/archive/2019/01/programmers-jersey-with-a-side-of-spring/
SpringでField InjectionよりConstructor Injectionが推奨される理由
http://pppurple.hatenablog.com/entry/2016/12/29/233141
JUnitでテストするときにも、DIが動作するようにするには
https://wikiwiki.jp/webapp/Spring/JUnit