2017.10.11

レガシーアプリを Java9 でモジュール化する最短の方法

D. M. です。
2017年9月に Java9 がリリースされました。

(画像はJava公式 Twitterからの引用)

目玉は新概念のモジュールです。モジュールはある意味破壊的な変更でもありつつ既存のパッケージの仕様の問題を解決する上で非常に効果的な新概念だと感じています。今回はこのモジュールを理解する上で役に立った情報を私なりに整理してご紹介します。

モチベーション

私の担当プロジェクトでは社内向けのAPIと管理システムがJavaで組まれており10年以上運営しています。運用上の問題として、根幹となる共通機能は単体テストコードがあまり充分ではなくおまけにライブラリのJarの依存関係が非常に複雑になっているため、変更時に予期せぬ影響が発生しがちです。

こうした Java レガシーシステムに多く見受けられる 「Jar地獄」の状況に対して Java が出した公式の回答が Project Jigsaw すなわちモジュールという新しいカプセル化と依存関係の定義方法です。

やりたいこと

完全新規案件ならいいのですが、既存システムの JDK ヴァージョンアップを考えたとき、特に Java9 についてはモジュールという新概念をどう適用するかが非常に重要になってきます。本稿ではこの課題について1つの指針を紹介したいと思います。

流れは以下の通りです。

1.簡単に Java9 の新機能のまとめ
2.そもそもモジュールとは
3.モジュールシステム導入手法

1.簡単に Java9 の新機能のまとめ

Java9の新機能は例によってとても多いです。そしてすでに紹介記事がたくさんあるため、1番コンパクトに特徴的な10点を綺麗にまとめたスライドを紹介します。細かい機能でもかなり改善点があるためまだよく調べていない方はぜひご一読ください。

「10のJava9で変わるJava8の嫌なとこ!」

本稿では非常にインパクトの大きい変更であるモジュールについてだけ扱います。

個人的に1番わかりやすくコンセプトが理解できたのは Alex Buckley さんのプレゼン動画です。彼は Oracle のテックリード( the Specification Lead for the Java programming language and the Java Virtual Machine at Oracle )。非常に平易な英語で英語字幕もあります。そして60分とやや長めです。


動画で使われていたプレゼン資料ではないのですが、 openjdk のサイトから近い内容のものを見つけましたので以下にリンクを貼っておきます。
http://openjdk.java.net/projects/jigsaw/talks/adv-modular-dev-j1-2015.pdf

今回はこの動画のエッセンスを日本語で紹介したいと思います。これが本稿の中心的な内容です。

2.そもそもモジュールとは

ここから Java9 のモジュールについて基本的なことを簡単に確認します。

結論として、モジュールは「Java package の上位概念」です。そのメリットは、スコープをより細分化して定義しカプセル化を強化できることと、依存関係を明示的に定義しコンパイル時にチェックできることと言えます。

モジュールはパッケージのルートディレクトリで module-info.java を記載することで定義できます。導入に既存コードの変更は必要ありません。

強力なカプセル化

ライブラリ内の特殊な Util などのクラスは、本来隠蔽したかった場合でもパッケージ構成上 Public になっていることがあり Java8 以前ではリフレクションを使えば詳細にアクセスが可能でした。Java9 ではモジュールの定義ファイルを作成すると完全に隠蔽できるようになります。

カプセル化の強化の理解に、以下がとても参考になります。
(これは先ほどの動画内のプレゼン資料の一部です。)

