CLOVER🍀

That was when it all began.

Spring BootでWARファイルを作る

そういえば、やったことなかったなぁと思いまして、Spring Bootで実行可能JARファイルではなく、WARファイルを作ってTomcatなどにデプロイするための方法を試してみました。

で、試すにあたって、せっかくなので以下のコンセプトでやってみたいと思います。

  • パッケージングはWARとする
  • Spring MVC、JPAを使った小さなサンプルを動かす
  • IDEなどの上から、mainメソッド越しに起動したい
  • 単体テストも書いてみる

こんな感じで。

準備

Mavenの設定。最終的には、こんな感じになりました。
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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>spring-boot-war-testing</artifactId>
    <version>0.01-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <failOnMissingWebXml>false</failOnMissingWebXml>
        <spring.boot.version>1.2.6.RELEASE</spring.boot.version>
    </properties>

    <build>
        <finalName>ROOT</finalName>
        <!--
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        -->
    </build>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.36</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.2.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

MySQLのJDBCドライバが入っているのはまあいいとして(?)、AssertJが入っているのはテスト時の好みです。

WARを作るための設定は、こちらを参考に。

74.1 Create a deployable war file

パッケージングはwarにします。

    <packaging>war</packaging>

ファイル名は、とりあえずROOTになるようにしました。

        <finalName>ROOT</finalName>

web.xmlはなくてもいいので、ビルド時にエラーにならないようにします。

        <failOnMissingWebXml>false</failOnMissingWebXml>

「spring-boot-starter-tomcat」のスコープは、「provided」にしておきます。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

コメントアウトしていますが、「spring-boot-maven-plugin」はなくてもいいみたい…?

アプリケーションの実装

サンプル用として、簡単なSpring MVC+JPAアプリケーションを書いておきます。

RestController。
src/main/java/org/littlewings/spring/war/ContentsController.java

package org.littlewings.spring.war;

import java.util.List;
import javax.persistence.EntityManager;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Transactional
@RequestMapping("contents")
public class ContentsController {
    @Autowired
    private EntityManager entityManager;

    @RequestMapping(value = "registry", method = RequestMethod.POST)
    public void registry(@RequestBody List<Contents> contentsList) {
        contentsList.forEach(entityManager::persist);
    }

    @RequestMapping(value = "all", method = RequestMethod.GET)
    public List<Contents> all() {
        return entityManager.createQuery("SELECT c FROM Contents c", Contents.class).getResultList();
    }
}

最小化のため、EntityManagerを直接使っています。

JPAのEntity。
src/main/java/org/littlewings/spring/war/Contents.java

package org.littlewings.spring.war;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "contents")
public class Contents implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String value;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

Spring Bootとしてのエントリポイントは、以下のようになります。
src/main/java/org/littlewings/spring/war/App.java

package org.littlewings.spring.war;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

@SpringBootApplication
public class App extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(App.class);
    }
}

こちらにも書いてあるように、SpringBootServletInitializerクラスを継承しておく必要があります。

74.1 Create a deployable war file

設定ファイルは、こんな感じで用意。
src/main/resources/application.yml

spring:
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=utf-8&characterSetResults=utf-8&useServerPrepStmts=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false
    username: kazuhira
    password: password
    testOnBorrow: true
    validationQuery: SELECT 1
  jpa:
    hibernate.ddl-auto: none
    properties:
      hibernate:
        show_sql: true
        format_sql: true

実行

ところで、mainメソッドを持っているからといって、先ほど作成したAppクラスを直接起動するとエラーになります。

2015-09-27 23:43:57.348 ERROR 49890 --- [           main] o.s.boot.SpringApplication               : Application startup failed

java.lang.NoClassDefFoundError: javax/servlet/ServletContext
	at java.lang.Class.getDeclaredMethods0(Native Method)
	at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
	at java.lang.Class.getDeclaredMethods(Class.java:1975)

Tomcatをprovidedに追い出してしまいましたからね…。

で、どうするか

