以下の記事の続きです。
Jersey+Spring FrameworkでRESTfulAPIの最小構成サンプル
https://qiita.com/kasa_le/items/59ebd6b5490945dd5580
Mavenでマルチモジュール構成にする(Jersey RESTful)
https://qiita.com/kasa_le/items/db0d84e3e868ff14bc2b
今回は上の2つの記事で作成したプロジェクトをマージするような作業です。
が、一応ゼロからプロジェクトを作るていで説明します。
目的
JserseyとSpring Framework(not Boot)でRESTfulAPIを実装する。
ゴール
- Jersey+Spring FrameworkでRESTfulAPIが一通り(CRUD)が動く。
- マルチモジュール構成でDIが出来る。
環境など
ツールなど | バージョンなど |
---|---|
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 |
プロジェクト設定
IntelliJで、Mavenプロジェクト
を新規作成します。
1.ルートpom
基本的に、依存関係やプラグインの設定は、Jersey+Spring Frameworkの最小構成プロジェクトのものをコピペすればOK。
<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>jersey-spring-restful</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>Jersey and Spring Framework RESTfulAPI Sample</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<inherited>true</inherited>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<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>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey</groupId>
<artifactId>jersey-bom</artifactId>
<version>${jersey.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Jersey -->
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-server -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-binding</artifactId>
</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>
<!-- テスト -->
<!-- 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>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<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>
2.サブモジュールの作成
(1)ルートのsrc
フォルダを削除
手動で削除します。
(2)サブモジュールの追加
IntelliJのメニュー[File]-[New]-[Module...]から、Mavenプロジェクトで以下の2つを追加します。
- repository
- serverapi
サブモジュールの名称は任意です。Parentの設定を忘れないでください。(ルートのプロジェクトを選んでくださいね。)
そうすると、親pom.xml
の<packaging>
がpom
に変わるはずです。
変わっていなければ手動で直してください。
<groupId>my.example.jerseyspring</groupId>
<artifactId>jersey-spring-restful</artifactId>
<packaging>pom</packaging>
サブモジュールのそれぞれのpom.xml
は次のようにします。
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jersey-spring-restful</artifactId>
<groupId>my.example.jerseyspring</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>repository</artifactId>
<packaging>jar</packaging>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jersey-spring-restful</artifactId>
<groupId>my.example.jerseyspring</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>serverapi</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>my.example.jerseyspring</groupId>
<artifactId>repository</artifactId>
<version>${project.version}</version>
</dependency>
<!-- for spring test-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
serverapiモジュールは、Spring TestがJunitテストで必要なので<test>
スコープで入れています。
ただ、他のサブモジュールでも必要な場合は、ルートのpom.xml
に入れてしまって良いでしょう。
なお、どちらも、自分のバージョンは指定していません。親のバージョンをそのまま使いたいので。
サブモジュールごとにバージョン管理をしない方針です。する必要がある場合は、<version>
タグでそれぞれにバージョン指定をしてください。
(3)フォルダ構成
現在のフォルダ構成はこうなっているはずです。
$ tree
.
├── SpringJerseyRest.iml
├── pom.xml
├── repository
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ └── java
└── serverapi
├── pom.xml
└── src
├── main
│ ├── java
│ └── resources
└── test
└── java
2.repositoryモジュールの設定
(1)モデルクラス
パッケージmy.example.jerseyspring.repository.models
に以下のクラスを作成します。
package my.example.jerseyspring.repository.models;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Employee {
private int id;
private String firstName;
public Employee() {
}
public Employee(int id, String firstName) {
this.id = id;
this.firstName = firstName;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
(2)リポジトリクラス
パッケージmy.example.jerseyspring.repository
にEmployeeRepository
クラスを作成します。
package my.example.jerseyspring.repository;
import my.example.jerseyspring.repository.exceptions.DuplicateIdException;
import my.example.jerseyspring.repository.exceptions.EmployeeNameNotFoundException;
import my.example.jerseyspring.repository.exceptions.EmployeeNotFoundException;
import my.example.jerseyspring.repository.models.Employee;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class EmployeeRepository {
private List<Employee> employeeList;
public EmployeeRepository() {
employeeList = new ArrayList<>();
employeeList.add(new Employee(3, "Cupcake"));
employeeList.add(new Employee(4, "Donuts"));
employeeList.add(new Employee(5, "Eclair"));
employeeList.add(new Employee(8, "Froyo"));
employeeList.add(new Employee(9, "Gingerbread"));
}
public List<Employee> selectAll() {
return employeeList;
}
public Employee select(int id) {
for (Employee employee : employeeList) {
if (employee.getId() == id) {
return employee;
}
}
throw new EmployeeNotFoundException();
}
public synchronized void insert(int id, String firstName) {
try {
select(id);
} catch (EmployeeNotFoundException e) {
// いなければ追加できる
employeeList.add(new Employee(id, firstName));
return;
}
// 同じIDが存在したら追加できない
throw new DuplicateIdException();
}
public synchronized void update(int id, String firstName) {
Employee employee = select(id);
employee.setFirstName(firstName);
}
public synchronized void delete(int id) {
Employee employee = select(id);
employeeList.remove(employee);
}
public List<Employee> search(String name) {
List<Employee> list = new ArrayList<>();
for (Employee employee : employeeList) {
if (employee.getFirstName().contains(name)) {
list.add(employee);
}
}
if (list.size() > 0) return list;
throw new EmployeeNameNotFoundException(name);
}
}
以前のマルチモジュールのサンプルでは、シングルトン実装を自前で入れていましたが、Spring FrameworkのDIは基本的にシングルトンになるということで、その実装を削除しています。
(3)例外クラス
EmployeeRepository
クラスで使っている例外クラスをそれぞれ作成します。
パッケージはrepository.exceptions
としました。
例)EmployeeNotFoundException
クラス
package my.example.jerseyspring.repository.exceptions;
public class EmployeeNotFoundException extends RuntimeException {
public EmployeeNotFoundException() {
super("そのIDのEmployeeは見つかりません。");
}
}
他のクラスも同じように作成します。
また、Jersey+Springの最小構成のプロジェクトで使ったtransacation
パッケージも一応持ってきます。
repository.transaction
パッケージで良いでしょう。
(4)Junitテストクラス
例外クラスのテストクラスを作成します。
例として、EmployeeNotFoundException
の場合を挙げておきます。
package my.example.jerseyspring.repository.exceptions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class EmployeeNotFoundExceptionTest {
@Test
void getMessage() {
EmployeeNotFoundException e = new EmployeeNotFoundException();
assertThat(e.getMessage()).isEqualTo("そのIDのEmployeeは見つかりません。");
}
}
IntelliJ上から、Junitテストの実行して通過を確認しておきましょう。
(5)最終構成
最終的に、次のようなフォルダ構成になっているはずです。
$ tree
.
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── my
│ │ └── example
│ │ └── jerseyspring
│ │ └── repository
│ │ ├── EmployeeRepository.java
│ │ ├── exceptions
│ │ │ ├── DuplicateIdException.java
│ │ │ ├── EmployeeNameNotFoundException.java
│ │ │ └── EmployeeNotFoundException.java
│ │ ├── models
│ │ │ └── Employee.java
│ │ └── transaction
│ │ ├── TransactionBo.java
│ │ └── impl
│ │ └── TransactionBoImpl.java
│ └── resources
└── test
└── java
└── my
└── example
└── jerseyspring
└── repository
└── exceptions
├── DuplicateIdExceptionTest.java
├── EmployeeNameNotFoundExceptionTest.java
└── EmployeeNotFoundExceptionTest.java
19 directories, 11 files
3.serverapiモジュールの設定
(1)サービスクラス
パッケージmy.example.jerseyspring.rest
にEmployeeService
クラスを作成します。
package my.example.jerseyspring.rest;
import my.example.jerseyspring.repository.EmployeeRepository;
import my.example.jerseyspring.repository.models.Employee;
import org.springframework.stereotype.Service;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.util.List;
@Service
@Path("/employees")
public class EmployeeService {
final EmployeeRepository employeeRepository;
public EmployeeService(EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}
@Context
UriInfo uriInfo;
@GET
@Path("/all")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public List<Employee> getAll() {
return employeeRepository.selectAll();
}
@GET
@Path("/{id}")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Employee getEmployee(@PathParam("id") int id) {
return employeeRepository.select(id);
}
@GET
@Path("/search")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public List<Employee> searchEmployee(@QueryParam("name") String name) {
return employeeRepository.search(name);
}
@POST
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response addEmployee(Employee employee) {
employeeRepository.insert(employee.getId(), employee.getFirstName());
UriBuilder builder = uriInfo.getAbsolutePathBuilder();
builder.path(String.valueOf(employee.getId()));
return Response.created(builder.build()).build();
}
@PUT
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response updateEmployee(Employee employee) {
employeeRepository.update(employee.getId(), employee.getFirstName());
// 新規作成した場合はcreatedを返す必要があるが、このサンプルではエラーとするため、常にokを返す
return Response.ok().build();
}
@DELETE
@Path("/{id}")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response deleteEmployee(@PathParam("id") int id) {
employeeRepository.delete(id);
// Entityの状態を返す場合はokを返す。
// 受け付けたが処理が終わっていない場合は(キューに乗っただけなど)acceptedを返す
// このサンプルでは削除が完了して該当コンテントがなくなったことだけ返す
return Response.noContent().build();
}
}
また、Jersey+Springの最小構成のプロジェクトで使ったPaymentService
クラスも一応持ってきます。
(2)例外ハンドラクラス
例外マッピング用のクラスをrest.handlers
パッケージに作ります。
repositoryモジュールで作った例外クラスごとに必要です。
例外のマッピングについては、こちらをご参照ください。
例) EmployeeNotFoundException
クラスのマッパー
package my.example.jerseyspring.rest.handler;
import my.example.jerseyspring.repository.exceptions.EmployeeNotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class NotFoundExceptionHandler implements ExceptionMapper<EmployeeNotFoundException> {
public Response toResponse(EmployeeNotFoundException ex) {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
(3)Beanの設定
src/main/resources
下に、applicationContext.xml
を作成し、以下のような内容にします。
<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.repository.transaction.impl.TransactionBoImpl"/>
<bean id="employeeRepository" class="my.example.jerseyspring.repository.EmployeeRepository"/>
</beans>
(4)webapp
フォルダの設定
src/main/webapp
フォルダを作ります。
-
webapp/WEB-INF
下にweb.xml
を作成して次の内容にする
<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container,
see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<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-servlet</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-servlet</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
</web-app>
-
webapp/
下にindex.jsp
を作成して次のような内容にする
<html>
<body>
<h2>Jersey + Spring RESTful Web Application!</h2>
<p><a href="rest/payment/mkyong">Jersey resource</a>
<br>
<p><a href="rest/employees/all">All Employee List</a>
<p><a href="rest/employees/3">get id=3 employee</a>
</body>
</html>
(5)Junitテストクラス
EmployeeServiceTest
クラス、PaymentServiceTest
クラスを作成してテストを書きます。
package my.example.jerseyspring.rest;
import my.example.jerseyspring.repository.EmployeeRepository;
import my.example.jerseyspring.repository.models.Employee;
import my.example.jerseyspring.rest.handler.DuplicateExceptionHandler;
import my.example.jerseyspring.rest.handler.NameNotFoundExceptionHandler;
import my.example.jerseyspring.rest.handler.NotFoundExceptionHandler;
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 org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:/applicationContext.xml")
public class EmployeeServiceTest extends JerseyTest {
@Autowired
EmployeeRepository employeeRepository;
@Override
protected Application configure() {
return new ResourceConfig(EmployeeService.class)
.register(DuplicateExceptionHandler.class)
.register(NameNotFoundExceptionHandler.class)
.register(NotFoundExceptionHandler.class)
.register(this);
}
@BeforeEach
@Override
public void setUp() throws Exception {
super.setUp();
}
@AfterEach
@Override
public void tearDown() throws Exception {
super.tearDown();
}
@ParameterizedTest
@ValueSource(strings = {MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public void getAll(String mediaType) {
final Response response = target("/employees/all").request().accept(mediaType).get();
assertThat(response.getHeaderString("Content-Type"))
.isEqualTo(mediaType);
List<Employee> content = response.readEntity(new GenericType<>() {
});
assertThat(content.size()).isEqualTo(5);
assertThat(content.get(0)).isEqualToComparingFieldByField(new Employee(3, "Cupcake"));
assertThat(content.get(1)).isEqualToComparingFieldByField(new Employee(4, "Donuts"));
assertThat(content.get(2)).isEqualToComparingFieldByField(new Employee(5, "Eclair"));
assertThat(content.get(3)).isEqualToComparingFieldByField(new Employee(8, "Froyo"));
assertThat(content.get(4)).isEqualToComparingFieldByField(new Employee(9, "Gingerbread"));
}
@ParameterizedTest
@MethodSource("getParamProvider")
public void getEmployee(int id, String mediaType) {
String urlPath = String.format("/employees/%d", id);
final Response response = target(urlPath).request().accept(mediaType).get();
assertThat(response.getHeaderString("Content-Type"))
.isEqualTo(mediaType);
Employee employee = response.readEntity(Employee.class);
Employee expect = employeeRepository.select(id);
assertThat(employee).isEqualToComparingFieldByField(expect);
}
static Stream<Arguments> getParamProvider() {
return Stream.of(
Arguments.of(3, MediaType.APPLICATION_JSON),
Arguments.of(4, MediaType.APPLICATION_JSON),
Arguments.of(5, MediaType.APPLICATION_JSON),
Arguments.of(8, MediaType.APPLICATION_JSON),
Arguments.of(9, MediaType.APPLICATION_JSON),
Arguments.of(3, MediaType.APPLICATION_XML),
Arguments.of(4, MediaType.APPLICATION_XML),
Arguments.of(5, MediaType.APPLICATION_XML),
Arguments.of(8, MediaType.APPLICATION_XML),
Arguments.of(9, MediaType.APPLICATION_XML)
);
}
@ParameterizedTest
@ValueSource(strings = {MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public void searchEmployee(String mediaType) {
final Response response = target("/employees/search")
.queryParam("name", "a")
.request()
.accept(mediaType)
.get();
assertThat(response.getHeaderString("Content-Type"))
.isEqualTo(mediaType);
List<Employee> content = response.readEntity(new GenericType<>() {
});
assertThat(content.size()).isEqualTo(3);
assertThat(content.get(0)).isEqualToComparingFieldByField(new Employee(3, "Cupcake"));
assertThat(content.get(1)).isEqualToComparingFieldByField(new Employee(5, "Eclair"));
assertThat(content.get(2)).isEqualToComparingFieldByField(new Employee(9, "Gingerbread"));
}
@ParameterizedTest
@MethodSource("postRawProvider")
public void addEmployee(int id, String bodyRaw, String mediaType) {
final Response response = target("/employees").request()
.post(Entity.entity(bodyRaw, mediaType));
assertThat(response.getStatus()).isEqualTo(201);
assertThat(response.getHeaderString("Location"))
.isEqualTo("http://localhost:9998/employees/" + id);
}
static Stream<Arguments> postRawProvider() {
final String json = "{\"firstName\":\"Honeycomb\",\"id\":11}";
final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<employee><firstName>KitKat</firstName><id>19</id></employee>";
return Stream.of(
Arguments.of(11, json, MediaType.APPLICATION_JSON),
Arguments.of(19, xml, MediaType.APPLICATION_XML)
);
}
@ParameterizedTest
@MethodSource("putRawProvider")
public void updateEmployee(int id, String bodyRaw, String mediaType) {
final Response response = target("/employees").request()
.put(Entity.entity(bodyRaw, mediaType));
assertThat(response.getStatus()).isEqualTo(200);
Employee employee = target("/employees/" + id).request().get(Employee.class);
Employee expected = employeeRepository.select(id);
assertThat(employee).isEqualToComparingFieldByField(expected);
}
static Stream<Arguments> putRawProvider() {
final String json = "{\"firstName\":\"Frozen yogurt\",\"id\":8}";
final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<employee><firstName>Cup Cake</firstName><id>3</id></employee>";
return Stream.of(
Arguments.of(8, json, MediaType.APPLICATION_JSON),
Arguments.of(3, xml, MediaType.APPLICATION_XML)
);
}
@Test
public void deleteEmployee() {
final Response response = target("/employees/9")
.request().delete();
assertThat(response.getStatus()).isEqualTo(204);
}
@Test
public void exception_selectEmployee() {
final Response response = target("/employees/1").request().get();
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
public void exception_searchEmployee() {
final Response response = target("/employees/search?name=android").request().get();
assertThat(response.getStatus()).isEqualTo(404);
}
@ParameterizedTest
@MethodSource("putRawProvider")
public void exception_addEmployee(int id, String bodyRaw, String mediaType) {
final Response response = target("/employees").request()
.post(Entity.entity(bodyRaw, mediaType));
assertThat(response.getStatus()).isEqualTo(409);
}
@ParameterizedTest
@MethodSource("putExceptionProvider")
public void exception_updateEmployee(int id, String bodyRaw, String mediaType) {
final Response response = target("/employees").request()
.put(Entity.entity(bodyRaw, mediaType));
assertThat(response.getStatus()).isEqualTo(404);
}
static Stream<Arguments> putExceptionProvider() {
final String json = "{\"firstName\":\"Lollipop\",\"id\":21}";
final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<employee><firstName>Jelly Bean</firstName><id>17</id></employee>";
return Stream.of(
Arguments.of(21, json, MediaType.APPLICATION_JSON),
Arguments.of(3, xml, MediaType.APPLICATION_XML)
);
}
@Test
public void exception_deleteEmployee() {
final Response response = target("/employees/1").request().get();
assertThat(response.getStatus()).isEqualTo(404);
}
}
マルチモジュールのサンプルでEmployeeRepository.getInstance
としてシングトンオブジェクトを取得していた部分は、 @Autowired
で宣言したメンバー変数へのアクセスと変更しています。
@Autowired
アノテーションにより、SpringがDIしてくれますが、実はそれだけだと、サービスで実際に動く方とテストの方で別のインスタンスが作られてしまうらしく(シングルトンのはずだけど、おそらくテストのコンテキストがアプリケーションコンテキストとは違うせいかと)、ResourceConfig#register(this)
をすれば大丈夫という記事を見かけてそのようにしています。
ただ、これで動いてしまうのはバグではないか?だって警告文が出ているよ?という意見もあるので、正しいのかどうかわかりません。
https://stackoverflow.com/questions/34453448/how-to-access-spring-bean-from-jerseytest-subclass
今回は、そもそもリポジトリクラスは本題ではなくて仮のデータアクセスを提供しているに過ぎないので、いったんはこれで良しとしています。ただ、SpringまたはJerseyのバージョンが上がると、動かなくなる可能性はあるので要注意です。
PaymentServiceTest
クラスも、最小構成のサンプルから持ってきます。
また、DIのモック差し替えテスト用に作ったTransactionBoMock
クラスも持ってきておきます。
そして、Beanを差し替えられるように、test
フォルダのresources
フォルダにもapplicationContext.xml
を置いて、次のようにします。
<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.repository.transaction.impl.TransactionBoMock"/>
<bean id="employeeRepository" class="my.example.jerseyspring.repository.EmployeeRepository"/>
</beans>
EmployeeRepositoryはインターフェースも作ってないので今回特に差し替えませんが、本来であれば、DBなどからデータを取得する本来の実装に対し、テスト用にMockしたクラスを使うべきでしょうね。
(6)最終構成
serverapi
フォルダの構成は次のようになっているはずです。
.
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── my
│ │ └── example
│ │ └── jerseyspring
│ │ └── rest
│ │ ├── EmployeeService.java
│ │ ├── PaymentService.java
│ │ └── handler
│ │ ├── DuplicateExceptionHandler.java
│ │ ├── NameNotFoundExceptionHandler.java
│ │ └── NotFoundExceptionHandler.java
│ ├── resources
│ │ └── applicationContext.xml
│ └── webapp
│ ├── WEB-INF
│ │ └── web.xml
│ └── index.jsp
└── test
├── java
│ └── my
│ └── example
│ └── jerseyspring
│ ├── repository
│ │ └── transaction
│ │ └── impl
│ │ └── TransactionBoMock.java
│ └── rest
│ ├── EmployeeServiceTest.java
│ └── PaymentServiceTest.java
└── resources
└── applicationContext.xml
21 directories, 13 files
4.実行、テスト
(1)JUnitTest
JUnitTestを実行して、すべてのテストが通過するか確認します。
$ mvn clean test
(2)動作確認
Tomcatを設定してデプロイします。
以下のようなページが表示され、リンクをクリックするとAPIの戻り値が表示されるはずです。
POST系や、Json/Xmlの入出力を確認したい場合は、CurlやPostmanなどを使って確認してみてください。
ここまでのプロジェクトは、以下にアップしてあります。
https://github.com/le-kamba/spring-jersey-sample/tree/spring_jersey
感想
シングルトンの箇所以外ではハマることなくすんなり出来ました。
ちなみに、フォルダ構成は、macにbrewでtreeコマンドを入れて出しました。
そもそもHomebrewのインストールからやらなきゃならなかったですが。(入れてなかったっけ?)
# Homebrewのインストール
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
# treeのインストール
$ brew install tree
参考
SpringのDIとnewってなにがちがうんだっけ?
https://qiita.com/uqichi/items/5f59817beb3dff9c0c1e