【Java】モジュール機能の利用

今回はJava9から利用可能になったモジュール機能についてみていきます。



モジュール機能

モジュール機能はパッケージをまとめるための機能です。

module-info.javaを利用することで、特定パッケージを非公開にするなど、アクセス制限を付与したりすることができます。

これまでもprivate修飾子やprotected修飾子を利用してアクセス制限を行えましたが、より細かくアクセス制限をすることが可能となります。

例えば、Aというクラスの機能が利用できるのはBというクラスのみ利用できるようにしたりできます。

その他には、ServiceLoaderで特定のサービスクラスとして利用できるようにする機能もあるようです。

ライブラリ作ったり、チームで開発したりする人くらいにしか役に立たない機能かもしれませんね。

では実際に利用しながら動きを確認してみましょう。

動作確認するための準備

今回は、より動きを細かく確認できるよう、実際にフォルダ構成を用意し、コマンドラインでの確認をできるようにします。

テスト環境はWindowsで、一発コンパイルとプログラムを実行するためのバッチファイルを用意し、より動作確認しやすい状態にしたいと思います。

構成は次のような形としました。

Java_module_test.zip

モジュールはcharamodモジュールとmainmodモジュールの二つを用意し、charamodは提供側でmainmodは利用側という前提にしたいと思います。

mainmodはcharamodモジュールを使用して処理を実行するという感じですね。

各種ファイルの初期状態は下記のような形です。

charamod/module-info.java
module charamod {
}
mainmod/module-info.java
module mainmod {
}
charamod/chara/Chara.java
package chara;

public class Chara{
	public String name;
}
charamod/chara/CharaBase.java
package chara;

public interface CharaBase{
	void attack();
}
mainmod/main/Main.java
package main;
public class Main{

	public static void main(String[] args){
		System.out.println("テスト");
	}
}
compile.bat
javac charamod/*.java charamod/chara/*.java
javac --module-path ./charamod --add-modules charamod mainmod/*.java mainmod/main/*.java
pause
execute.bat
java --module-path ./charamod;./mainmod --add-modules charamod,mainmod -m mainmod/main.Main
pause

※バッチファイルは環境変数のpathにJavaへのパスが通っている前提のコードです。

実行結果
テスト

実際使う場合はモジュールはjar化することがほとんどかと思われますが、今回はわかりやすいようにフォルダにして確認できるようにしています。

requiresでモジュールの機能を利用する

別のモジュールの機能を利用する場合requires宣言を行い使用したいモジュールを指定します。

試しにMainクラスでCharaクラスを利用するコードを試します。

mainmod/main/Main.java
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 モジュール名」などを指定してみましょう

mainmod/module-info.java
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 パッケージ名」などのように指定します。

上記エラーが発生した後の状態で下記のように修正します。

charamod/module-info.java
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宣言を省略できます。

わかりにくいと思うので下記のようにファイル修正し実際に動作を確認して理解していくことにしましょう。

charamod/chara/Chara.java
package chara;

import java.awt.Rectangle;

public class Chara{
	public String name;
	public Rectangle rect;
}
charamod/module-info.java
module charamod {
	exports chara;
	requires java.desktop;
}
mainmod/main/Main.java
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);
	}
}
mainmod/module-info.java
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する必要がなくなります。

試しに下記のように修正してみましょう。

charamod/module-info.java
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な変数やメソッドにアクセス可能ですが、今回のモジュール機能を利用するとアクセスできなくなる状態がデフォルトの動きとなります。

実際に試してみましょう。

charamod/chara/Chara.java
package chara;

public class Chara{
	private String name;
	public void view(){
		System.out.println(name);
	}
}
charamod/module-info.java
module charamod {
	exports chara;
}
mainmod/main/Main.java
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();
	}
}
mainmod/module-info.java
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宣言を行うことで、これまで通り、リフレクションによる強制アクセスを許可できます。

charamod/module-info.java
module charamod {
	exports chara;
	opens chara;
}

compile.bat、execute.batを実行しましょう。

実行結果
テスト

強制アクセスができていることが確認できました。


指定したモジュールにのみ許可する「opens パッケージ to モジュール名(複数指定の場合はカンマ区切り)」宣言なども使用できます。

charamod/module-info.java
module charamod {
	exports chara;
	opens chara to mainmod;
}

※今回のセットではコンパイルは通りません。


モジュール宣言の先頭にopenを宣言しておくことで個々のopens宣言を省略することもできます。

charamod/module-info.java
open module charamod {
	exports chara;
}

全て許可する場合は利用しましょう。

provides宣言でサービスプロバイダ機能を利用可能な状態にする

provides宣言を行うことでServiceLoaderクラスで対象のインターフェースを実装したクラスを一度にロードできます。

宣言は「provides interface名 with 実装クラス名」のような形になります。

実際にコードを書くと下記のような形となります。

charamod/module-info.java
module charamod {
	exports chara;
	provides chara.CharaBase with chara.Chara;
}

次にServiceLoaderを使用するためのクラスとインターフェースを定義してみましょう。

charamod/chara/Chara.java
package chara;

public class Chara implements CharaBase{
	public String name;
	public void attack(){
		System.out.println("KICK");
	}
}
charamod/chara/CharaBase.java
package chara;

public interface CharaBase{
	void attack();
}

これでモジュール提供側の定義は完了です。

uses宣言でサービスプロバイダ機能を利用する

provides宣言で定義したものを利用する場合は利用側でuses宣言を行います。

動きを確認するために上記で定義した状態で上記で定義したあとに利用側のコードを定義して動きを確認してみましょう。

宣言は「uses インターフェース名」のような形になります。

実際にコードを書くと下記のような形となります。

mainmod/module-info.java
module mainmod {
	requires charamod;
	uses chara.CharaBase;
}

動作を確認するためにServiceLoaderで実装クラスを呼び出すコードが下記になります。

mainmod/main/Main.java
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が表示されれば成功です。

Java

Posted by nompor