テスト側から起動すればよいのでは?と。
src/test/java/org/littlewings/spring/war/Launcher.java

package org.littlewings.spring.war;

public class Launcher {
    public static void main(String... args) {
        App.main(args);
    }
}

「spring-boot-starter-test」が依存関係にいるからか、これでも起動してくれます。

WARファイルを作る

WARファイルを作るには、普通に「mvn package」すればOKです。

$ mvn package

今回の設定の場合、ROOT.warができあがります。

[INFO] --- maven-war-plugin:2.2:war (default-war) @ spring-boot-war-testing ---
[INFO] Packaging webapp
[INFO] Assembling webapp [spring-boot-war-testing] in [/xxxxx/target/ROOT]
[INFO] Processing war project
[INFO] Webapp assembled in [76 msecs]
[INFO] Building war: /xxxxx/target/ROOT.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 10.324 s
[INFO] Finished at: 2015-09-27T23:46:34+09:00
[INFO] Final Memory: 27M/250M
[INFO] ------------------------------------------------------------------------

あとは、TomcatにでもデプロイすればOKです。

まあ、WebLogicにデプロイする場合や

74.4 Deploying a WAR to Weblogic

Servlet 2.5以前のコンテナにデプロイする場合は、また事情が違うようですが。

74.5 Deploying a WAR in an Old (Servlet 2.5) Container


WARとしてアプリケーションサーバーにデプロイする場合は、以下の設定からサーバーに関するものを分離して考えればいいのかな?

Appendix A. Common application properties

テストを書く

最後に、このアプリケーションのテストを書いてみます。

そういえば、Bootに限らず、Springアプリケーションのテストって書いたことがありません…。

とりあえず、このあたりを参考に。

43. Testing

書いてみたテストコード。
src/test/java/org/littlewings/spring/war/ContentsControllerTest.java

package org.littlewings.spring.war;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.client.RestTemplate;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = App.class)
@WebIntegrationTest("server.port=0")
public class ContentsControllerTest {
    private RestTemplate restTemplate = new TestRestTemplate();

    @Value("${local.server.port}")
    private int port;

    @Test
    public void testGetAll() {
        String body =
                restTemplate
                        .getForEntity("http://localhost:" + port + "/contents/all", String.class)
                        .getBody();

        assertThat(body)
                .isEqualTo("[{\"id\":1,\"value\":\"はじめてのSpring Boot\"},{\"id\":2,\"value\":\"高速スケーラブル検索エンジン ElasticSearch Server\"},{\"id\":3,\"value\":\"わかりやすいJava EE ウェブシステム入門\"},{\"id\":4,\"value\":\"[改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン\"},{\"id\":5,\"value\":\"Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer’s SELECTION)\"},{\"id\":6,\"value\":\"Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ\"}]");
    }
}

このあたりのアノテーションを付与すればよい、と。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = App.class)
@WebIntegrationTest("server.port=0")

使用するポートは、以下に沿って動的に指定する形に。

64.5 Discover the HTTP port at runtime

ポートの値は、@Valueで受け取れるみたいです。

    @Value("${local.server.port}")
    private int port;

このコードで、データベースの状態がこんな感じだと

mysql> SELECT * FROM contents;
+----+----------------------------------------------------------------------------------------------------+
| id | value                                                                                              |
+----+----------------------------------------------------------------------------------------------------+
|  1 | はじめてのSpring Boot                                                                              |
|  2 | 高速スケーラブル検索エンジン ElasticSearch Server                                                  |
|  3 | わかりやすいJava EE ウェブシステム入門                                                             |
|  4 | [改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン                                        |
|  5 | Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer’s SELECTION)               |
|  6 | Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ                                     |
+----+----------------------------------------------------------------------------------------------------+
6 rows in set (0.00 sec)

テストにパスします、と。

OKそうです。

今回、テストはTestRestTemplateを使ってみましたが、MockMvcというものを使う方法もあるようなので、こちらも後で見てみようと思います。

Spring-Boot の @RestController の単体テストを記述する