S-JIS[2007-01-05/2015-07-28]変更履歴
Javaのアーカイブファイル。(JavaArchiveを縮めてjar)
複数のclassファイルを圧縮して1つのアーカイブにまとめるので、配布するのに便利。
|
classesというディレクトリの中を圧縮し、test.jarというjarファイルを生成するには、以下のようにする。
C:\>cd tempC:\temp>treeフォルダ パスの一覧ボリューム シリアル番号は 00008XXX BYYY:9ZZZ ですC:.├─classes│ └─jp│ └─hishidama│ └─example└─src └─jp └─hishidama └─exampleC:\temp>jarcftest.jar -Cclasses .
jarコマンドのオプションの「-Cclasses」でclasses直下に移動し、「.」でその場所(およびサブディレクトリ全て)を指定している。
生成されたtest.jarには、「META-INF」というディレクトリと、その中に「MANIFEST.MF」というファイル(マニフェストファイル)が勝手に追加される。
(ここで「-Cclasses/」というようにスラッシュを付けると使えないjarファイルになるので注意。[2008-12-20])
何度も生成を実行するなら、build.xmlを作っておいてantで実行するのも便利。
なお、jarファイルの中に(圧縮したままの)jarファイルを指定することは出来ない。[2009-01-15]
test.jarというjarファイルの内容を表示するには、以下のようにする。
>jartftest.jarまた、圧縮形式としてはzipなので、ZIPを扱えるツールで中を見ることも出来る。
Windowsなら解凍ツールが拡張子に連動していることが多いだろうから、拡張子にzipを付加してやればよい。
>copytest.jartest.jar.zip>test.jar.zip
test.jarというjarファイルを解凍するには、以下のようにする。
>jarxftest.jar要するに、jarコマンドはtarコマンドと同じ形式をしている。
なお、jarコマンドはjavacコマンドと同じディレクトリに入っている。
test.jarというjarファイルの中のクラスを実行するには、以下のようにする。
>java-cpjarファイル パッケージ.クラス>java-cptest.jar jp.hishidama.example.Hellojarファイル生成時の-Cオプションによるディレクトリー指定で末尾に「/」を付けると、生成のされ方がおかしくなる。[2008-12-20]
(生成のされ方がおかしいというより、クラスをロードできる形にならない)
| 普通(通常) | 変(異常) | |
|---|---|---|
C:\temp>jarcftest.jar -Cclasses . C:\temp>jarcftest.jar -Cclasses jp | C:\temp>jarcftest.jar -Cclasses/ . | C:\temp>jarcftest.jar -Cclasses/ jp |
C:\temp>jartftest.jarMETA-INF/META-INF/MANIFEST.MFjp/jp/hishidama/jp/hishidama/Example.class | C:\temp>jartftest.jarMETA-INF/META-INF/MANIFEST.MFclasses/./classes/./jp/classes/./jp/hishidama/classes/./jp/hishidama/Example.class | C:\temp>jartftest.jarMETA-INF/META-INF/MANIFEST.MFclasses/jp/classes/jp/hishidama/classes/jp/hishidama/Example.class |
C:\temp>java-cptest.jar jp.hishidama.Exampleexample! | >java-cptest.jar jp.hishidama.ExampleException in thread "main"java.lang.NoClassDefFoundError: jp/hishidama/Example >java-cptest.jarclasses/jp.hishidama.ExampleException in thread "main"java.lang.NoClassDefFoundError:classes/jp/hishidama/Example >java-cptest.jarclasses/./jp.hishidama.ExampleException in thread "main"java.lang.NoClassDefFoundError:classes///jp/hishidama/Example | >java-cptest.jar jp.hishidama.ExampleException in thread "main"java.lang.NoClassDefFoundError: jp/hishidama/Example >java-cptest.jarclasses/jp.hishidama.Examplexception 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 -cphello.jar jp.hishidama.example.Hello…通常の実行方法>java-jarhello.jar…jarファイル内部で指定されているクラスが実行される
また、Windowsの場合は、直接jarファイルをダブルクリックすることで実行することが出来るようになる。
すなわち、以下のようにコマンドラインからファイルを直接実行することも出来る。
>hello.jar
ただし これは「javaw -jarhello.jar」が裏で実行されているだけ。
すなわち(javaでなくjavawだから)ウィンドウが開かないので、System.out.println()等でコンソール入出力をしているプログラムは何も表示されない。だから それしかやってないプログラムは、動いたかどうか確認できないぞ(爆)
なお、実行可能でないjarファイルを上記のように実行しようとすると、エラーが発生して実行できない。
>java -jartest.jarFailed to load Main-Class manifest attribute fromtest.jar
実行可能jarファイルを作るには、jarファイル内のマニフェストファイルに実行するクラス(メインクラス:Main-Class)を指定する。
これには、jarファイルを作る際に追加用のマニフェストファイルを用意し、jarコマンドのオプションでそのファイルを指定する。
>typemani.mfMain-Class: jp.hishidama.example.Hello
マニフェストファイルの名前は何でもいい。jarファイルが作られる際には自動的にMETA-INF/MANIFEST.MFという名前のファイルが作られ、自分で指定したマニフェストファイルの内容がMANIFEST.MFに追加される。
マニフェストファイルを書く際には以下のような注意点がある。
属性名: 値」という形式で書く。コロンの直後にはスペースが必要。スペースが無いと生成時にエラーになる。jarファイルの生成時に、jarコマンドにオプション「m」を追加してマニフェストファイルを指定する。
>jar cmfmani.mfhello.jar -Cclasses .または>jar cfmhello.jarmani.mf -Cclasses .
オプション(m,f)の順序とその後のファイル名指定(mani.mf, hello.jar)の順序を一致させる。
ちなみに、antのjarタスクなら、マニフェストファイルを用意しなくても実行するクラスを指定することが出来る。
jarコマンドでも、JDK1.6からは コマンドの引数で実行するクラス(エントリーポイント)を指定できるようになった。[2008-08-01]
>jar cfehello.jarjp.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.Callhello, jar!>java-classpath exec.jar-jar call.jarException in thread "main" java.lang.NoClassDefFoundError: jp/hishidama/example/Execat 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.CallClass-Path: exec.jarClass-Path属性には、それを書いているjarファイルからの相対パスで“依存しているjarファイル”を指定する。
(複数のファイルを書く場合はスペース区切りで列挙する)
>dir /bcall.jarexec.jar←必要なjarファイルが同じディレクトリーに存在することの確認>java -jar call.jar←exec.jarをパスに書かなくても実行できるhello, jar!
しかしClass-Pathに指定する方法は、(相対パスとは言うものの、)実行側の自由度に欠ける。
「ユーザークラスパス」が無視されるだけなので、ブートクラスパスに指定するなんて方法も考えられる。
>java-Xbootclasspath/a:exec.jar -jar call.jarhello, 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ファイルを指定Filefile = new File(System.getProperty("user.dir"), "hello.jar");try (JarFilejarFile = newJarFile(file)) {Manifestmanifest =jarFile.getManifest(); //マニフェストの取得// jarファイル内のファイルとディレクトリを表示printEntries(jarFile);// マニフェストの内容を表示printManifestAttributes(manifest);// jarファイル内のファイルを読み込むprintFile(jarFile, "META-INF/MANIFEST.MF");// マニフェストの属性取得StringclassName =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 jarFilejarファイル */private static voidprintEntries(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 voidprintEntries(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 voidprintManifestAttributes(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 StringgetManifestAttribute(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 zipFilezipファイル * @param nameファイル名 * @throws IOException */private static voidprintFile(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)の内容を表示した結果:
↓printFileManifest-Version: 1.0Ant-Version: Apache Ant 1.6.5Created-By: 1.4.2_13-b06 (Sun Microsystems Inc.)JarCall-Class: jp.hishidama.example.jar.JarJikken
とは言え、jarファイルをわざわざzipファイルとして扱う必要もないと思う。[2014-04-16]
/** * jarファイル内のファイルの内容を出力する * * @param jarFilejarファイル * @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.8System.out.println(text);});}}JarEntryを取得するにはgetJarEntry()を使う。
getEntry()でも実質的にはJarEntryが返ってくるが、戻り値の型としてはZipEntryになっているので、JarEntryにキャストする必要がある。
getEntry()の戻り型を共変戻り値型でJarEntryにすればいいのに…と思ったが、JarFileはJDK1.2で導入されたクラスで、共変戻り値型はJDK1.5以降だった^^;
(JDK1.5用に修正。[2014-04-16])
/** * jarファイル内のクラスを呼び出す * * @param filejarファイル * @param className呼び出すクラス名 * @throws Exception */private static voidcallCalc(File file, String className) throws Exception {System.out.println("↓クラスとしてロード");URL[] urls = { file.toURI().toURL() };ClassLoaderloader =URLClassLoader.newInstance(urls);// クラスをロードClass<?> clazz =loader.loadClass(className);//Class<?> clazz = Class.forName(className, true,loader);…ClassLoader#loadClass()と同じSystem.out.println(clazz);//呼び出すメソッドは「intcalc(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);}//インターフェースを使って呼び出す実験{JarCallobj = (JarCall) clazz.newInstance();int ret = obj.calc(12, 34);System.out.println("インターフェース経由戻り値:" + ret);}}実行結果:
↓クラスとしてロードclass jp.hishidama.example.jar.JarJikkencalled. a=12, b=34リフレクション経由戻り値:46called. a=12, b=34インターフェース経由戻り値:46
呼び出されるクラス(jarファイルの中に有る)(リフレクション専用):
package jp.hishidama.example.jar;public class JarJikken {public intcalc(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 interfaceJarCall {public intcalc(int a, int b);}package jp.hishidama.example.jar;public class JarJikkenimplementsJarCall {@Overdiepublic intcalc(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 (InputStreamis = this.getClass().getResourceAsStream("/META-INF/MANIFEST.MF")) {Manifest mf = newManifest(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 (InputStreamis = url.openStream()) {Manifest mf = newManifest(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 classServiceMain {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 classServiceMain {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 interfaceMyService {public StringgetHello();}package jp.hishidama.example.service1;import jp.hishidama.example.services.MyService;public classJapanService implementsMyService {@Overridepublic StringgetHello() {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 └─service1JapanService.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.jarMETA-INF/META-INF/MANIFEST.MFjp/jp/hishidama/jp/hishidama/example/jp/hishidama/example/service1/jp/hishidama/example/service1/JapanService.classMETA-INF/services/META-INF/services/jp.hishidama.example.services.MyService
classesをプログラム本体(ServiceMain)がある場所とする。
> java -cp classes;bin/service1.jarServiceMainこんにちは
さらにservice2.jarを作って、プロバイダー構成ファイルに複数のクラス名を書けば、それも取得される。
> java -cp classes;bin/service1.jar;service2.jarServiceMainこんにちはHelloこんちゃ!
複数取れたインスタンスのうち、どれを使うのかは、プログラム本体の作り(仕様)次第。
JDBC4.0のJDBCドライバー(DriverManager)もサービスプロバイダーの仕組みを使っている。
(DriverManagerの初期処理の中でService#providers()を呼び出している)
>jar -tfderbyclient.jar |findstr META-INF/servicesMETA-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();//プロバイダー構成ファイルのURLInputStream is = url.openStream();//isを使ってプロバイダー構成ファイルを読み込み、クラス名を取得するClass<?> c = 〜;//その中に書かれているクラスをインスタンス化するObject obj = c.newInstance();}したがって、jarファイル化しなくても、コンパイルしたクラスファイルを置くclasses直下に「classes/META-INF/services/プロバイダー構成ファイル」を置けば、そのまま読み込まれる。
(Eclipseの場合、ソースディレクトリーに同様の構成「src/META-INF/services/プロバイダー構成ファイル」でファイルを置いておけば自動的にclassesにコピーされるので、サービスプロバイダー機能を試すには楽かも)