そういえば、やったことなかったなぁと思いまして、Spring Bootで実行可能JARファイルではなく、WARファイルを作ってTomcatなどにデプロイするための方法を試してみました。
で、試すにあたって、せっかくなので以下のコンセプトでやってみたいと思います。
こんな感じで。
準備
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 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としてアプリケーションサーバーにデプロイする場合は、以下の設定からサーバーに関するものを分離して考えればいいのかな?
テストを書く
最後に、このアプリケーションのテストを書いてみます。
そういえば、Bootに限らず、Springアプリケーションのテストって書いたことがありません…。
とりあえず、このあたりを参考に。
書いてみたテストコード。
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というものを使う方法もあるようなので、こちらも後で見てみようと思います。