SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Java 9で「変わること」と、Javaのこれまで

Java 9のモジュール機能「サービス(SPI)」と既存ライブラリの共存

Java 9で「変わること」と、Javaのこれまで 第3回

実際の利用例(Gsonライブラリを使った場合)

 

 文章で説明しただけではわかりにくい部分もあるため、ここからは実際にどのようなエラーが発生し、どのように解決すればいいのかを見ていきます。今回の例ではGsonというオープンソースライブラリを使った場合を紹介します。

 まず、リスト2の通りに定義したクラスを用意します。

リスト2 JSON文字列にするためのクラス定義(gson/src/main/java/com/coltware/gson/JsonObject.javaの抜粋)
public class JsonObject {
    private String hello;
    private String lang;
    // (省略)
}

 リスト3は、Gsonライブラリを使ってJsonObjectオブジェクトをJSON文字列にするコードです。

リスト3 GsonでオブジェクトをJSON文字列にするコード例(gson/src/main/java/com/coltware/gson/Main.javaの抜粋)
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());

 このとき、以下の文字列が生成されることを期待しています。

リスト4 実行した際に期待される結果
{"hello":"Hello","lang":"ja"}

 しかし、これらのコードをコンパイルし、実行するにはリスト5のオプションが必要になります。

リスト5 Gsonライブラリを使ったときの実行例(gson/cmd/java.shの抜粋)
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のエラーが発生します。

リスト6 Gsonライブラリを使いjava.sqlモジュールを追加せずに実行した際のエラー
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です。

リスト7 Gsonからのアクセス許可を与えずに実行した際のエラー
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のコマンドを実行します。

リスト8 jdepsコマンドを使って必要なアクセス権限を調べる方法
$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の内容が表示されます。

リスト9 jdepsコマンドを使ったモジュール依存の取得例
$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の通り出力されます。先ほどの「見つかりません」の部分がなくなっているのがわかります。

リスト10 jdepsコマンドを使ったモジュール依存の取得例(複数のjarファイルを同時に指定したとき)
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クラスを使ってその実装を見つけます。

リスト11 JDBCドライバのサービスを見つける場合のコード例(service1/src/main/java/com/coltware/sercice/Main.java)
// 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の記述がされています。

リスト12 java.sql.Driver(MySQLドライバでの定義例)
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にて宣言をしています。

リスト13 サービスを利用する側のモジュールで必要な宣言(service1/src/main/java/module-info.java)
module com.coltware.service {
    uses java.sql.Driver;    // (1)ServiceLoaderを通じて取得するサービス
    requires java.sql;       // (2)java.sqlモジュールを使う宣言
}

 例えば、この(1)の宣言をせずにコンパイルをすると、リスト14のエラーが表示されます。したがって、もし記述を忘れていてもusesでのモジュール追加が必要だということがわかるはずです。

リスト14 宣言せずにコンパイルを実行したときのエラー例
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の通りモジュールを構築した例を示します。

図3 サービスを利用する場合のモジュール例
図3 サービスを利用する場合のモジュール例

 先ほどと同じように、サービスの実装を呼ぶ側では、リスト15の通りusesにて宣言をします。

リスト15 サービスを利用する側のmodule-infoの記述(service2/application/src/main/java/module-info.java)
module com.coltware.application {
    requires com.coltware.service;
    uses com.coltware.service.Hello; // 利用するSPIのインターフェース
}

 Helloインターフェースを実装したモジュールであるcom.coltware.serviceモジュールでは、サービスのための宣言は必要ありません。しかし、そのインターフェースを実装したモジュール側で、リスト16の(1)のようにprovides/withでの宣言が必要になります。

リスト16 サービスの実装を提供するモジュール側のmodule-infoの記述(service2/english/src/main/java/module-info.java)
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など、他の機能の紹介をしていきます。

参考資料

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
Java 9で「変わること」と、Javaのこれまで連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 小林 昌弘(コバヤシ マサヒロ)

WINGSプロジェクト について> 有限会社 WINGSプロジェクト が運営する、テクニカル執筆コミュニティ(代表 ...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/10581 2018/01/05 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー