実際の利用例(Gsonライブラリを使った場合)
文章で説明しただけではわかりにくい部分もあるため、ここからは実際にどのようなエラーが発生し、どのように解決すればいいのかを見ていきます。今回の例ではGsonというオープンソースライブラリを使った場合を紹介します。
まず、リスト2の通りに定義したクラスを用意します。
public class JsonObject {
private String hello;
private String lang;
// (省略)
}
リスト3は、Gsonライブラリを使ってJsonObjectオブジェクトをJSON文字列にするコードです。
Gson gson = new Gson();
JsonObject obj = new JsonObject();
obj.setLang("ja");
obj.setHello("Hello");
String json = gson.toJson(obj); // (1)JSON文字列を作成
System.out.println(json.toString());
このとき、以下の文字列が生成されることを期待しています。
{"hello":"Hello","lang":"ja"}
しかし、これらのコードをコンパイルし、実行するにはリスト5のオプションが必要になります。
java --add-modules java.sql \ // (1)java.sqlモジュールの追加
--add-opens com.coltware.gson/com.coltware.gson=gson \ // (2)gsonモジュールからのアクセス許可
--module-path libs \ // モジュールパスを指定
-m com.coltware.gson/com.coltware.gson.Main
(1)では、java.sqlモジュールを実行時に追加しています。このオプションをつけずに実行をするとリスト6のエラーが発生します。
Exception in thread "main" java.lang.NoClassDefFoundError: java/sql/Time
at gson@2.8.2/com.google.gson.Gson.<init>(Gson.java:240)
at gson@2.8.2/com.google.gson.Gson.<init>(Gson.java:174)
このエラーはGsonライブラリがjava.sql.Timeクラスを利用しているために生じるエラーです。java.sql.Timeクラスはjava.baseモジュールではなくjava.sqlモジュールになるため、デフォルトでは利用できず、また、プログラム内のコードでも利用していないためリンクされません。
このオプションを指定しなくても、module-info.java内にjava.sqlモジュールの宣言を追加することで問題を回避できます。しかし、ライブラリ側で将来的にjava.sqlモジュールへの依存がなくなっても、意味のないコードが残ってしまうことになります。そのため、できるだけ--add-modulesオプションを使った方がいいでしょう。
続いて、(2)の指定を行わずに、実行したときに生じるエラーがリスト7です。
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make public com.coltware.gson.JsonObject() accessible: module com.coltware.gson does not "exports com.coltware.gson" to module gson
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
// 以下省略
これは、Gsonモジュール側からディープ・リフレクションを使ってcom.coltware.gsonモジュールにアクセスしようとしていますが、その権限がないために発生するエラーです。
ここで表示されているエラーではexportsが足りないと表示されています。ディープ・リフレクションになるため、リスト5では--add-opensを追加しています。
ここで、エラーを見てexportsなのかopensなのか判断がつかなくても、exportsをつけてみると、その後「opensが足りない」といったエラーが表示されます。そのため心配する必要はありません。
また、利用するjarファイルがモジュールパスではなく、クラスパスに配置された場合には、Unnamed Moduleとして扱われます。この場合は、先ほどのgsonというモジュール名の代わりに、ALL-UNNAMEDを指定します。
ALL-UNNAMEDはクラスパス内のUnnamed Moduleの全てを指すため、クラスパス内の全てのモジュールから自由にアクセスできることになります。したがって、セキュリティ上はあまり好ましい状態ではありません。その点ご注意ください。
実行前に既存のライブラリの制限を知る
ここまで、発生しうる問題とその解決方法の関係がわかりやすいように、実行しながら問題と解決方法を説明してきました。しかし、実際は問題を事前に知り、その解決を図りたいはずです。その場合には、jdepsコマンドが利用可能です。
例えば、先ほどのGsonライブラリがjava.sqlモジュールに依存していることを知るには、リスト8のコマンドを実行します。
$jdeps gson-2.8.2.jar gson-2.8.2.jar -> java.base gson-2.8.2.jar -> java.sql com.google.gson -> com.google.gson.internal gson-2.8.2.jar com.google.gson -> com.google.gson.internal.bind gson-2.8.2.jar // 以下省略
実行結果からは、java.baseとjava.sqlのモジュールに依存していることがわかります。
確認しようとしているライブラリがJavaのシステムモジュールのみに依存している場合、これでも問題ありません。しかし、実際にはjarファイルが他のjarファイルに依存しているケースが多々あります。
例えば、SLF4Jというロギングライブラリがこのケースにあたり、基本となるAPI部分のみで実行するとリスト9の内容が表示されます。
$jdeps slf4j-api-1.7.25.jar slf4j-api-1.7.25.jar -> java.base slf4j-api-1.7.25.jar -> 見つかりません org.slf4j -> java.io java.base // 省略 org.slf4j -> org.slf4j.event slf4j-api-1.7.25.jar org.slf4j -> org.slf4j.helpers slf4j-api-1.7.25.jar org.slf4j -> org.slf4j.impl 見つかりません // 以下省略
この場合、必要なjarファイルを全て指定してjdepsを実行すれば、リスト10の通り出力されます。先ほどの「見つかりません」の部分がなくなっているのがわかります。
jdeps slf4j-api-1.7.25.jar slf4j-simple-1.7.25.jar
slf4j-api-1.7.25.jar -> java.base
slf4j-api-1.7.25.jar -> slf4j-simple-1.7.25.jar
// 省略
org.slf4j -> org.slf4j.impl slf4j-simple-1.7.25.jar
// 省略
slf4j-simple-1.7.25.jar -> java.base
slf4j-simple-1.7.25.jar -> slf4j-api-1.7.25.jar
org.slf4j.impl -> java.io java.base
// 省略
jdepsコマンドはこれ以外にも、--generate-module-infoオプションなどを使ってjarファイルからmodule-info.javaファイルを生成することができます。
このように便利な使い方があるため、ぜひjdeps --helpなどを実行して調べてみてください。
サービス(SPI)機能を使うには
「サービス(SPI)」とは、Java 1.6にて導入されたサービスプロバイダロード機構を示しています。この機能はサービスとするインターフェースが実装されているjarファイルなどを、クラスパスに配置することで実装を提供します。
例えば、これらの方法はJDBCドライバの提供方法で用いられています。そして、利用者はJDBCドライバの実装オブジェクトを、jarファイル内の定義に従い取得することが可能です。
実際にはリスト11のコードのようにServiceLoaderクラスを使ってその実装を見つけます。
// Driverインターフェースの実装を取得
Iterator<java.sql.Driver> i = ServiceLoader.load(java.sql.Driver.class).iterator();
for(i.hasNext()){
java.sql.Driver driver = i.next();
// ここで必要なJDBCドライバを見つけるなど
}
このとき、Driverインターフェースの実装側となるjarファイル内のMETA-INF/servicesフォルダー配下に、インターフェースとなるクラス名と同名でファイルを用意します。この例であれば「java.sql.Driver」をファイル名とします。
MySQLのJDBCドライバである、Connector/J(mysql-connector-java-5.1.44)ではリスト12の記述がされています。
com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver
この記述によって、ServiceLoaderは2つのクラスを見つけることができます。
今回は、SPI機能についての詳細な説明は割愛しますが、Java 9以前の説明はJava6 APIリファレンスなどを参照してください。
Java 9でのサービスの利用方法
Java 9ではこのServiceLoaderを利用して実装を取得する際には、あらかじめmodule-info.java内でリスト13の(1)のようにusesでの宣言が必要になりました。また(2)では、実際に呼び出した際にコード内でDriverクラスを利用する必要があるため、requiresにて宣言をしています。
module com.coltware.service {
uses java.sql.Driver; // (1)ServiceLoaderを通じて取得するサービス
requires java.sql; // (2)java.sqlモジュールを使う宣言
}
例えば、この(1)の宣言をせずにコンパイルをすると、リスト14のエラーが表示されます。したがって、もし記述を忘れていてもusesでのモジュール追加が必要だということがわかるはずです。
Exception in thread "main" java.util.ServiceConfigurationError: java.sql.Driver: module com.coltware.service does not declare `uses`
Java 9以前は特に何もせず利用できていましたが、追加の対応が必要になるため注意が必要です。
Java 9でのサービスの提供方法
続いて、サービスを提供する方法を紹介します。Java 9ではサービスの定義方法にも変更があります。
図3の通りモジュールを構築した例を示します。
先ほどと同じように、サービスの実装を呼ぶ側では、リスト15の通りusesにて宣言をします。
module com.coltware.application {
requires com.coltware.service;
uses com.coltware.service.Hello; // 利用するSPIのインターフェース
}
Helloインターフェースを実装したモジュールであるcom.coltware.serviceモジュールでは、サービスのための宣言は必要ありません。しかし、そのインターフェースを実装したモジュール側で、リスト16の(1)のようにprovides/withでの宣言が必要になります。
module com.coltware.english {
requires com.coltware.service; // Helloインターフェースを利用するための宣言
provides com.coltware.service.Hello with com.coltware.english.HelloImpl; // (1)provides <インターフェース> with <実装クラス>
}
ここで記述したコードはコンパイラによってチェックされるため、間違いが起きにくく使いやすくなったのではないかと思います。
ただしこれまでと同じように、META-INF/servicesを用いた指定の意味がなくなるわけではありません。モジュール機能を使わない、つまり実行時にモジュールパスを使わず、クラスパスに指定した場合は、これまでと同様にMETA-INF側の宣言が使われます。
したがって、モジュールパスとクラスパス、どちらのケースも想定されている場合には2つの指定をしなければいけません。
さいごに
前回に引き続き、モジュール機能「Project Jigsaw」について紹介してきました。また、既存のライブラリなどを組み合わせて利用してみると、エラーの原因がわからず、実行ができないケースがありました。
全てのケースおいて、プロダクションレベルで既存のコードをJava 9のモジュール対応にするには、まだ知見が蓄積されていない状態です。特に、大規模なサーバサイド側のプロジェクトでは、Java 9以前を前提としてコンテナ上で動作することが多く、機能よりも実績を優先させる必要があるため、普及するのはもう少し先になるでしょう。
しかし、IoTシステムの一部として機器に組み込んでJavaを使う場合など、モジュール機能が持つアクセス権限機能はセキュリティ対策にも貢献できるはずです。また未対応ではありますが、Androidでもこのモジュール機能はセキュリティを高める機能になるはずです。
次回からは、Java 9でリリースされたjShellなど、他の機能の紹介をしていきます。
