そういえば、やったことなかったなぁと思いまして、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"
xmlnsxsi="http://www.w3.org/2001/XMLSchema-instance"
xsischemaLocation="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>
<projectbuildsourceEncoding>UTF-8</projectbuildsourceEncoding>
<javaversion>1.8</javaversion>
<mavencompilersource>1.8</mavencompilersource>
<mavencompilertarget>1.8</mavencompilertarget>
<failOnMissingWebXml>false</failOnMissingWebXml>
<springbootversion>1.2.6.RELEASE</springbootversion>
</properties>
<build>
<finalName>ROOT</finalName>
</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 の単体テストを記述する