模組與 ServiceLoader
September 28, 2022你在 cc.openhome.api
模組中定義了個 Player
介面:
package cc.openhome.api;
public interface Player {
void play(String video);
}
顯式依賴
在 cc.openhome.impl
模組中,有個 ConsolePlayer
實作了該介面:
package cc.openhome.impl;
import cc.openhome.api.Player;
public class ConsolePlayer implements Player {
@Override
public void play(String video) {
System.out.println("正在播放 " + video);
}
}
現在 cc.openhome
模組中,想要使用 Player
介面,並搭配某個實作品,若不想綁定在特定的實作品上,運用反射是其中一種方式:
package cc.openhome;
import cc.openhome.api.Player;
import java.util.Scanner;
public class MediaMaster {
public static void main(String[] args) throws ReflectiveOperationException {
String playerImpl = System.getProperty("cc.openhome.PlayerImpl");
Player player = (Player) Class.forName(playerImpl)
.getDeclaredConstructor().newInstance();
System.out.print("輸入想播放的影片:");
player.play(new Scanner(System.in).nextLine());
}
}
雖然程式碼上沒有依賴在實作品,然而,為了要能對 cc.openhome.impl
模組中的 cc.openhome.impl.ConsolePlayer
進行反射,cc.openhome
模組的 module-info.java 中必須撰寫 requires cc.openhome.impl
,這就使得 cc.openhome
模組顯式依賴在 cc.openhome.impl
模組。
使用 ServiceLoader
你可以使用 java.util.ServiceLoader
來解決這個問題,首先,可以在 cc.openhome.api
模組中新增一個 PlayerProvider
定義:
package cc.openhome.api;
import java.util.Optional;
import java.util.ServiceLoader;
public interface PlayerProvider {
Player player();
public static Player providePlayer() {
return ServiceLoader.load(PlayerProvider.class)
.findFirst()
.orElseThrow(() -> new RuntimeException("沒有服務提供者"))
.player();
}
}
ServiceProvider
會尋找各模組中,是否有 PlayerProvider
的具體實作,並運用反射建立實例,然而基於效率與定義上的清晰起見,必須在 cc.openhome.api
模組的 module-info.java 中,使用 uses
來設定這個模組會使用哪個介面提供服務:
module cc.openhome.api {
exports cc.openhome.api;
uses cc.openhome.api.PlayerProvider;
}
模組描述檔中允許 import
語句,必要時也可以如下撰寫:
import cc.openhome.api.PlayerProvider;
module cc.openhome.api {
exports cc.openhome.api;
uses PlayerProvider;
}
接著在 cc.openhome.impl
模組中,新增一個 ConsolePlayerProvider
實作 PlayerProvider
,以提供具體的 Player
實例:
package cc.openhome.impl;
import cc.openhome.api.Player;
import cc.openhome.api.PlayerProvider;
public class ConsolePlayerProvider implements PlayerProvider {
@Override
public Player player() {
return new ConsolePlayer();
}
}
基於效率與定義上的清晰起見,Java 模組系統會掃描模組中具有 provides
語句的模組,看看是否有符合 uses
指定的 API 實作,因此在 cc.openhome.impl
模組的 module-info.java 中,必須使用 privides
設定此模組為 cc.openhome.api.PlayerProvider
提供的實作類別:
module cc.openhome.impl {
requires cc.openhome.api;
provides cc.openhome.api.PlayerProvider
with cc.openhome.impl.ConsolePlayerProvider;
}
類似地,也可以使用 import
語句來讓 provides
的部份更簡潔:
import cc.openhome.api.PlayerProvider;
import cc.openhome.impl.ConsolePlayerProvider;
module cc.openhome.impl {
requires cc.openhome.api;
provides PlayerProvider with ConsolePlayerProvider;
}
在這樣的設定之下,cc.openhome.api
模組也沒有依賴在 cc.openhome.impl
模組,至於 cc.openhome
模組也不用 requires cc.openhome.impl
模組,只要使用以下的程式碼就可以了:
package cc.openhome;
import cc.openhome.api.Player;
import cc.openhome.api.PlayerProvider;
import java.util.Scanner;
public class MediaMaster {
public static void main(String[] args) throws ReflectiveOperationException {
Player player = PlayerProvider.providePlayer();
System.out.print("輸入想播放的影片:");
player.play(new Scanner(System.in).nextLine());
}
}
Java SE API 中實際例子就是 java.sql.Driver
,察看 java.sql
模組的 module-info.java 定義,就可以看到以下的內容:
module java.sql {
requires transitive java.logging;
requires transitive java.xml;
exports java.sql;
exports javax.sql;
exports javax.transaction.xa;
uses java.sql.Driver;
}
ServiceLoader
是從 JDK6 開始就存在的API,在基於類別路徑的情境下,也可以使用 ServiceLoader
, 以便為服務提供可抽換的實作,又不用接觸反射的細節,方式是在服務實作的 JAR 中 META-INF/services 資料夾,放入與服務 API 類別全名相同名稱的檔案,當中寫入實作品的類別全名。
基於相容性,服務實作的 JAR 中 META-INF/services 資料夾,若有這樣的檔案,而 JAR 被放在模組路徑中成為自動模組,那就等同於使用了 provides
語句,而服務 API 的 JAR 若被放在模組路徑中成為自動模組,等同於可使用任何可取得的 API 服務。
更多詳情可查看〈ServiceLoader 的 API 文件〉說明。