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 MVCFreeMarkerで作成することにしましょう。

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を指定しなかった場合には、このような一時ディレクトリは作成されません。

Jupyter NotebookをUbuntu Linux 18.04 LTSに、Extension入りでインストールする

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

ちょっと、Jupyter Notebookをインストールしてみようかなと。

Jupyter Notebook

Jupyter Notebookは、Pythonのエディタと実行環境をまとめたツールで、プログラムや文章を書いたり、実行結果をまとめたりできます。

Project Jupyter | Home

Jupyter Notebookをインストールするにあたっての前提条件は、Pythonがインストールされていること、みたいです。

Project Jupyter | Installing the Jupyter Software

While Jupyter runs code in many programming languages, Python is a requirement (Python 3.3 or greater, or Python 2.7) for installing the JupyterLab or the classic Jupyter Notebook.

インストール自体はpipで行うようです。

環境

今回の環境はこちら。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.3 LTS
Release:    18.04
Codename:   bionic


$ python3 -V
Python 3.6.8


$ pip3 -V
pip 9.0.1 from /path/to/lib/python3.6/site-packages (python 3.6)

Jupyter Notebookのインストール

では、ドキュメントに沿ってインストール。

Project Jupyter | Installing the Jupyter Software

pipでインストールします。

$ pip3 install jupyterlab

インストールされた、Jupyter Notebookのバージョン。

$ jupyter --version
jupyter core     : 4.6.0
jupyter-notebook : 6.0.1
qtconsole        : not installed
ipython          : 7.8.0
ipykernel        : 5.1.2
jupyter client   : 5.3.4
jupyter lab      : 1.1.4
nbconvert        : 5.6.0
ipywidgets       : not installed
nbformat         : 4.4.0
traitlets        : 4.3.3

余談

Ubuntu Linuxの場合はaptでもインストールすることが可能です。

jupyter-notebook package : Ubuntu

ですが、このあとのExtensionのインストールなどで困ったことになるので、ここはpipでインストールすることにしました。

Jupyter Notebookを起動してみる

以下のコマンドで、カレントディレクトリを起点として起動します。

$ jupyter notebook

自動でブラウザが起動しますが、コンソールに以下のような出力も行われるので、こちらでアクセスしてもよいでしょう。

    To access the notebook, open this file in a browser:
        file://$HOME/.local/share/jupyter/runtime/nbserver-21589-open.html
    Or copy and paste one of these URLs:
        http://localhost:8888/?token=xxxxx
     or http://127.0.0.1:8888/?token=xxxxx

Web UI。

f:id:Kazuhira:20191014221522p:plain

ヘルプ。

$ jupyter -h
usage: jupyter [-h] [--version] [--config-dir] [--data-dir] [--runtime-dir]
               [--paths] [--json]
               [subcommand]

Jupyter: Interactive Computing

positional arguments:
  subcommand     the subcommand to launch

optional arguments:
  -h, --help     show this help message and exit
  --version      show the jupyter command's version and exit
  --config-dir   show Jupyter config dir
  --data-dir     show Jupyter data dir
  --runtime-dir  show Jupyter runtime dir
  --paths        show all Jupyter paths. Add --json for machine-readable
                 format.
  --json         output paths as machine-readable json

Available subcommands: bundlerextension kernel kernelspec lab labextension
labhub migrate nbconvert nbextension notebook run serverextension troubleshoot
trust

Extensionをインストールする

追加で、Jupyter NotebookのExtensionをインストールします。

jupyter_contrib_nbextensionsは、コミュニティが開発したJupyter NotebookのアンオフィシャルなExtensionです。

Unofficial Jupyter Notebook Extensions — jupyter_contrib_nbextensions 0.5.0 documentation

Extensionの一覧は、こちらです。

List of provided nbextensions — jupyter_contrib_nbextensions 0.5.0 documentation

インストール方法は、こちら。

Installing jupyter_contrib_nbextensions — jupyter_contrib_nbextensions 0.5.0 documentation

pipでインストール。

$ pip3 install jupyter_contrib_nbextensions

インストールされたバージョンは、0.5.1です。

$ pip3 freeze | grep nbextensions
jupyter-contrib-nbextensions==0.5.1
jupyter-nbextensions-configurator==0.4.1

jupyter-nbextensions-configuratorというのは、Extensionの有効/無効をWeb UIで切り替えられるようにするものです。

GitHub - Jupyter-contrib/jupyter_nbextensions_configurator: A jupyter notebook serverextension providing config interfaces for nbextensions.

Extensionをインストールし、jupyter-nbextensions-configuratorを有効にします。

$ jupyter contrib nbextension install --user
$ jupyter nbextensions_configurator enable --user

pipでインストールしたのでは?と思うのですが、「jupyter contrib nbextension install」ではJavaScriptCSSがインストールされるようです。

Jupyter Notebookを1度終了させて、再度起動。

$ jupyter notebook

すると、「Nbextensions」というタブが増えています。

f:id:Kazuhira:20191014222154p:plain

最初は全体が「disable」になっているので

f:id:Kazuhira:20191014223537p:plain

こちらのチェックを外して

f:id:Kazuhira:20191014223557p:plain

あとは好きなExtensionを有効にしましょう。

f:id:Kazuhira:20191014223618p:plain

一応、コマンドラインでもExtensionの有効/無効は切り替えられるのですが、Web UIで切り替えられた方が便利でしょう。

Enabling/Disabling extensions