LoginSignup
0
0

More than 3 years have passed since last update.

IntelliJ+Jersey+Spring FrameworkでRESTfulAPIのマルチモジュールサンプル

Last updated at Posted at 2020-04-08

以下の記事の続きです。

Jersey+Spring FrameworkでRESTfulAPIの最小構成サンプル
https://qiita.com/kasa_le/items/59ebd6b5490945dd5580

Mavenでマルチモジュール構成にする(Jersey RESTful)
https://qiita.com/kasa_le/items/db0d84e3e868ff14bc2b

今回は上の2つの記事で作成したプロジェクトをマージするような作業です。
が、一応ゼロからプロジェクトを作るていで説明します。

目的

JserseySpring 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。

pom.xml
<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に変わるはずです。
変わっていなければ手動で直してください。

root/pom.xml
    <groupId>my.example.jerseyspring</groupId>
    <artifactId>jersey-spring-restful</artifactId>
    <packaging>pom</packaging>

サブモジュールのそれぞれのpom.xmlは次のようにします。

repository/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>
serverapi/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>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に以下のクラスを作成します。

Employee.java
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.repositoryEmployeeRepositoryクラスを作成します。

EmployeeRepository.java
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クラス

EmployeeNotFoundException.java
package my.example.jerseyspring.repository.exceptions;

public class EmployeeNotFoundException extends RuntimeException {
    public EmployeeNotFoundException() {
        super("そのIDのEmployeeは見つかりません。");
    }
}

他のクラスも同じように作成します。

また、Jersey+Springの最小構成のプロジェクトで使ったtransacationパッケージも一応持ってきます。
repository.transactionパッケージで良いでしょう。

(4)Junitテストクラス

例外クラスのテストクラスを作成します。

例として、EmployeeNotFoundExceptionの場合を挙げておきます。

EmployeeNotFoundExceptionTest.java
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.restEmployeeServiceクラスを作成します。

EmployeeService.java
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クラスのマッパー

NotFoundExceptionHandler.java
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を作成し、以下のような内容にします。

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を作成して次の内容にする
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を作成して次のような内容にする
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クラスを作成してテストを書きます。

EmployeeServiceTest.java
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を置いて、次のようにします。

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の戻り値が表示されるはずです。

jersey-spring-restful.png

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0