【Java】モジュール機能の利用
今回はJava9から利用可能になったモジュール機能についてみていきます。
モジュール機能
モジュール機能はパッケージをまとめるための機能です。
module-info.javaを利用することで、特定パッケージを非公開にするなど、アクセス制限を付与したりすることができます。
これまでもprivate修飾子やprotected修飾子を利用してアクセス制限を行えましたが、より細かくアクセス制限をすることが可能となります。
例えば、Aというクラスの機能が利用できるのはBというクラスのみ利用できるようにしたりできます。
その他には、ServiceLoaderで特定のサービスクラスとして利用できるようにする機能もあるようです。
ライブラリ作ったり、チームで開発したりする人くらいにしか役に立たない機能かもしれませんね。
では実際に利用しながら動きを確認してみましょう。
動作確認するための準備
今回は、より動きを細かく確認できるよう、実際にフォルダ構成を用意し、コマンドラインでの確認をできるようにします。
テスト環境はWindowsで、一発コンパイルとプログラムを実行するためのバッチファイルを用意し、より動作確認しやすい状態にしたいと思います。
構成は次のような形としました。
モジュールはcharamodモジュールとmainmodモジュールの二つを用意し、charamodは提供側でmainmodは利用側という前提にしたいと思います。
mainmodはcharamodモジュールを使用して処理を実行するという感じですね。
各種ファイルの初期状態は下記のような形です。
module charamod { }
module mainmod { }
package chara; public class Chara{ public String name; }
package chara; public interface CharaBase{ void attack(); }
package main; public class Main{ public static void main(String[] args){ System.out.println("テスト"); } }
javac charamod/*.java charamod/chara/*.java javac --module-path ./charamod --add-modules charamod mainmod/*.java mainmod/main/*.java pause
java --module-path ./charamod;./mainmod --add-modules charamod,mainmod -m mainmod/main.Main pause
※バッチファイルは環境変数のpathにJavaへのパスが通っている前提のコードです。
テスト
実際使う場合はモジュールはjar化することがほとんどかと思われますが、今回はわかりやすいようにフォルダにして確認できるようにしています。
requiresでモジュールの機能を利用する
別のモジュールの機能を利用する場合requires宣言を行い使用したいモジュールを指定します。
試しにMainクラスでCharaクラスを利用するコードを試します。
package main; import chara.Chara; public class Main{ public static void main(String[] args){ Chara ch = new Chara(); ch.name = "テスト"; System.out.println(ch.name); } }
上記を修正してコンパイル
C:\Java_module_test>javac charamod/*.java charamod/chara/*.java C:\Java_module_test>javac --module-path ./charamod --add-modules charamod mainmo d/*.java mainmod/main/*.java mainmod\main\Main.java:3: エラー: パッケージcharaは表示不可です import chara.Chara; ^ (パッケージcharaはモジュールcharamodで宣言されていますが、モジュールmainmodに 読み込まれていません) エラー1個
モジュール読み込みエラーが発生しました。これはmainmodにcharamodのrequires宣言をすることで回避できます。
mainmodのmodule-infoに「requires モジュール名」などを指定してみましょう
module mainmod { requires charamod; }
上記を修正してコンパイル
C:\Java_module_test>javac charamod/*.java charamod/chara/*.java C:\Java_module_test>C:\Java_module_test>javac charamod/*.java charamod/chara/*.java C:\Java_module_test>javac --module-path ./charamod --add-modules charamod mainm d/*.java mainmod/main/*.java mainmod\main\Main.java:3: エラー: パッケージcharaは表示不可です import chara.Chara; ^ (パッケージcharaはモジュールcharamodで宣言されていますが、エクスポートされてい ません) エラー1個
エラー内容が変わり、モジュールの読み込みはできているようです。
このエラーは読み込み先モジュールで、公開宣言がされていないことによるエラーです。
exportsで機能を公開
通常公開されていないクラスを利用しようとするとコンパイルエラーになり利用できません。
モジュール内の機能を公開するには「exports パッケージ名」などのように指定します。
上記エラーが発生した後の状態で下記のように修正します。
module charamod { exports chara; }
compile.batでコンパイルしてみましょう。
C:\Java_module_test>javac charamod/*.java charamod/chara/*.java C:\Java_module_test>javac --module-path ./charamod --add-modules charamod mainmo d/*.java mainmod/main/*.java C:\Java_module_test>pause 続行するには何かキーを押してください . . .
コンパイルエラーがなくなり成功しました。
特定のモジュールに対してのみ公開したい場合は「exports パッケージ to モジュール名(複数指定の場合はカンマ区切り)」と指定することでできます。
module charamod { exports chara to mainmod; }
※今回のセットではモジュール指定がないためコンパイルは通りません
これによりモジュールの公開範囲を限定できます
transitiveで依存先モジュールのrequires宣言を省略
requires宣言にtransitiveを指定することで指定したモジュールを利用する側でrequires宣言を省略できます。
わかりにくいと思うので下記のようにファイル修正し実際に動作を確認して理解していくことにしましょう。
package chara; import java.awt.Rectangle; public class Chara{ public String name; public Rectangle rect; }
module charamod { exports chara; requires java.desktop; }
package main; import chara.Chara; public class Main{ public static void main(String[] args){ Chara ch = new Chara(); ch.name = "テスト"; System.out.println(ch.name); } }
module mainmod { requires charamod; }
compile.batでコンパイルしてみましょう。
C:\Java_module_test>javac charamod/*.java charamod/chara/*.java C:\Java_module_test>javac --module-path ./charamod --add-modules charamod mainmo d/*.java mainmod/main/*.java mainmod\main\Main.java:4: エラー: パッケージjava.awtは表示不可です import java.awt.Rectangle; ^ (パッケージjava.awtはモジュールjava.desktopで宣言されていますが、モジュールmai nmodに読み込まれていません) エラー1個
mainmodにjava.desktopがrequiresされていないので当然です。
ですがmainmodにjava.desktopがrequiresされていなくてもcharamodのrequiresにtransitiveをつけておくと読み込み先でrequiresする必要がなくなります。
試しに下記のように修正してみましょう。
module charamod { exports chara; requires transitive java.desktop; }
compile.batでコンパイルしてみましょう。
C:\Java_module_test>javac charamod/*.java charamod/chara/*.java C:\Java_module_test>javac --module-path ./charamod --add-modules charamod mainmo d/*.java mainmod/main/*.java C:\Java_module_test>pause 続行するには何かキーを押してください . . .
特にエラーが出ずにコンパイルに成功しました。
これにより依存先のrequiresをしなくてよくなるので楽です。
opens宣言でリフレクションによる強制アクセスを許可する
Javaではリフレクションでprivateな変数やメソッドにアクセス可能ですが、今回のモジュール機能を利用するとアクセスできなくなる状態がデフォルトの動きとなります。
実際に試してみましょう。
package chara; public class Chara{ private String name; public void view(){ System.out.println(name); } }
module charamod { exports chara; }
package main; import java.lang.reflect.Field; import chara.Chara; public class Main{ public static void main(String[] args)throws Exception{ Chara ch = new Chara(); Field fld = ch.getClass().getDeclaredField("name"); fld.setAccessible(true); fld.set(ch,"テスト"); ch.view(); } }
module mainmod { requires charamod; }
compile.batを実行するとコンパイルに成功するはずです。
次にexecute.batを実行しましょう。
C:\Java_module_test>java --module-path ./charamod;./mainmod --add-modules charam od,mainmod -m mainmod/main.Main Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String chara.Chara.name accessible: module char amod does not "opens chara" to module mainmod at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(Un known Source) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(Un known Source) at java.base/java.lang.reflect.Field.checkCanSetAccessible(Unknown Sourc e) at java.base/java.lang.reflect.Field.setAccessible(Unknown Source) at mainmod/main.Main.main(Main.java:11)
エラーで実行できませんでした。
module-infoにopens宣言を行うことで、これまで通り、リフレクションによる強制アクセスを許可できます。
module charamod { exports chara; opens chara; }
compile.bat、execute.batを実行しましょう。
テスト
強制アクセスができていることが確認できました。
指定したモジュールにのみ許可する「opens パッケージ to モジュール名(複数指定の場合はカンマ区切り)」宣言なども使用できます。
module charamod { exports chara; opens chara to mainmod; }
※今回のセットではコンパイルは通りません。
モジュール宣言の先頭にopenを宣言しておくことで個々のopens宣言を省略することもできます。
open module charamod { exports chara; }
全て許可する場合は利用しましょう。
provides宣言でサービスプロバイダ機能を利用可能な状態にする
provides宣言を行うことでServiceLoaderクラスで対象のインターフェースを実装したクラスを一度にロードできます。
宣言は「provides interface名 with 実装クラス名」のような形になります。
実際にコードを書くと下記のような形となります。
module charamod { exports chara; provides chara.CharaBase with chara.Chara; }
次にServiceLoaderを使用するためのクラスとインターフェースを定義してみましょう。
package chara; public class Chara implements CharaBase{ public String name; public void attack(){ System.out.println("KICK"); } }
package chara; public interface CharaBase{ void attack(); }
これでモジュール提供側の定義は完了です。
uses宣言でサービスプロバイダ機能を利用する
provides宣言で定義したものを利用する場合は利用側でuses宣言を行います。
動きを確認するために上記で定義した状態で上記で定義したあとに利用側のコードを定義して動きを確認してみましょう。
宣言は「uses インターフェース名」のような形になります。
実際にコードを書くと下記のような形となります。
module mainmod { requires charamod; uses chara.CharaBase; }
動作を確認するためにServiceLoaderで実装クラスを呼び出すコードが下記になります。
package main; import java.util.ServiceLoader; import chara.CharaBase; public class Main{ public static void main(String[] args){ for (CharaBase ch : ServiceLoader.load(CharaBase.class)) { ch.attack(); } } }
compile.bat、execute.batを実行すると動作が確認できます。
KICK
KICKが表示されれば成功です。
ディスカッション
コメント一覧
まだ、コメントがありません