S-JIS[2007-01-05/2015-07-28] 変更履歴
Javaのアーカイブファイル。(Java Archiveを縮めてjar)
複数のclassファイルを圧縮して1つのアーカイブにまとめるので、配布するのに便利。
|
classesというディレクトリの中を圧縮し、test.jarというjarファイルを生成するには、以下のようにする。
C:\>cd temp C:\temp>tree フォルダ パスの一覧 ボリューム シリアル番号は 00008XXX BYYY:9ZZZ です C:. ├─classes │ └─jp │ └─hishidama │ └─example └─src └─jp └─hishidama └─example C:\temp>jar cf test.jar -C classes .
jarコマンドのオプションの「-C classes」でclasses直下に移動し、「.」でその場所(およびサブディレクトリ全て)を指定している。
生成されたtest.jarには、「META-INF」というディレクトリと、その中に「MANIFEST.MF」というファイル(マニフェストファイル)が勝手に追加される。
(ここで「-C classes/」というようにスラッシュを付けると使えないjarファイルになるので注意。[2008-12-20])
何度も生成を実行するなら、build.xmlを作っておいてantで実行するのも便利。
なお、jarファイルの中に(圧縮したままの)jarファイルを指定することは出来ない。[2009-01-15]
test.jarというjarファイルの内容を表示するには、以下のようにする。
>jar tf test.jar
また、圧縮形式としてはzipなので、ZIPを扱えるツールで中を見ることも出来る。
Windowsなら解凍ツールが拡張子に連動して
いることが多いだろうから、拡張子にzipを付加してやればよい。
>copy test.jar test.jar.zip >test.jar.zip
test.jarというjarファイルを解凍するには、以下のようにする。
>jar xf test.jar
要するに、jarコマンドはtarコマンドと同じ形式をしている。
なお、jarコマンドはjavacコマンドと同じディレクトリに入っている。
test.jarというjarファイルの中のクラスを実行するには、以下のようにする。
>java -cp jarファイル パッケージ.クラス
>java -cp test.jar jp.hishidama.example.Hello
jarファイル生成時の-Cオプションによるディレクトリー指定で末尾に「/」を付けると、生成のされ方がおかしくなる。[2008-12-20]
(生成のされ方がおかしいというより、クラスをロードできる形にならない)
| 普通(通常) | 変(異常) | |
|---|---|---|
C:\temp>jar cf test.jar -C classes . C:\temp>jar cf test.jar -C classes jp |
C:\temp>jar cf test.jar -C classes/ . |
C:\temp>jar cf test.jar -C classes/ jp |
C:\temp>jar tf test.jar META-INF/ META-INF/MANIFEST.MF jp/ jp/hishidama/ jp/hishidama/Example.class |
C:\temp>jar tf test.jar META-INF/ META-INF/MANIFEST.MF classes/./ classes/./jp/ classes/./jp/hishidama/ classes/./jp/hishidama/Example.class |
C:\temp>jar tf test.jar META-INF/ META-INF/MANIFEST.MF classes/jp/ classes/jp/hishidama/ classes/jp/hishidama/Example.class |
C:\temp>java -cp test.jar jp.hishidama.Example example! |
>java -cp test.jar jp.hishidama.Example Exception in thread "main" java.lang.NoClassDefFoundError : jp/hishidama/Example >java -cp test.jar classes/jp.hishidama.Example Exception in thread "main" java.lang.NoClassDefFoundError : classes/jp/hishidama/Example >java -cp test.jar classes/./jp.hishidama.Example Exception in thread "main" java.lang.NoClassDefFoundError : classes///jp/hishidama/Example |
>java -cp test.jar jp.hishidama.Example Exception in thread "main" java.lang.NoClassDefFoundError : jp/hishidama/Example >java -cp test.jar classes/jp.hishidama.Example xception in thread "main" java.lang.NoClassDefFoundError : classes/jp/hishidama/Example (wrong name: jp/hishidama/Example) at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(Unknown Source) |
アーカイブ作成時のjarコマンドの末尾には、圧縮対象ファイル(ディレクトリー)を列挙する。
「.」なら、その位置にあるファイルとディレクトリー全てが対象となる。つまり「classes/jp/〜」と「classes/com/〜」があれば、jpとcomの両方がアーカイブされる。
しかし「jp」という指定なら、jpだけがアーカイブされ、その他は対象にならない。
jarファイル内に実行するクラスを指定しておくことで、実行を簡単にすることが出来る。[2007-01-09]
この形式のjarファイルは
以下のようにして実行できる。
>java -cp hello.jar jp.hishidama.example.Hello …通常の実行方法 >java -jar hello.jar …jarファイル内部で指定されているクラスが実行される
また、Windowsの場合は、直接jarファイルをダブルクリックすることで実行することが出来るようになる。
すなわち、以下のようにコマンドラインからファイルを直接実行することも出来る。
>hello.jar
ただし これは「javaw -jar hello.jar」が裏で実行されているだけ。
すなわち(javaでなくjavawだから)ウィンドウが開かないので、System.out.println()等でコンソール入出力をしているプログラムは何も表示されない。だから
それしかやってないプログラムは、動いたかどうか確認できないぞ(爆)
なお、実行可能でないjarファイルを上記のように実行しようとすると、エラーが発生して実行できない。
>java -jar test.jar Failed to load Main-Class manifest attribute from test.jar
実行可能jarファイルを作るには、jarファイル内のマニフェストファイルに実行するクラス(メインクラス
:Main-Class)を指定する。
これには、jarファイルを作る際に追加用のマニフェストファイルを用意し、jarコマンドのオプションでそのファイルを指定する。
>type mani.mf Main-Class: jp.hishidama.example.Hello
マニフェストファイルの名前は何でもいい。jarファイルが作られる際には自動的にMETA-INF/MANIFEST.MFという名前のファイルが作られ、自分で指定したマニフェストファイルの内容がMANIFEST.MFに追加される。
マニフェストファイルを書く際には以下のような注意点がある。
属性名: 値」という形式で書く。コロンの直後にはスペースが必要。スペースが無いと生成時にエラーになる。jarファイルの生成時に、jarコマンドにオプション「m」を追加してマニフェストファイルを指定する。
>jar cmf mani.mf hello.jar -C classes . または >jar cfm hello.jar mani.mf -C classes .
オプション(m,f)の順序とその後のファイル名指定(mani.mf, hello.jar)の順序を一致させる。
ちなみに、antのjarタスクなら、マニフェストファイルを用意しなくても実行するクラスを指定することが出来る。
jarコマンドでも、JDK1.6からは コマンドの引数で実行するクラス(エントリーポイント)を指定できるようになった。[2008-08-01]
>jar cfe hello.jar jp.hishidama.example.Hello -C classes .
javaコマンドの-jarオプションを指定すると、-classpath(-cp)は無視される。[2009-01-15]
Execクラスがexec.jarに入っていて、それを呼び出すCallクラスがcall.jar(実行可能jarファイル)に入っている場合、
>java -classpath call.jar;exec.jar jp.hishidama.example.Call hello, jar! >java -classpath exec.jar -jar call.jar Exception in thread "main" java.lang.NoClassDefFoundError: jp/hishidama/example/Exec at jp.hishidama.example.Call.main(Call.java:6)
-classpathのみを指定して実行すると大丈夫だが、-jarを使うとダメ。
これは、javaコマンドの仕様なんだそうだ。
SunのJavaアプリケーション起動ツールに「このオプションを使用すると、指定したJAR
ファイルがすべてのユーザークラスのソースになり、ユーザークラスパスのほかの設定は無視されます。」とある。
(参考:sardineさんのJava:
-jar と -classpath は併用できない)
依存するjarファイル(やclassesディレクトリー)を、マニフェストのClass-Pathという属性に指定する方法がある。
Main-Class: jp.hishidama.example.Call
Class-Path: exec.jar
Class-Path属性には、それを書いているjarファイルからの相対パスで“依存しているjarファイル”を指定する。
(複数のファイルを書く場合はスペース区切りで列挙する)
>dir /b call.jar exec.jar ←必要なjarファイルが同じディレクトリーに存在することの確認 >java -jar call.jar ←exec.jarをパスに書かなくても実行できる hello, jar!
しかしClass-Pathに指定する方法は、(相対パスとは言うものの、)実行側の自由度に欠ける。
「ユーザークラスパス」が無視されるだけなので、ブートクラスパスに指定するなんて方法も考えられる。
>java -Xbootclasspath/a:exec.jar -jar call.jar hello, jar!
「-Xbootclasspath/a」は、ブートクラスパスにライブラリーを追加する指定。
とは言うものの、ブートクラスパスの使い方としては誤っているような…(苦笑)
複数のパスを追加したい場合はセミコロン「;」で区切る。[2009-04-12]
スペース入りのパス名を使う場合はパス部分全体をダブルクォーテーション「"」でくくる。
>java -Xbootclasspath/a:"C:\Program Files\Java\jdk1.6.0\db\lib\derby.jar;C:\Program Files\Java\jdk1.6.0\db\lib\derbyclient.jar" -jar hoge.jar
jarファイルを、javaのプログラムの中から読み込むことが出来る。[2007-01-19/2014-04-16]
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// カレントディレクトリにあるjarファイルを指定
File file = new File(System.getProperty("user.dir"), "hello.jar");
try (JarFile jarFile = new JarFile(file)) {
Manifest manifest = jarFile.getManifest(); //マニフェストの取得
// jarファイル内のファイルとディレクトリを表示
printEntries(jarFile);
// マニフェストの内容を表示
printManifestAttributes(manifest);
// jarファイル内のファイルを読み込む
printFile(jarFile, "META-INF/MANIFEST.MF");
// マニフェストの属性取得
String className = getManifestAttribute(manifest, "JarCall-Class");
System.out.println("[JarCall-Class]=[" + className + "]");
// jarファイル内のクラスを呼び出す
callCalc(file, className);
}
}
JDK1.5では以下のようにする。[/2014-04-16]
import java.util.jar.JarFile; import java.util.jar.JarEntry;
/**
* jarファイル内のファイルとディレクトリの一覧を表示する
*
* @param jarFile jarファイル
*/
private static void printEntries(JarFile jarFile) {
System.out.println("↓JarEntry");
for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {
JarEntry entry = e.nextElement();
String dir = entry.isDirectory() ? "D" : "F";
System.out.printf("[%s]%s%n", dir, entry.getName());
}
}
実行結果:
↓JarEntry [D]META-INF/ [F]META-INF/MANIFEST.MF [D]jp/ [D]jp/hishidama/ [D]jp/hishidama/example/ [D]jp/hishidama/example/jar/ [F]jp/hishidama/example/jar/JarJikken.class
JDK1.8では、Stream<JarEntry>を返すstream()が使える。[2014-04-16]
private static void printEntries(JarFile jarFile) {
System.out.println("↓JarEntry");
jarFile.stream().forEach(entry -> {
String dir = entry.isDirectory() ? "D" : "F";
System.out.printf("[%s]%s%n", dir, entry.getName());
});
}
import java.util.jar.Attributes; import java.util.jar.Manifest;
/**
* マニフェストの内容を全て表示する
*
* @param manifest マニフェスト
*/
private static void printManifestAttributes(Manifest manifest) {
System.out.println("↓MainAttributes");
Attributes ma = manifest.getMainAttributes();
for (Iterator<Object> i = ma.keySet().iterator(); i.hasNext();) {
Object key = i.next();
String val = (String) ma.get(key);
System.out.printf("[%s]=[%s]%n", key, val);
}
}
実行結果:
↓MainAttributes [JarCall-Class]=[jp.hishidama.example.jar.JarJikken] [Created-By]=[1.4.2_13-b06 (Sun Microsystems Inc.)] [Ant-Version]=[Apache Ant 1.6.5] [Manifest-Version]=[1.0]
import java.util.jar.Attributes; import java.util.jar.Manifest;
/**
* マニフェストの属性を取得する
*
* @param manifest マニフェスト
* @param key キー
* @return 値
*/
private static String getManifestAttribute(Manifest manifest, String key) {
Attributes a = manifest.getMainAttributes();
return a.getValue(key);
}
この例では、キーに「JarCall-Class」を指定して呼び出すと「jp.hishidama.example.jar.JarJikken」が返る。
JAR ファイルの仕様に書かれているマニフェストに指定可能な属性は、java.util.jar.Attributes.Nameクラスに定数として保持されている。[2009-01-15]
したがって、そちらを使う方が便利。
import java.util.jar.Attributes; import java.util.jar.Attributes.Name;
Attributes a = manifest.getMainAttributes(); return a.getValue(Name.MAIN_CLASS);
jarファイルの圧縮形式は単なるzipなので、zipファイルとして扱える。
(JarFileやJarEntryクラスは それぞれZipFile・ZipEntryクラスを継承しているので、メソッドもそのまま使える)
/**
* zipファイル内のファイルの内容を出力する
*
* @param zipFile zipファイル
* @param name ファイル名
* @throws IOException
*/
private static void printFile(ZipFile zipFile, String name) throws IOException {
System.out.println("↓printFile");
ZipEntry ze = zipFile.getEntry(name);
// テキストファイルとして読み込む(JDK1.7 [2014-04-16])
try (BufferedReader reader = new BufferedReader(new InputStreamReader(zipFile.getInputStream(ze)))) {
for (;;) {
String text = reader.readLine();
if (text == null) {
break;
}
System.out.println(text);
}
}
}
マニフェストファイル(META-INF/MANIFEST.MF)の内容を表示した結果:
↓printFile Manifest-Version: 1.0 Ant-Version: Apache Ant 1.6.5 Created-By: 1.4.2_13-b06 (Sun Microsystems Inc.) JarCall-Class: jp.hishidama.example.jar.JarJikken
とは言え、jarファイルをわざわざzipファイルとして扱う必要もないと思う。[2014-04-16]
/**
* jarファイル内のファイルの内容を出力する
*
* @param jarFile jarファイル
* @param name ファイル名
* @throws IOException
*/
private static void printFile(JarFile jarFile, String name) throws IOException {
System.out.println("↓printFile");
JarEntry entry = jarFile.getJarEntry(name);
// テキストファイルとして読み込む
try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(entry)))) {
reader.lines().forEach(text -> { // JDK1.8
System.out.println(text);
});
}
}
JarEntryを取得するにはgetJarEntry()を使う。
getEntry()でも実質的にはJarEntryが返ってくるが、戻り値の型としてはZipEntryになっているので、JarEntryにキャストする必要がある。
getEntry()の戻り型を共変戻り値型でJarEntryにすればいいのに…と思ったが、JarFileはJDK1.2で導入されたクラスで、共変戻り値型はJDK1.5以降だった^^;
(JDK1.5用に修正。[2014-04-16])
/**
* jarファイル内のクラスを呼び出す
*
* @param file jarファイル
* @param className 呼び出すクラス名
* @throws Exception
*/
private static void callCalc(File file, String className) throws Exception {
System.out.println("↓クラスとしてロード");
URL[] urls = { file.toURI().toURL() };
ClassLoader loader = URLClassLoader.newInstance(urls);
// クラスをロード
Class<?> clazz = loader.loadClass(className);
//Class<?> clazz = Class.forName(className, true, loader); …ClassLoader#loadClass()と同じ
System.out.println(clazz);
//呼び出すメソッドは「int calc(int a, int b)」
// リフレクションを使って呼び出す実験
{
Object obj = clazz.newInstance();
Method method = clazz.getMethod("calc", int.class, int.class);
int ret = (Integer) method.invoke(obj, 12, 34);
System.out.println("リフレクション経由戻り値:" + ret);
}
// インターフェースを使って呼び出す実験
{
JarCall obj = (JarCall) clazz.newInstance();
int ret = obj.calc(12, 34);
System.out.println("インターフェース経由戻り値:" + ret);
}
}
実行結果:
↓クラスとしてロード class jp.hishidama.example.jar.JarJikken called. a=12, b=34 リフレクション経由戻り値:46 called. a=12, b=34 インターフェース経由戻り値:46
呼び出されるクラス(jarファイルの中に有る)(リフレクション専用):
package jp.hishidama.example.jar;
public class JarJikken {
public int calc(int a, int b) {
System.out.println("called. a=" + a + ", b=" + b);
return a + b;
}
}
ここでの例では、マニフェストファイルの中に呼び出すクラス名を記述し(下記のJarCall-Class属性(今回の実験用に勝手に名付けた)) 、呼び出し側ではその属性を取得している。
build.xml:
<jar basedir="classes" jarfile="hello.jar"> <manifest> <attribute name="JarCall-Class" value="jp.hishidama.example.jar.JarJikken" /> </manifest> </jar>
呼び出されるクラス(jarファイルの中に有る)(インターフェース使用):
package jp.hishidama.example.jar;
public interface JarCall {
public int calc(int a, int b);
}
package jp.hishidama.example.jar;
public class JarJikken implements JarCall {
@Overdie
public int calc(int a, int b) {
System.out.printf("called. a=%d, b=%d%n", a, b);
return a + b;
}
}
呼び出す側と呼び出される側のインターフェース(この例ではJarCall)は、当然同じ内容である必要がある。
同じ内容(パッケージ名・クラス名・メソッドのシグニチャー)でありさえすれば、そのソースが同一ライブラリー(同一jarファイル・あるいはEclipseで言えば同一プロジェクト)内にあろうが別ライブラリーにあろうが関係ない。
実行時に、jarファイルのマニフェストを取得することが出来る。[2007-11-15/2014-04-16]
try (InputStream is = this.getClass().getResourceAsStream("/META-INF/MANIFEST.MF")) { Manifest mf = new Manifest(is); Attributes a = mf.getMainAttributes(); String val = a.getValue("マニフェスト内のキー"); }
全く同じロジックで、自分がjarファイルでなくても何かしら値が取れる…デフォルトのマニフェストがあるのかな? というか、システムのjarファイルかも。
ただし上記のやり方では クラスパスに複数のjarファイルが有ると自分のjarファイルが取れるとは限らず、最初のjarファイルのマニフェストが取れちゃうみたい…。
完全にjarファイルだと分かっているなら、下記のようなやり方で無理矢理なんとか出来そう。
// 自分のクラス自身のパス(URL)を取得する
Class<?> c = this.getClass();
URL u = c.getResource(c.getSimpleName() + ".class");
String s = u.toExternalForm();
// jarファイル内の指定をマニフェストファイルに差し替える
String jar = s.substring(0, s.lastIndexOf(c.getPackage().getName().replace('.', '/')));
URL url = new URL(jar + "META-INF/MANIFEST.MF");
try (InputStream is = url.openStream()) {
Manifest mf = new Manifest(is);
//以下同じ
}
jarファイル内のファイルを示すURLは、「jar:file:/C:/example/bin/example.jar!/jp/hishidama/example/jar/Main.class」という感じ。
どうでもいいけど、全部のマニフェストファイルを取得したいなら、こうだ↓
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Enumeration<URL> urls = cl.getResources("META-INF/MANIFEST.MF");
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
System.out.println(url);
}
※クラスローダーでリソース名を指定する時は、先頭に「/」を付けない
サービスプロバイダー(Service Provider Interface:SPI)とは、直訳するとサービスの供給機能?[2009-04-14]
すなわち、具象クラスを提供する機能。
使い道としては、プラグイン開発のようなものを想定しているのだろう。
つまり、プログラム本体に対し、拡張機能(プラグイン)を別途jarファイルで提供する方式。jarファイルを差し替えれば別の機能が実現できるわけだ。
本体側はインターフェースや抽象クラスを用意し、jarファイル側でそれを継承した具象クラスを用意する。
jarファイルそのものは、ディレクトリーを決めておけば、そのディレクトリー内のファイル一覧取得は簡単に出来るので、問題なく取得できる。
問題は、プログラム本体側は、jarファイル内の具象クラス名をどうやって知るか?ということ。
たぶん一番考えられるのは、マニフェスト内に自分で属性を定義しておいて、そこに具象クラス名を書いておいてもらうこと。
あとは、具象クラス名自体を決めておくとか?(苦笑)
この、具象クラス名(実際は、そのインスタンス)を取得する機能が、サービスプロバイダー。
実現方法としては、jarファイルのMETA-INF内にservicesというディレクトリーを作り、そこに抽象クラス(インターフェース)と同名のファイル(プロバイダー構成ファイルという)を用意しておいて、その中に具象クラス名を書く。というだけ。
こうしておけば、サービスをロードするメソッドを呼ぶことにより、jarファイルで提供している具象クラスのインスタンスが取得できる。
JDK1.5では、sun.miscのServiceクラスを使ってサービスをロードする。
import java.util.Iterator; import sun.misc.Service; import jp.hishidama.example.services.MyService;
public class ServiceMain {
public static void main(String[] args) {
// jarファイルからMyServiceのインスタンスを収集する
@SuppressWarnings("unchecked")
Iterator<MyService> i = Service.providers(MyService.class);
while (i.hasNext()) {
MyService s = i.next();
String hello = s.getHello();
System.out.println(hello);
}
}
}
Service.providers()は同じ抽象クラスを指定して何度でも呼び出すことが出来るが、返ってくるインスタンスは
その都度毎回生成される。
(なお、Service.providers()はジェネリクス化されていない)
JDK1.6で、java.utilにServiceLoaderというクラスが用意された。[2015-07-28]
import java.util.Iterator; import java.util.ServiceLoader; import jp.hishidama.example.services.MyService;
public class ServiceMain {
public static void main(String[] args) {
// jarファイルからMyServiceのインスタンスを収集する
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (Iterator<MyService> i = loader.iterator(); i.hasNext();) {
MyService s = i.next();
String hello = s.getHello();
System.out.println(hello);
}
}
}
package jp.hishidama.example.services;
public interface MyService {
public String getHello();
}
package jp.hishidama.example.service1;
import jp.hishidama.example.services.MyService;
public class JapanService implements MyService {
@Override
public String getHello() {
return "こんにちは";
}
}
サービスプロバイダーによるインスタンス化では、Class#newInstance()が使われる。
つまり具象クラスにはpublicなデフォルトコンストラクター(引数無しコンストラクター)が必要。
>tree /f
フォルダ パスの一覧
ボリューム シリアル番号は BXXX-9YYY です
C:.
├─bin
│ build.xml
│
├─classes
│ └─jp
│ └─hishidama
│ └─example
│ └─service1
│ JapanService.class
│
├─META-INF
│ └─services
│ jp.hishidama.example.services.MyService
│
└─src
└─jp
└─hishidama
└─example
└─service1
JapanService.java
プロバイダー構成ファイルの置き場は、META-INF/servicesの直下。
プロバイダー構成ファイルのファイル名は、インターフェース(抽象クラス)名を完全修飾クラス名(FQCN)で表したものと全く同一にする必要がある。
つまりこの例では、ファイルはMETA-INF/services/jp.hishidama.example.services.MyService。
プロバイダー構成ファイルの中には、具象クラス名をFQCNで書く。
jp.hishidama.example.service1.JapanService
「#」で始めると行コメントになる。
改行区切りで複数のクラスを書くことも可。
UTF-8でないといけないので、もし日本語クラス名を使っている場合は要注意。
<?xml version="1.0" encoding="Shift_JIS"?> <project name="make service1.jar" basedir=".." default="make service1.jar"> <target name="make service1.jar"> <jar destfile="bin/service1.jar"> <fileset dir="." > <include name="META-INF/**/*" /> </fileset> <fileset dir="classes"> <include name="**/*.class" /> </fileset> </jar> </target> </project>
※ちなみに、META-INFをclasses直下に置いておけば、指定するfilesetはclassesのみで済む。
<fileset dir="classes"> <include name="**/*.class" /> <include name="META-INF/**/*" /> </fileset>
bin> jar -tf service1.jar META-INF/ META-INF/MANIFEST.MF jp/ jp/hishidama/ jp/hishidama/example/ jp/hishidama/example/service1/ jp/hishidama/example/service1/JapanService.class META-INF/services/ META-INF/services/jp.hishidama.example.services.MyService
classesをプログラム本体(ServiceMain)がある場所とする。
> java -cp classes;bin/service1.jar ServiceMain こんにちは
さらにservice2.jarを作って、プロバイダー構成ファイルに複数のクラス名を書けば、それも取得される。
> java -cp classes;bin/service1.jar;service2.jar ServiceMain こんにちは Hello こんちゃ!
複数取れたインスタンスのうち、どれを使うのかは、プログラム本体の作り(仕様)次第。
JDBC4.0のJDBCドライバー(DriverManager)もサービスプロバイダーの仕組みを使っている。
(DriverManagerの初期処理の中でService#providers()を呼び出している)
> jar -tf derbyclient.jar | findstr META-INF/services META-INF/services/java.sql.Driver
org.apache.derby.jdbc.ClientDriver
JDBCだと、実際には複数のドライバーが取得される場合がある。
DriverManager#getConnection()の場合、順番にドライバーインスタンスのconnect()を呼び出し、URLが最初に適合したもの(connect()によってnull以外が返ってきたコネクション)を返している。
sun.misc.Service#providers()が実際にやっていることは、(ClassLoader#getResources()を使って)同一名の全ファイルを取得するのと同様。
Iterator<Object> i = Service.providers(抽象クラス.class);
while (i.hasNext()) {
Object obj = i.next();
}
↑同様↓
Enumeration<URL> urls = cl.getResources("META-INF/services/抽象クラス名"); //プロバイダー構成ファイル
while (urls.hasMoreElements()) {
URL url = urls.nextElement(); //プロバイダー構成ファイルのURL
InputStream is = url.openStream();
//isを使ってプロバイダー構成ファイルを読み込み、クラス名を取得する
Class<?> c = 〜;
//その中に書かれているクラスをインスタンス化する
Object obj = c.newInstance();
}
したがって、jarファイル化しなくても、コンパイルしたクラスファイルを置くclasses直下に「classes/META-INF/services/プロバイダー構成ファイル」を置けば、そのまま読み込まれる。
(Eclipseの場合、ソースディレクトリーに同様の構成「src/META-INF/services/プロバイダー構成ファイル」でファイルを置いておけば自動的にclassesにコピーされるので、サービスプロバイダー機能を試すには楽かも)