模組與 ServiceLoader


你在 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 模組。

你可以使用 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 文件〉說明。