Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

S-JIS[2007-01-05/2015-07-28] 変更履歴

jar(ジャー)ファイル

Javaのアーカイブファイル。(Java Archiveを縮めてjar)
複数のclassファイルを圧縮して1つのアーカイブにまとめるので、配布するのに便利。


jarファイルの作成

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]

→Sunのjar - Java ARchive ツール

何度も生成を実行するなら、build.xmlを作っておいてantで実行するのも便利。

なお、jarファイルの中に(圧縮したままの)jarファイルを指定することは出来ない。[2009-01-15]


jarファイルの内容

test.jarというjarファイルの内容を表示するには、以下のようにする。

>jar tf test.jar

また、圧縮形式としてはzipなので、ZIPを扱えるツールで中を見ることも出来る。
Windowsなら解凍ツール拡張子に連動して いることが多いだろうから、拡張子にzipを付加してやればよい。

>copy test.jar test.jar.zip
>test.jar.zip

jarファイルの解凍

test.jarというjarファイルを解凍するには、以下のようにする。

>jar xf test.jar

要するに、jarコマンドtarコマンドと同じ形式をしている。
なお、jarコマンドはjavacコマンドと同じディレクトリに入っている。


jarファイルの実行

test.jarというjarファイルの中のクラスを実行するには、以下のようにする。

>java -cp jarファイル パッケージ.クラス
>java -cp test.jar jp.hishidama.example.Hello

アプレットでjarファイルから起動する方法


異常なjarファイル

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ファイル

実行可能jarファイルの実行方法

jarファイル内に実行するクラス指定しておくことで、実行を簡単にすることが出来る。[2007-01-09]
この形式のjarファイルは 以下のようにして実行できる。

>java -cp hello.jar jp.hishidama.example.Hello	通常の実行方法
>java -jar hello.jar			jarファイル内部で指定されているクラスが実行される

-classpathとの併用について

また、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ファイルを作るには、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 .

-jarと-classpathの併用

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という属性に指定する方法がある。

call.jarのMETA-INF/MANIFEST.MF(抜粋):

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プログラム内から操作

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);
		}
	}

jarファイル内のファイルとディレクトリの一覧を取得

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ファイル内のファイルを読み込む

jarファイルの圧縮形式は単なるzipなので、zipファイルとして扱える。
JarFileJarEntryクラスは それぞれZipFileZipEntryクラスを継承しているので、メソッドもそのまま使える)

	/**
	 * 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

zipファイル内の全エントリーを順次読み込む例


とは言え、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以降だった^^;


jarファイル内のクラスをロードし、メソッドを呼び出す

(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クラスを使ってサービスをロードする。

プログラム本体側の例(JDK1.5):

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]

プログラム本体側の例(JDK1.6):

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();
}

具象クラス1(JapanService.java):

package jp.hishidama.example.service1;

import jp.hishidama.example.services.MyService;

public class JapanService implements MyService {

	@Override
	public String getHello() {
		return "こんにちは";
	}
}

サービスプロバイダーによるインスタンス化では、Class#newInstance()が使われる。
つまり具象クラスにはpublicなデフォルトコンストラクター(引数無しコンストラクター)が必要。


具象クラス1に関するファイルの配置:

>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

具象クラス1の為のプロバイダー構成ファイル

プロバイダー構成ファイルの置き場は、META-INF/servicesの直下。
プロバイダー構成ファイルのファイル名は、インターフェース(抽象クラス)名を完全修飾クラス名(FQCN)で表したものと全く同一にする必要がある。
つまりこの例では、ファイルはMETA-INF/services/jp.hishidama.example.services.MyService。
プロバイダー構成ファイルの中には、具象クラス名をFQCNで書く。

jp.hishidama.example.service1.JapanService

「#」で始めると行コメントになる。
改行区切りで複数のクラスを書くことも可。
UTF-8でないといけないので、もし日本語クラス名を使っている場合は要注意。


具象クラス1を含むjarファイルを作るAntbuild.xml

<?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-INFclasses直下に置いておけば、指定するfilesetはclassesのみで済む。

			<fileset dir="classes">
				<include name="**/*.class" />
				<include name="META-INF/**/*" />
			</fileset>

出来上がったjarファイル

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()を呼び出している)

JavaDBのJDBCドライバー(derby.jar):

> jar -tf derbyclient.jar | findstr META-INF/services
META-INF/services/java.sql.Driver

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にコピーされるので、サービスプロバイダー機能を試すには楽かも)


Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま