Java でプラグイン機構を作ったときのお話です。プラグインを JAR ファイルとして実装して動的にロードさせることは簡単にできたのですが、 そのプラグイン JAR ファイルがネイティブライブラリー (DLL) を必要とした場合に、 DLL を動的にロードさせるのに手間取りました。
実行時に java.library.path
を変更して、 ユーザーが指定した場所にある DLL をロードできるようにする方法を紹介します。
仕組みから説明しているので少し長いです。手っ取り早く方法だけ知りたいという方は以下のリンクをクリックしてジャンプしてください。
ライブラリーロードの仕組み
Java では System.load()
または System.loadLibrary()
を使用してネイティブライブラリー (DLL) をロードすることができます。これらにはライブラリーのフルパスを指定するか、 ライブラリー名のみを指定するかという違いがあります。
ライブラリーのフルパスを指定する場合は System.load()
を使用します。
System.load("C:/mylibs/foobar.dll");
ライブラリー名を指定する場合は System.loadLibrary()
を使用します。このとき、 java.library.path
プロパティーで設定されているパスからライブラリーが検索されることになります。
System.loadLibrary("foobar");
この java.library.path
プロパティーというのが曲者で、 このプロパティーを実行に変更しても、 なぜかライブラリーの検索対象になってくれません。
//プロパティーの変更はできるが実際の検索対象パスは変わらない
System.setProperty("java.library.path", "C:/mylibs");
Javaのソースコードを調べる
ソースコードを追いかけて、 ライブラリーがロードされる仕組みを調べてみます。
System.load
System.load()
のソースコードは以下のようになっていました。
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
Runtime.load0()
を呼び出していますね。このソースコードは以下のようになっています。
synchronized void load0(Class<?> fromClass, String filename) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkLink(filename);
}
if (!(new File(filename).isAbsolute())) {
throw new UnsatisfiedLinkError(
"Expecting an absolute path of the library: " + filename);
}
ClassLoader.loadLibrary(fromClass, filename, true);
}
セキュリティーチェックをして、 ClassLoader.loadLibrary()
を呼び出していることが分かります。ここで少し System.load()
の調査を中断して、 System.loadLibrary()
を掘り下げていきます。
System.loadLibrary
System.loadLibrary()
のソースコードは以下のようになっていました。
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
System.load()
と似ています。Runtime.loadLibrary0()
のソースコードを見てみます。
synchronized void loadLibrary0(Class<?> fromClass, String libname) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkLink(libname);
}
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
ClassLoader.loadLibrary(fromClass, libname, false);
}
ここで、 System.load()
のときと同じ ClassLoader.loadLibrary()
が出てきました。つまり、 System.load()
も System.loadLibrary()
も最終的には ClassLoader.loadLibrary()
に辿り着くわけです。
ClassLoader.loadLibrary
辿り着いた ClassLoader.loadLibrary()
のソースコードを追っていきましょう。このメソッドの実装は少し長いですが、 重要なのは先頭の数行だけですので、 そこだけ抜粋します。
static void loadLibrary(Class<?> fromClass, String name, boolean isAbsolute) {
ClassLoader loader = (fromClass == null) ? null : fromClass.getClassLoader();
if (sys_paths == null) {
usr_paths = initializePath("java.library.path");
sys_paths = initializePath("sun.boot.library.path");
}
(以下省略)
ここに java.library.path
を実行時に変更しても効果がない理由がありました。初回の呼び出し時は java.library.path
が参照されて usr_paths
フィールドに代入されます。また、 sun.boot.library.path
が参照されて sys_paths
フィールドも設定されています。
2 回目以降は、 すでに sys_paths
フィールドが null ではなくなっているので、 検索パスの初期化はスキップされます。
これが、 実行時に java.library.path
プロパティーを変更しても検索パスに反映されない理由です。
ライブラリー検索パスを実行時に変更するには
ソースコードを追いかけたことで検索パスが更新されない原因が分かりました。原因が分かれば対処も簡単ですね。
対処方法は 2 つあります。
1. sys_pathsをnullにする方法
簡単なのは sys_paths
フィールドを null にするという方法です。ソースコードを見れば分かる通り、 sys_paths
フィールドが null ではないからパスの初期化がスキップされています。逆に言うと、 sys_paths
フィールドが null になれば、 もう一度、 パスの初期化処理が行なわれるということになります。
sys_paths
フィールドは private なので、 書き換えるためにはリフレクションを使う必要があります。
java.library.path
を変更して、 検索パスに反映させる完全なコードは以下のようになります。
// java.library.path を変更します。(この時点では反映されません)
System.setProperty("java.library.path", "C:/mylibs");
// sys_paths フィールドに null を代入します。
// これで次にライブラリーをロードするときに最新の java.library.path が参照されます。
Field sys_paths = ClassLoader.class.getDeclaredField("sys_paths");
sys_paths.setAccessible(true);
sys_paths.set(null, null);
2. usr_pathsを直接変更する方法
もうひとつの方法として usr_paths
フィールドを直接変更する方法があります。
usr_paths = initializePath("java.library.path");
このコードが初回しか実行されないのが原因なわけですから、 直接 usr_paths
に検索パスを設定してしまえばいいということになります。
こちらは sys_paths
を null にして再処理を促す方法に比べて少し複雑です。usr_paths
の現在の値を取得して、 そこに新たなパスを足して再設定するという流れになります。
完全なコード例は以下のようになります。
String newPath = "C:/mylibs";
Field usr_paths = ClassLoader.class.getDeclaredField("usr_paths");
usr_paths.setAccessible(true);
// 現在の検索パスを取得します。
String[] paths = (String[])usr_paths.get(null);
// すでに検索パスに含まれていたら何もせずに復帰します。
for(String path : paths) {
if(path.equals(newPath)) {
return;
}
}
// 検索パスを追加した配列を新しく作成して usr_paths に代入します。
String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
newPaths[newPaths.length - 1] = newPath;
usr_paths.set(null, newPaths);
java.library.path
を実行時に変更して、 ライブラリーの検索パスに反映させる方法は以上です。