CLOVER🍀

That was when it all began.

Spring BootのUber JARで、アプリケーションコードとJARを作成するプロジェクトを別々にする(オマケでrequiresUnpack)

これは、なにをしたくて書いたもの?

ふと、これをやってもふつうに動くのかな?と思ったので。

いや、動いたんですけど。

Spring BootでSpring Web MVCを使って作成したMavenプロジェクトと、それをUber JARにパッケージングするMavenプロジェクトを
別々にして、マルチモジュールプロジェクトにして試してみます。

いや、ふつうやらない気はしますけど。

環境

今回の環境は、こちら。

$ java -version
openjdk version "11.0.4" 2019-07-16
OpenJDK Runtime Environment (build 11.0.4+11-post-Ubuntu-1ubuntu218.04.3)
OpenJDK 64-Bit Server VM (build 11.0.4+11-post-Ubuntu-1ubuntu218.04.3, mixed mode, sharing)


$ mvn -version
Apache Maven 3.6.2 (40f52333136460af0dc0d7232c0dc0bcf0d9e117; 2019-08-28T00:06:16+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.4, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-65-generic", arch: "amd64", family: "unix"

準備

マルチモジュール構成の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>unpack-jar</artifactId>
    <packaging>pom</packaging>
    <version>0.0.1</version>
    <modules>
        <module>app</module>
        <module>launcher</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
</project>

この配下に、appとlauncherという2つのMavenプロジェクトを作成していきます。

アプリケーションコード側

Spring Web MVCとFreeMarkerで作成することにしましょう。

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>unpack-jar</artifactId>
        <groupId>org.littlewings</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>app</artifactId>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.9.RELEASE</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-freemarker</artifactId>
        </dependency>
    </dependencies>
</project>

Controller。
app/src/main/java/org/littlewings/spring/boot/App.java

package org.littlewings.spring.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@SpringBootApplication
@Controller
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("message", "World");

        return "hello";
    }
}

FreeMarkerテンプレート。
app/src/main/resources/templates/hello.ftl

Hello ${message?html}!!

この中身なら、HTMLエスケープ要らなかった…。

静的ファイル。
app/src/main/resources/static/index.html

Hello FreeMarker

HTMLじゃないですけど。

IDEとかからmainメソッドを持つクラスを起動する分には、これでもふつうに使えます。

Uber JARを作成する

もうひとつのMavenプロジェクト側では、アプリケーションコード側を依存関係に加えて、Uber JARを作成します。

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>unpack-jar</artifactId>
        <groupId>org.littlewings</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>launcher</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.littlewings</groupId>
            <artifactId>app</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.9.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

こんな感じで。

パッケージング。

$ mvn package

すると、「main classがわからない」とエラーになるので

[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.1.9.RELEASE:repackage (default) on project launcher: Execution default of goal org.springframework.boot:spring-boot-maven-plugin:2.1.9.RELEASE:repackage failed: Unable to find main class -> [Help 1]

「app」側に入っていた、mainClassを指定します。

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.9.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>org.littlewings.spring.boot.App</mainClass>
                </configuration>
            </plugin>

今度は成功するので、パッケージングして起動。

$ mvn package
$ java -jar launcher/target/launcher-0.0.1.jar

これで、問題なく動作します。

$ curl localhost:8080/hello
Hello World!!

$ curl localhost:8080/index.html
Hello FreeMarker

実に、ふつうでした…。

まあ、こんな構成しないと思いますけど。

なお、通常だとSpring BootでメインのプロジェクトでUber JARにするとJARの中にこんな感じで入りますが

    17 Thu Oct 17 00:25:38 JST 2019 BOOT-INF/classes/static/index.html
    24 Thu Oct 17 00:25:38 JST 2019 BOOT-INF/classes/templates/hello.ftl
  1410 Thu Oct 17 00:25:40 JST 2019 BOOT-INF/classes/org/littlewings/spring/boot/App.class

依存関係として突っ込むと、こんな感じで入ります。

  3787 Wed Oct 16 15:31:40 JST 2019 BOOT-INF/lib/app-0.0.1.jar

BOOT-INF/classesな構成を守らないといけないのかな?と思ったところが、今回の確認をした理由だったりします。

requiresUnpack?

ところで、Spring Boot Maven Pluginを見ていて、repackageゴールに「requiresUnpack」なるものがあったので、もしかしてこのあたり
必要だったりするのかな?とかちょっと思ったりしていました。

Spring Boot Maven Plugin – Spring Boot

Spring Boot Maven Plugin – spring-boot:repackage

requiresUnpack

結論としては、不要だったのですが。

この設定、なにに使うかというと、指定したJARが「ファイルとして欲しい場合に使う」みたいです。

Extract Specific Libraries When an Executable Jar Runs

requiresUnpackに特定の依存関係を指定しておくと、実行時に「java.io.tmpdir」で指定した一時ディレクトリ内に取り出してくれます。

たとえばこういう設定をすると

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.9.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>org.littlewings.spring.boot.App</mainClass>
                    <requiresUnpack>
                        <dependency>
                            <groupId>org.littlewings</groupId>
                            <artifactId>app</artifactId>
                        </dependency>
                    </requiresUnpack>
                </configuration>
            </plugin>

指定したJARが、Uber JARの実行時に一時ディレクトリ内に取り出されていることが確認できます。

$ find /tmp/launcher-0.0.1.jar-spring-boot-libs-9a5ae828-0009-47ca-b1fa-f26a9e906307 -type f
/tmp/launcher-0.0.1.jar-spring-boot-libs-9a5ae828-0009-47ca-b1fa-f26a9e906307/app-0.0.1.jar

requiresUnpackを指定しなかった場合には、このような一時ディレクトリは作成されません。