(This image is originally from https://youtu.be/rfOjch4p0Po?t=8m34s)

モジュール化により Public の範囲がさらに詳細にきめ細かく設定できるようになりました。
Public が以下のように細分化されました。
・ public to everyone (全体への Public)
・ public but only to specific modules (同一モジュールおよび特定モジュールへの Public)
・ public only within a module (同一モジュール内での Public)
今までは同じ Public なパッケージは外から参照可能でした。パッケージ内でしか利用されない Util なども Public を選択せざるを得ませんでした。モジュール化はパッケージの親ディレクトリ以下全体を制御し、パッケージを読むことができる範囲を特定のモジュールにのみ限定することができます。

カプセル化強化の最たるメリットはプレゼン中に紹介されたこの図です。

(This image is originally from https://youtu.be/rfOjch4p0Po?t=20m24s)

hello.world は X.jar を利用していて X は lib.jar のヴァージョン1に依存しています。そして hello.world は Y.jar も利用することになりました、が、 Y.jar は lib.jar のヴァージョン2に依存していることがわかりました。この状況は既存のJavaではバージョン違いの lib.jar をクラスパス内に2つ含める必要が出てきてしまい、コンパイルが通らない(または実行できたがどっちが読まれているのかわからない)状況が発生します。
(この状況は Wikipedia の「クラスローダー」のページにもJar地獄の記載があります )

このバージョン混在問題はモジュール化によって解決できるようになります。(今回は詳細なコードの例は割愛します)

信頼できる依存関係

次のメリットは「信頼できる依存関係」です。

Class A の依存先 B.jar のさらに依存先の C.jar があるケースを考えてください。Java8 以前では Class A のコンパイルは B.jar があれば C.jar が抜けていても通ります。ですが C.jar がないと実行時に例外が発生するケースがありました。コンパイル通っているのになんなんだよっていう。

Java9 ではモジュールの記述を行うことでこうした依存関係をすべてコンパイル時に網羅的にチェックできます。(このメリットを活かすには requires transitive を使います。)長年の悩みがついに仕様的に解決できるようになるわけです!

ワクワクしてきませんか?なんかモジュールいけるんじゃないかって思えてきませんか?

具体的な議論に入ります。

3.モジュールシステム導入手法

基本的なサンプルコード

完全な初学者に向けて、以下のサイトからの引用で1番簡単なコードを用いてモジュールの使い方を紹介します。ここは既に最低限の理解がある方は読み飛ばしても大丈夫です。

Project Jigsaw: Module System Quick-Start Guide
http://openjdk.java.net/projects/jigsaw/quick-start

ルートディレクトリごとに module-info.java を定義します。
そのファイルの中でパッケージの公開状態を定義します。
1番基本的な部分としては exports で外部に公開、
requires で読み込みます。

// org.astro パッケージがモジュールとなります
src/org.astro/module-info.java
src/org.astro/org/astro/World.java
// com.greetings パッケージがモジュールとなります
src/com.greetings/module-info.java
src/com.greetings/com/greetings/Main.java

org.astro モジュール情報ファイル。公開を宣言します。
src/org.astro/module-info.java

    module org.astro {
        // モジュール公開しますというのを定義
        exports org.astro;
    }
World クラス。この機能は名前を返すだけです。
src/org.astro/org/astro/World.java
    package org.astro;
    public class World {
        public static String name() {
            // 名前を返すだけ
            return "world";
        }
    }
ポイントとして、このクラスは Java8 以前と書き方が変わっていません。モジュールは既存の処理コードに影響せずに定義できるわけです。

com.greetings のモジュール定義ファイル。
org.astro モジュールを使いますと言うのを宣言します。
src/com.greetings/module-info.java
    module com.greetings {
        //モジュールの依存関係を定義
        requires org.astro;
    }
アプリのメインクラス。World の名前を表示するだけです。ここも記載方法自体は既存Javaと同じです。
src/com.greetings/com/greetings/Main.java
    package com.greetings;
    import org.astro.World;
    public class Main {
        public static void main(String[] args) {
            // 単に表示する
            System.out.format("Greetings %s!%n", World.name());
        }
    }
コンパイルと実行です。

// クラスを入れる箱を作ります。
$ mkdir -p mods/org.astro mods/com.greetings

// org.astro のコンパイル
$ javac -d mods/org.astro \
src/org.astro/module-info.java src/org.astro/org/astro/World.java

// com.greetings のコンパイル。依存する org.astro モジュールを module-path に含めています。これによりクラスパスは不要です。
$ javac –module-path mods -d mods/com.greetings \
src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java

// 実行
$ java –module-path mods -m com.greetings/com.greetings.Main
Greetings world!

モジュール開放の定義はヴァリエーションがあります。他の記事も参考にしてサンプルコードを実行して理解を深めてください。

レガシーシステムのモジュール化

ここからがメインコンテンツです。

既存のプロジェクトをモジュール化するには以下が必要です。

1. 自分で作った既存のクラスについては、パッケージのルートディレクトリに module-info.java を記述して配置する。
※既存の処理のコード自体に手を加えることは必要ありません。新しいモジュール情報クラスを作成しその中で依存関係を明示的に記述します。

2. 既存の外部 Jar は Automatic Module にする。

サンプルシナリオ

再び動画からの引用で、レガシーシステムモジュール化の課題を考えます。

(This image is originally from https://youtu.be/rfOjch4p0Po?t=25m45s)

このシナリオでは、既存で動いているレガシーのアプリについて以下の3点をモジュール化することを行います。

A. 自分で作ったアプリ
myapp.jar mylib.jar の2つ。もちろんモジュール化されていません。

B. 外部ライブラリ
jackson-core-2.6.2.jar など3つ。モジュール化されていないです。

C. jdk モジュール
java.base など4つ。これは jdk9 からデフォルトでモジュール化されて提供されます。

実はこのシナリオでは具体的なコードは出てきません。モジュール化は実際の処理がどうなっているかは理解していなくても可能であるためです。

ここから個別のモジュール化対応方法を見ていきましょう。

自分で作ったアプリのモジュール化

まずは自分の作ったパッケージをモジュール化します。それをパッケージディレクトリのルートでモジュール定義ファイル module-info.java で依存関係を記述します。モジュール名、必要な依存モジュール、公開するモジュールを決める必要があります。
ここで問題になるのが依存モジュールです。依存関係を詳細に記述するには既存の java のソースから import ~ と書いている部分をパッケージ内の全ファイルにわたって抽出することが必要になります。
「ええっ!こんな面倒な作業するなら導入はそうとうハードルあるな。。」
こういうケースが大半になるかと思いますが、その解決作として jdeps を使います。
jdeps で依存モジュールをリスト化できる
Youtube を見る限り Oracle の人はジェイデップスと発音しているようです。

jdeps はクラスファイルおよび Jar に対して依存関係のリストを表示できるコマンドです。java や javac と同じ bin ディレクトリのなかにありますので PATH が通っていればすぐ使えます。 -s で jar 単位で依存関係モジュールと jar のリストを作ることができます。-s なしだと依存モジュールの一覧だけが表示されます。

これがあれば module-info.java を作るのはけっこう簡単ですね。
今回のサンプルシナリオの jar については以下のような結果になりました。

$ jdeps -s lib/myapp.jar lib/mylib.jar
myapp.jar -> lib/jackson-core-2.6.2.jar
myapp.jar -> lib/jackson-databind-2.6.2.jar
myapp.jar -> mylib.jar
myapp.jar -> java.base
myapp.jar -> java.sql
mylib.jar -> java.base

module-info.java を書いてみます。
まずは mylib のほうですが、
java.base に依存してるので requires を書きます。
myapp に依存されているので exports を書きます。

module mylib{
  requires java.base
  exports com.mylib.util to myapp
}
requires java.base は実際にはデフォルトで読み込まれるので書く必要はありません。
exports com.myapp.util to myapp の to myapp は公開先を限定したいときだけ使います。柔軟性を残したいときは to を書く必要はありません。実際の案件でパッケージ名含めてクラス名が被ったときに、公開先を制限することで使い分けるなどの使い方ができると思います。

exports は公開するクラスがあるフォルダを1つずつを指定します。仮に com.mylib.util 内に別のディレクトリがある場合は別パッケージになりますので分けて書く必要があります。親ディレクトリだけ指定しても子ディレクトリは対象になりません。

モジュール未対応の外部ライブラリは Automatic Module にする

外部のライブラリはもちろん外部の方が開発しているものですので、既存プロジェクトでは現段階ではまだ module-info 対応してない jar があります。これは待つほか無いというのではモジュール化対応が進みません。ただこれは別の対応方法があるので大丈夫です。それが Automatic Module です。なんと何もしないでも既存 jar を1発で Java9 モジュールシステムへ組み込めます。事情があってレガシーのものを使いつづける状況などではこれが非常に便利です。
Atomatic Module

・module-info.java の定義が無い jar をモジュールパス module-path で読み込んだ場合、そのクラスは Automatic Module となる。
・–module-path で jar を含むディレクトリを指定して実行したタイミングで自動的にこの状態になる。
・他のモジュールから認識されるモジュール名は jar ファイル名がルールに基づいて適用される。
・特にコードの追加・変更や設定ファイルは必要ありません。
・他の全モジュールへExportsされ、他の全モジュールをRequiresしています。
(実態として未定義のものは既存の挙動と変わらない状態になる)

Java9 のモジュールパスを利用せず既存と同じクラスパス(–class-path)で読み込まれたクラスまたは Jar は Unnamed Module と呼ばれ、こちらも Automatic Module と同様にその中のパッケージが全ての他のモジュールに exports された状態になります。(こちらも既存の状態と同じなので実質不自由は無い)ただデメリットとして、モジュール名称がないので他のモジュールの module-info.java で依存性の定義を記述することができません。特別な事情がなければモジュールパスに含めてモジュールシステム内で制御したほうが理に適います。

結論として myapp のモジュール情報は以下の定義になります。
jackson.core, jackson.databind が未対応 Jar の Automactic Module です。

module myapp {
  require mylib;
  require java.base;
  require java.sql;
  require jackson.core
  require jackson.databind
}
ライブラリがリフレクションをする場合の問題
myapp モジュールは原則的に exports していないので、自分以外のモジュールから呼ばれることはありません。ただ外部のモジュールが myapp をリフレクションで使っている場合、処理を実行できなくとも中身が読める状態を作る必要があります。そのとき出てくるのが opens です。 private でもアクセスできますので、既存のライブラリがリフレクションをするときと同じ挙動になるため、整合性をとることが可能です。

ただ根幹となる jdk9 のモジュールは隠ぺいが増えるので既存でリフレクションしているライブラリは修正が発生するものと思われます。

実行コマンドがシンプルに

Java の実行時は今まではこんな感じで全ての Jar を指定していました。

java –class-path lib/myapp.jar:\
lib/mylib.jar:\
lib/jackson-core-2.6.2.jar:\
lib/jackson-databind-2.6.2.jar:\
lib/jackson-annotation-2.6.2.jar\
myapp.Main

Java9 でモジュール化するとこのめんどくさい羅列化はなくなるので、以下のようにシンプルな1行になります。

java –module-path lib –module myapp

ちなみにクラスパスも無くなってはいないので、併用することも可能です。

これで一通りのモジュール化は完了です。

まとめです。
・自分で作ったアプリには module-info.java をパッケージのルートディレクトリに記述する。
・module-info.java は jdeps で依存関係を調べて記述する。
・モジュール未対応の jar は実行時にモジュールパス –module-path に指定すれば Automatic Module になる。

読むべき記事のまとめ

モジュールの詳細については Project Jigsaw が発表になってから既にいくつかの記事が検証していて、基本的な事柄は以下の記事を読むと理解を深めることができます。

「java 9のmodule機能を試してみる」
http://qiita.com/nmatsui/items/73ad642838631bcdc92e
とてもシンプルで基本を理解するにはもってこいの記事です。細かいですが「case4 : jarの依存追跡の解決」の service/src/module-info.java には requires transitive commons が必要です。

「ヌーラボのアカウント基盤を Java 9 にマイグレーションして起きた問題と解決法」
https://nulab-inc.com/ja/blog/nulab/java9-migration/
既存のソースコードを Java9 コンパイルするという挑戦的な記事です。非常に実践的。驚異的な面白さがあります。

「Jigsaw 勉強メモ」
https://qiita.com/opengl-8080/items/1007c2b2543c2fe0d7d5
単なる勉強メモとなっていますが非常に網羅的に文法を解説しています。例も豊富。困ったときに辞書的に読んだほうがよいです。

「JDK9のモジュールとjlinkでアプリ配布向けのJVMを作る」
https://qiita.com/koduki/items/5a1b5e5da95a21935d18
これはいわゆる fat-ja rとか GO のシングルバイナリのようなものを作る機能です。 JDK +必要なライブラリをモジュール情報の定義に基づいてサブセット化し、まるっと配布しやすくします。

「Java SE 9、Project Jigsawの標準モジュールと依存性の記述」
http://itpro.nikkeibp.co.jp/atcl/column/15/120700278/050900012/?rt=nocnt
jdk9 のデフォルトのモジュールについて詳細に書いてあります。

最後に

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事