Java 9 揭秘(5. 實現服務)


Tips
做一個終身學習的人。

Java 9
Implementing Services

在這章中,主要介紹如下內容:

  • 什么服務,服務接口,服務提供者;
  • 在 JDK 9之前和在JDK 9中如何實現服務
  • 如何使用Java接口作為服務實現
  • 如何使用ServiceLoader 類加載服務提供者
  • 如何在模塊聲明中使用uses語句來指定當前模塊使用ServiceLoader類加載的服務接口
  • 如何使用provides語句指定當前模塊為服務接口提供的服務提供者
  • 如何發現,過濾和選擇基於他們的類的服務提供者,而不實例化它們
  • 如何在JDK 9之前的版本打包服務提供者

一. 什么是服務

應用程序(或類庫)提供的特定功能稱為服務。 例如,可以有不同的類庫提供一個數字是否是素數的服務,可以檢查數字是否為素數,並在給定數字后生成下一個素數。

為服務提供實現的應用程序和類庫被稱為服務提供者。 使用該服務的應用程序稱為服務使用者或客戶端。 客戶端如何使用該服務? 客戶是否知道所有服務提供者? 客戶端在不知道任何服務提供商的情況下如何獲得服務?

Java SE6提供了一種機制,允許服務提供者和服務使用者之間松耦合。 也就是說,服務消費者可以使用由服務提供者提供的服務而不需要知道服務提供者。

在Java中,服務由一組接口和類定義。 該服務包含定義服務提供的功能的接口或抽象類,它被稱為服務提供者接口或簡單的服務接口。 請注意,“服務提供者接口”或“服務接口”中的“接口”一詞並不指Java中的接口構造。 它可以是Java接口或抽象類。 可以但不推薦使用具體的類作為服務接口。 有時,服務接口也稱為服務類型 ——- 用於標識服務的類型。

服務的具體實現被稱為服務提供者。 服務提供者接口可以有多個服務提供者構成。 通常,服務提供者由一些接口和類組成,以提供服務接口的實現。

JDK包含一個java.util.ServiceLoader <S>類,其唯一目的是在運行時發現並加載類型為S的服務接口的服務提供者。ServiceLoader類允許服務使用者和服務提供者之間的解耦。 服務消費者只知道服務接口; ServiceLoader類為使用者產生服務提供者的實例來實現服務接口。 下圖顯示了服務,服務提供者和服務使用者的布置的圖示。

三者之間的關系

通常,服務將使用ServiceLoader類加載所有服務提供者並使其可用於服務使用者(或客戶端)。 該架構允許一種插件機制,其中可以添加或刪除服務提供者,而不影響服務和服務使用者。 服務使用者只知道該服務接口。 他們不知道該服務接口的任何特定實現(服務提供者)。

Tips
建議閱讀java.util.ServiceLoader類的API文檔,以完整了解JDK 9提供的服務加載工具。

在本章中,使用了一個服務接口和三個服務提供者。 它們的模塊,類/接口名稱和簡要描述如下所示。

模塊 類/接口 描述
com.jdojo.prime PrimeChecker 作為服務和服務接口
com.jdojo.prime.generic GenericPrimeChecker 服務提供者
com.jdojo.prime.faster FasterPrimeChecker 服務提供者
com.jdojo.prime.probable ProbablePrimeChecker 服務提供者
com.jdojo.prime.client Main 服務使用者

圖5-2顯示了作為服務,服務提供者和服務使用者的類/接口關系,可以與上面表格進行比較。

關系圖

二. 發現服務

為了使用服務,它的提供者需要被發現和加載。 java.util.ServiceLoader類執行發現和加載服務提供程序的工作。 發現和加載服務提供者的模塊必須在其聲明中包含一個uses語句,該語句語法如下:
uses <service-interface>;

這里,<service-interface>是服務接口的名稱,它是Java接口名稱,類名稱或注解類型名稱。 如果一個模塊使用ServiceLoader <S>類加載名為S的服務接口的服務提供者的實例,則模塊聲明必須包含以下語句:

uses S;

Tip
聲明的名稱,uses似乎是一個不恰當的用語。乍一看,當前的模塊似乎會使用指定的服務。 但是,情況並非如此。 客戶端使用服務,而不是定義服務的模塊。 一個更直觀的聲明名稱可以是發現或加載。 如果讀取其定義,則可以正確地理解其含義:具有uses語句的模塊使用ServiceLoader類加載此服務接口的服務提供者。 不需要在客戶端模塊中使用uses語句,除非客戶端模塊加載服務的服務提供者。 客戶端模塊加載服務是不常見的。

模塊可以發現並加載多個服務接口。 以下模塊聲明使用兩個uses語句,表示它將發現並加載類型為com.jdojo.PrimeCheckercom.jdojo.CsvParser的兩個服務接口:

module com.jdojo.loader {
    uses com.jdojo.PrimeChecker;
    uses com.jdojo.CsvParser:
    // Other module statements go here
}

模塊聲明允許導入語句,為了更好的可讀性,可以如下重寫此模塊聲明:

// Import types from other packages
import com.jdojo.PrimeChecker;
import com.jdojo.CsvParser:
module com.jdojo.loader {
    uses PrimeChecker;
    uses CsvParser:
    // Other module statements go here
}

uses語句中指定的服務接口可以在當前模塊或另一個模塊中聲明。 如果在另一個模塊中聲明,則服務接口必須能夠訪問當前模塊中的代碼。 否則,會發生編譯時錯誤。 例如,在上面聲明中的uses語句中使用的com.jdojo.CsvParser服務接口可以在com.jdojo.loader模塊或另一個模塊中聲明,例如com.jdojo.csvutil。 在后一種情況下,com.jdojo.CsvParser接口必須可以訪問com.jdojo.loader模塊。

服務提供者發現在運行時動態發生。 發現服務提供者的模塊通常不(並且不需要)聲明對服務提供者模塊的編譯時依賴性,因為不可能提前知道所有提供者模塊。 服務發現者模塊不聲明對服務提供者模塊依賴的另一個原因是保持服務提供者和服務消費者解耦。

三. 提供服務實現

為服務接口提供實現的模塊必須包含一個provides 語句。
如果模塊包含服務提供者,但在其聲明中不包含provides 語句,則該服務提供者將不會通過ServiceLoader類加載。 也就是說,模塊聲明中的provides語句是一種告訴ServiceLoader類的方法,“嘿! 我提供了一個服務的實現。 你可以在需要該服務時使用我作為提供者”。提供語句的語法如下:

provides <service-interface> with <service-implementation-name>;

這里,provides子句指定服務接口的名稱,with子句指定實現服務提供者接口的類的名稱。 在JDK 9中,服務提供者可以將接口指定為服務接口的實現。 這可能聽起來不正確,但它是真的。 下面提供了一個接口作為服務提供者實現類型的例子。 以下模塊聲明包含兩個provides 的語句:

module com.jdojo.provider {
    provides com.jdojo.PrimeChecker with com.jdojo.impl.PrimeCheckerFactory;
    provides com.jdojo.CsvParser with com.jdojo.impl.CsvFastParser;
    // Other module statements go here
}

第一個provides 語句聲明com.jdojo.impl.PrimeCheckerFactory是名為com.jdojo.PrimeChecker的服務接口的一個可能的實現。 第二個provides語句聲明com.jdojo.impl.CsvFastParser是名為com.jdojo.CsvParser的服務接口的一個可能的實現。 在JDK 9之前,PrimeCheckerFactoryCsvParser必須是類。 在JDK 9中,它們可以是類或接口。

模塊可以包含usesprovides 語句的組合 —— 相同的模塊可以為服務提供實現並發現相同的服務; 它只能為一個或多個服務提供實現,或者它可以為一個服務提供實現並發現另一種類型的服務。 以下模塊聲明發現並提供相同服務的實現:

module com.jdojo.parser {
    uses com.jdojo.XmlParser;
    provides com.jdojo.XmlParser with com.jdojo.xml.impl.XmlParserFactory;
    // Other module statements go here
}

為了更好的可讀性,可以使用import語句重寫上一個模塊聲明:

import com.jdojo.XmlParser;
import com.jdojo.xml.impl.XmlParserFactory
module com.jdojo.parser {
    uses XmlParser;
    provides XmlParser with XmlParserFactory;
    // Other module statements go here
}

Tips
必須在當前模塊中聲明在provides語句的with子句中指定的服務實現類/接口。 否則,會發生編譯時錯誤。

ServiceLoader類創建服務實現的實例。 當服務實現是一個接口時,它只是加載並返回接口引用。 服務實現(類或接口)必須遵循以下規則:

  • 如果服務實現隱式或顯式地聲明沒有形式參數的公共構造函數,則該構造函數稱為提供者構造函數。
  • 如果服務實現包含一個沒有形式參數的的public static修飾的provider方法,則該方法稱為提供者方法。
  • provider方法的返回類型必須是服務接口類型或其子類。
  • 如果服務實現不包含provider方法,則服務實現的類型必須是具有提供者構造函數的類,並且類必須是服務接口類型或其子類。

ServiceLoader類請求發現和加載服務提供者時,它檢查服務實現是否包含provider方法。 如果找到provider方法,則返回的方法值是ServiceLoader類返回的服務。 如果沒有找到provider方法,它將使用提供者構造函數實例化服務實現。 如果服務實現既不包含provider方法也不包含提供者構造函數,則會發生編譯時錯誤。

通過這些規則,可以使用Java接口作為服務實現。 該接口應該有一個名為provider的公共靜態方法,它返回一個服務接口類型的實例。

四. 定義服務接口

在本節中,開發一個稱為PrimeChecker的服務。 此服務的要求如下:

  • 該服務應提供一個API來檢查一個數字是否是素數。
  • 客戶應該能夠知道服務提供者的名稱。 也就是說,每個服務提供者都應該能夠指定其名稱。
  • 客戶端應該能夠檢索服務實例而不用指定服務提供者的名稱。 在這種情況下,返回由ServiceLoader類找到的第一個服務提供者。 如果沒有找到服務提供者,則拋出RuntimeException異常。
  • 客戶端應該能夠通過指定服務提供者名稱來檢索服務實例。 如果具有指定名稱的服務提供者不存在,則拋出RuntimeException異常。

服務提供的功能將由名為PrimeChecker的接口表示。 它包含兩種方法:

public interface PrimeChecker {
    String getName();
    boolean isPrime(long n);
}

getName()方法返回服務提供者的名字。 如果指定的參數是素數,則isPrime()方法返回true,否則返回false。 所有服務提供商都將實現PrimeChecker接口。 PrimeChecker接口是我們的服務接口(或服務類型)。

該服務需要向客戶端提供API以檢索服務提供者的實例。 在客戶端檢索之前,服務需要發現和加載所有服務提供者。 服務提供者使用ServiceLoader類加載。 該類沒有公共構造函數。 可以使用其一個load()方法來獲取其實例來加載特定類型的服務。 需要傳遞服務提供者接口的類。 ServiceLoader類包含一個iterator()方法,該方法返回一個為該ServiceLoader加載的特定服務接口的所有服務提供者迭代器。 以下代碼顯示了如何加載和遍歷PrimeChecker的所有服務提供者實例:

// Load the service providers for PrimeChecker
ServiceLoader<PrimeChecker> loader = ServiceLoader.load(PrimeChecker.class);
// Iterate through all service provider instances
Iterator<PrimeChecker> iterator = loader.iterator();
if(iterator.hasNext()) {
   PrimeChecker checker = iterator.next();
   // Use the prime checker here...
}

在JDK 8之前,必須創建一個類來為服務提供發現,加載和檢索功能。 從JDK 8開始,可以向接口添加靜態方法。現在為服務接口添加兩個靜態方法:

public interface PrimeChecker {
    String getName();
    boolean isPrime(long n);
    static PrimeChecker newInstance();
    static PrimeChecker newInstance(String providerName)
 }

newInstance()方法返回首先找到的PrimeChecker的一個實例。 另一個版本將返回具有指定提供程序名稱的服務提供程序的實例。

創建一個名為com.jdojo.prime的模塊。 下面顯示了PrimeChecker接口的完整代碼。

// PrimeChecker.java
package com.jdojo.prime;
import java.util.ServiceLoader;
public interface PrimeChecker {
    /**
     * Returns the service provider name.
     *
     * @return The service provider name
     */
    String getName();
    /**
     * Returns true if the specified number is a prime, false otherwise.
     *
     * @param n The number to be check for being prime
     * @return true if the specified number is a prime, false otherwise.
     */
    boolean isPrime(long n);
    /**
     * Returns the first PrimeChecker service provider found.
     *
     * @return The first PrimeChecker service provider found.
     * @throws RuntimeException When no PrimeChecker service provider is found.
     */
    static PrimeChecker newInstance() throws RuntimeException {
        return ServiceLoader.load(PrimeChecker.class)
                            .findFirst()
                            .orElseThrow(() -> new RuntimeException(
                              "No PrimeChecker service provider found."));
    }
    /**
     * Returns a PrimeChecker service provider instance by name.
     *
     * @param providerName The prime checker service provider name
     * @return A PrimeChecker
     */
    static PrimeChecker newInstance(String providerName) throws RuntimeException {
        ServiceLoader<PrimeChecker> loader = ServiceLoader.load(PrimeChecker.class);
        for (PrimeChecker checker : loader) {
            if (checker.getName().equals(providerName)) {
                return checker;
            }
        }
        throw new RuntimeException("A PrimeChecker service provider with the name '"
                + providerName + "' was not found.");
    }
}

下面顯示了com.jdojo.prime模塊的聲明。 它導出com.jdojo.prime包,因為PrimeChecker接口需要由服務提供者模塊和客戶端模塊使用。

// module-info.java
module com.jdojo.prime {
    exports com.jdojo.prime;
    uses com.jdojo.prime.PrimeChecker;
}

需要使用具有PrimeChecker接口的完全限定名稱的uses語句,因為此模塊中的代碼(此接口中的newInstance()方法)將使用ServiceLoader類加載此接口的服務提供程序。 如果要在uses語句中使用簡單的名稱,請添加相應的import語句,如上一節所示。

五. 定義服務提供者

在接下來的兩節中,為PrimeChecker服務接口創建兩個服務提供者。 第一個服務提供者實現一個通用的素數檢查,而第二個將實施一個更快的素數檢查。 稍后,將創建一個客戶端來測試服務。 可以選擇使用其中一個服務提供者或兩者。

這些服務提供者將執行算法來檢查給定的數字是否為素數。 素數的定義:一個大於1的自然數,除了1和它自身外,不能被其他自然數整除的數叫做質數。 1不是素數。 素數的幾個例子是2,3,5,7和11。

1. 定義一個通用的素數服務提供者

在本節中,將為PrimeChecker服務定義一個通用服務提供者。 定義服務的服務提供者只是創建一個實現服務接口的類,或創建一個具有提供方法的接口。 在這種情況下,創建一個名為GenericPrimeChecker的類,該類實現PrimeChecker接口,並包含一個提供者構造函數。

該服務提供者在名為com.jdojo.prime.generic的單獨模塊中定義。 下面包含模塊聲明。 此時模塊聲明將不會編譯。

// module-info.java
module com.jdojo.prime.generic {
    requires com.jdojo.prime;
    provides com.jdojo.prime.PrimeChecker
        with com.jdojo.prime.generic.GenericPrimeChecker;
}

com.jdojo.prime.generic模塊的聲明
需要requires語句,因為該模塊將使用com.jdojo.prime模塊中的PrimeChecker接口。 在NetBeans中,將com.jdojo.prime項目添加到com.jdojo.prime.generic項目的模塊路徑中。 這會解決模塊聲明中的編譯時錯誤,這是requires語句中引用的缺少的模塊com.jdojo.prime引起的。

provides語句指定該模塊為PrimeChecker接口提供了一個實現,其with子句指定了實現類的名稱。 實現類必須滿足以下條件:

  • 它必須是一個公共的具體類。 它可以是頂級或嵌套的靜態類。 但不能是內部類或抽象類。
  • 它必須有一個公共的無參數構造函數。 ServiceLoader類使用此構造函數來使用反射來實例化服務提供者。 請注意,沒有在GenericPrimeChecker類中提供provider 方法,這是提供無參數公共構造函數的替代方法。
  • 實現類的實例必須與服務提供者接口分配兼容。

如果不滿足任何這些條件,則會發生編譯時錯誤。 注意,不需要導出包含服務實現類的com.jdojo.prime.generic包,因為沒有客戶端應該直接依賴於服務實現。 客戶只需要知道服務接口,而不是任何具體的服務實現。ServiceLoader類可以訪問和實例化實現類,而不包含由模塊導出的服務實現的包。

Tips
如果一個模塊使用了一個provides語句,則指定的服務接口可能在當前模塊或另一個可訪問模塊中。 必須在當前模塊中定義在該子句中指定的服務實現類/接口。

下面代包含作為PrimeChecker服務接口的服務實現的GenericPrimeChecker類的代碼。 isPrime()方法返回服務提供者的名稱。 這里提供了jdojo.generic.primechecker作為它的名字。 這是一個任意的名字。 如果指定的數字是素數,則isPrime()方法返回true; 否則返回false。 getName()方法返回提供者名稱。

// GenericPrimeChecker.java
package com.jdojo.prime.generic;
import com.jdojo.prime.PrimeChecker;
public class GenericPrimeChecker implements PrimeChecker {
    private static final String PROVIDER_NAME = "jdojo.generic.primechecker";
    @Override
    public boolean isPrime(long n) {
        if(n <= 1) {
            return false;
        }
        if (n == 2) {
            return true;
        }
        if(n%2 == 0) {
            return false;
        }
        for(long i = 3; i < n; i += 2) {
            if(n%i == 0) {
                return false;
            }
        }
        return true;
    }
    @Override
    public String getName() {
        return PROVIDER_NAME;
    }
}

這就是這個模塊的全部。 如前所述,要編譯此模塊,需要在模塊路徑中使用com.jdojo.prime模塊。 將此模塊編譯並封裝為模塊化JAR。

2. 創建一個快速素數服務提供者

在本節中,將為PrimeChecker服務接口定義另一個服務提供者。 稱之為一個更快的服務提供者,因為將實現一個更快的算法來檢查素材。 該服務提供程序將在名為com.jdojo.prime.faster的單獨模塊中定義,服務實現類稱為FasterPrimeChecker

下面代碼包含模塊聲明,與對com.jdojo.prime.generic模塊的聲明類似。 這一次,只有with子句中的類名已經改變了。

// module-info.java
module com.jdojo.prime.faster {
    requires com.jdojo.prime;
    provides com.jdojo.prime.PrimeChecker
        with com.jdojo.prime.faster.FasterPrimeChecker;
}

模塊聲明中的requires語句需要將com.jdojo.prime項目添加到NetBeans中com.jdojo.prime.faster項目的模塊路徑中,因此可以在模塊路徑中找到com.jdojo.prime模塊。

下面包含FasterPrimeChecker類的代碼,該類的isPrime()方法比GenericPrimeChecker類的isPrime()方法執行得更快。 這一次,該方法循環遍歷從3開始的所有奇數,並結束於正被測試的素數的平方根。

// FasterPrimeChecker.java
package com.jdojo.prime.faster;
import com.jdojo.prime.PrimeChecker;
public class FasterPrimeChecker implements PrimeChecker {
    private static final String PROVIDER_NAME = "jdojo.faster.primechecker";
    // No provider constructor
    private FasterPrimeChecker() {
        // No code
    }
    // Define a provider method
    public static PrimeChecker provider() {
        return new FasterPrimeChecker();
    }
    @Override
    public boolean isPrime(long n) {
        if (n <= 1) {
            return false;
        }
        if (n == 2) {
            return true;
        }
        if (n % 2 == 0) {
            return false;
        }
        long limit = (long) Math.sqrt(n);
        for (long i = 3; i <= limit; i += 2) {
            if (n % i == 0) {
                return false;
            }
        }
        return true;
    }
    @Override
    public String getName() {
        return PROVIDER_NAME;
    }
}

注意,GenericPrimeCheckerFasterPrimeChecker 兩個類的不同,GenericPrimeChecker類包含用作提供程序構造函數的默認構造函數。 它不包含provider 方法。 FasterPrimeChecker類使無參構造函數,切訪問權限為private,它不符合提供者構造函數的限制。 回想一下,提供者構造函數是一個public 無參的構造函數,由ServiceLoader類用於實例化服務實現類。 FasterPrimeChecker類提供了一個provider方法,它被聲明為:
public static PrimeChecker provider(){/*...*/}

ServiceLoader類需要實例化較快的素數服務時,它將調用此方法。 該方法非常簡單 —— 它創建並返回FasterPrimeChecker類的對象。
這就是這個模塊所需要的。 要編譯此模塊,com.jdojo.prime模塊需要在模塊路徑中。 將此模塊編譯並封裝為模塊化JAR。

3.定義一個更快的素數服務提供者

在本節中,將介紹如何使用Java接口作為服務實現。 將為PrimeChecker服務接口定義另一個服務提供者。 我們稱這是一個可能的素數服務提供者,因為它告訴你一個數字可能是一個素數。 該服務提供者在名為com.jdojo.prime.probable的單獨模塊中定義,服務實現接口稱為ProbablePrimeChecker

該服務是關於檢查素數的。 java.math包中的BigInteger類包含一個名為isProbablePrime(int certainty)的方法。 如果方法返回true,則該數字可能是素數。 如果方法返回false,則該數字當然不是素數。 確定性參數確定方法在返回true之前確保數字為素數的程度。 確定性參數的值越高,該方法產生的成本越高,當方法返回true時數字是素數的概率越高。

下面代碼包含模塊聲明,與之前的其他兩個模塊類似。 這一次,只有在with子句中的類/接口名已經改變了。

// module-info.java
module com.jdojo.prime.probable {
    requires com.jdojo.prime;
    provides com.jdojo.prime.PrimeChecker
        with com.jdojo.prime.probable.ProbablePrimeChecker;
}

模塊聲明中的requires語句將要求將·com.jdojo.prime·項目添加到NetBeans中·com.jdojo.prime.probable·項目的模塊路徑中,因此·com.jdojo.prime·模塊位於模塊路徑上。 下面包含了ProbablePrimeChecker類的代碼。

// ProbablePrimeChecker.java
package com.jdojo.prime.probable;
import com.jdojo.prime.PrimeChecker;
import java.math.BigInteger;
public interface ProbablePrimeChecker {
    // A provider method
    public static PrimeChecker provider() {
        final String PROVIDER_NAME = "jdojo.probable.primechecker";
        return new PrimeChecker() {
            @Override
            public boolean isPrime(long n) {
                // Use 1000 for high certainty, which is an arbitrary big number I chose
                int certainty = 1000;
                return BigInteger.valueOf(n).isProbablePrime(certainty);
            }
            @Override
            public String getName() {
                return PROVIDER_NAME;
            }
        };
    }
}

ProbablePrimeChecker接口只包含一個provider方法:
public static PrimeChecker provider() {/*...*/}

ServiceLoader類需要實例化可能的素數服務時,它會調用此方法。 該方法非常簡單 —— 它創建並返回PrimeChecker接口的實例。 isPrime()方法使用BigInteger類來檢查數字是否是可能的素數。 提供者的名稱是jdojo.probable.primechecker。 請注意,該接口不擴展PrimeChecker接口。 要作為服務實現,其provider方法必須返回服務接口(PrimeChecker接口)或其子類的實例。 通過將provider方法的返回類型聲明為PrimeChecker,已經滿足了此要求。

這就是這個模塊的全部。 要編譯此模塊,需要將com.jdojo.prime模塊添加到模塊路徑。 將此模塊編譯並封裝為模塊化JAR。

六. 測試素數提供者

在本節中,將通過創建客戶端應用程序來測試該服務,該應用程序將在名為com.jdojo.prime.client的單獨模塊中定義。 下面包含模塊聲明。

// module-info.java
module com.jdojo.prime.client {
    requires com.jdojo.prime;
}

客戶端模塊只需要了解服務接口。 在這種情況下,com.jdojo.prime模塊定義了服務接口。 因此,客戶端模塊讀取服務接口模塊,沒有其他的。 在現實世界中,客戶端模塊將比這更復雜,它也可能讀取其他模塊。 在NetBeans中,對於要編譯的com.jdojo.prime.client模塊,需要將com.jdojo.prime項目添加到其模塊路徑。 com.jdojo.prime.client模塊的模塊圖如下所示。

client模塊圖

Tips
客戶端模塊不知道服務提供者模塊,並且不需要直接讀取它們。 服務的責任是發現所有服務提供者,並將其實例提供給客戶。 在這種情況下,om.jdojo.prime模塊定義了com.jdojo.prime.PrimeChecker接口,它既是服務接口,也用作服務。

下面包含使用PrimeChecker服務的客戶端的代碼。

package com.jdojo.prime.client;
import com.jdojo.prime.PrimeChecker;
public class Main {
    public static void main(String[] args) {
        // Numbers to be checked for prime
        long[] numbers = {3, 4, 121, 977};
        // Try a default prime checker service provider
        try {
            PrimeChecker checker = PrimeChecker.newInstance();
            checkPrimes(checker, numbers);
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
        // Try the faster prime checker
        try {
            PrimeChecker checker = PrimeChecker.newInstance("jdojo.faster.primechecker");
            checkPrimes(checker, numbers);
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
        // Try the probable prime checker
        try {
            PrimeChecker checker = PrimeChecker.newInstance("jdojo.probable.primechecker");
            checkPrimes(checker, numbers);
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
    }
    public static void checkPrimes(PrimeChecker primeChecker, long... numbers) {
        System.out.format("Using %s:%n",  primeChecker.getName());
        for (long n : numbers) {
            if (primeChecker.isPrime(n)) {
                System.out.printf("%d is a prime.%n", n);
            } else {
                System.out. printf("%d is not a prime.%n", n);
            }
        }
    }
}

checkPrimes()方法使用一個PrimeChecker實例,和可變參數。 它使用PrimeChecker來檢查數字是否為素數,並打印相應的消息。 main()方法檢索默認的PrimeChecker服務提供程序實例和jdojo.faster.primecheckerjdojo.probable.primechecker服務提供程序的實例。 它使用所有三個服務提供者實例來檢查相同的數字集合。 編譯並封裝模塊的代碼。 如果只在模塊路徑中使用兩個模塊com.jdojo.primecom.jdojo.prime.client運行Main類,則可以獲得以下輸出:

No PrimeChecker service provider found.
A PrimeChecker service provider with the name 'jdojo.faster.primechecker' was not found.
A PrimeChecker service provider with the name 'jdojo.probable.primechecker' was not found.
There was no service provider in the module path and, therefore, all three attempts to retrieve a service provider fail.

如果在模塊路徑上使用com.jdojo.primecom.jdojo.prime.genericcom.jdojo.prime.client模塊運行Main類,則可以獲得以下輸出:

Using jdojo.generic.primechecker:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.
A PrimeChecker service provider with the name 'jdojo.faster.primechecker' was not found.
A PrimeChecker service provider with the name 'jdojo.probable.primechecker' was not found.

這一次,在模塊路徑中有一個服務提供者jdojo.generic.primechecker。 因此,嘗試檢索默認服務提供程序將為你提供此服務提供者的PrimeChecker實例。 這在輸出的第一部分是顯而易見的。 嘗試檢索其他服務提供程序失敗,因為它們在模塊路徑上找不到。

如果使用com.jdojo.primecom.jdojo.prime.genericcom.jdojo.prime.fastercom.jdojo.prime.probablecom.jdojo.prime.client模塊在模塊路徑上,運行Main類 ,會得到類似於以下輸出的輸出。 默認provider 是迭代器首先找到的那個。 輸出顯示通用服務提供程序作為默認值,但在運行程序時可能會將任何其他提供程序作為默認提供程序。

Using jdojo.generic.primechecker:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.
Using jdojo.faster.primechecker:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.
Using jdojo.probable.primechecker:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.

當模塊系統在模塊聲明中遇到uses語句時,它將掃描模塊路徑以查找包含provides 語句的所有模塊,該語句指定在uses語句中指定的服務接口的實現。 在這個意義上,模塊中的uses語句表示對其他模塊的間接可選依賴性。 該依賴關系在運行時解決。

七. 選擇和過濾提供者

有時候,需要根據類名選擇提供者。 例如,可能只想選擇只有完全限定類名以com.jdojo開頭的素數服務提供者。 實現這一點的典型邏輯是使用ServiceLoader類的iterator()方法返回的迭代器。 然而,這個操作員是昂貴的。 迭代器會在方法返回前實例化提供者,即使實例化發生懶加載 ——這意味着在需要返回時提供者被實例化。

JDK 9向ServiceLoader類添加了一種新方法。 該方法的簽名如下:

public Stream<ServiceLoader.Provider<S>> stream()

stream()方法返回ServiceProvider.Provider接口的實例流,它在ServiceLoader類中被聲明為嵌套接口,如下所示:

public static interface Provider<S> extends Supplier<S> {
    Class<? extends S> type();
    @Override
    S get();
}

ServiceLoader.Provider接口的一個實例代表服務提供者。 它的type()方法返回服務實現的Class對象。 get()方法實例化並返回服務提供者。 ServiceLoader.Provider接口如何獲得幫助? 當使用stream()方法時,流中的每個元素都是ServiceLoader.Provider類型。 可以根據提供者的類名稱或類型來過濾流,而不會實例化提供者。 可以在過濾器中使用type()方法。 當找到所需的提供程序時,調用get()方法來實例化提供程序。 這樣,當知道需要它時,將實例化一個提供者,而不是在遍歷所有提供者時進行實例化。

以下是使用ServiceLoader類的stream()方法的示例。 它提供了名稱以com.jdojo開頭的所有素數服務提供者的列表。

static List<PrimeChecker> startsWith(String prefix) {
    return ServiceLoader.load(PrimeChecker.class)
                        .stream()
                        .filter((Provider p) -> p.type().getName().startsWith(prefix))
                        .map(Provider::get)
                        .collect(Collectors.toList());
}

可以將此方法添加到PrimeChecker接口。 添加此方法時,需要添加幾個import語句:

import java.util.List;
import java.util.ServiceLoader.Provider;
import java.util.stream.Collectors;
The following is an example of calling this method, for example, from the client class:
// Get the list of all prime services whose class names start with "com.jdojo"
List<PrimeChecker> jdojoService = PrimeChecker.startsWith("com.jdojo");

八. 以傳統模式測試素數服務

並非所有應用程序都將遷移到uses模塊。 你的主要服務的模塊化JAR可以與類路徑上的其他JAR一起使用。 假設將所有五個模塊化JAR作為素數服務放在C:\Java9Revealed\lib目錄中。 通過使用以下命令將四個模塊化JAR放置在類路徑上來運行com.jdojo.prime.client.Main類:

C:\Java9Revealed>java --class-path lib\com.jdojo.prime.jar;lib\com.jdojo.prime.client.jar;lib\com.jdojo.prime.faster.jar;lib\com.jdojo.prime.generic.jar;lib\com.jdojo.prime.probable.jar com.jdojo.prime.client.Main

輸出結果為:

No PrimeChecker service provider found.
A PrimeChecker service provider with the name 'jdojo.faster.primechecker' was not found.
A PrimeChecker service provider with the name 'jdojo.probable.primechecker' was not found.

輸出表示使用傳統模式 —— 通過將所有模塊化JAR放置在類路徑上的JDK 9之前的模式 —— 沒有找到任何服務提供者。 在傳統模式下,服務提供者發現機制是不同的。 ServiceLoader類掃描類路徑上的所有JAR,以查找META-INF/services目錄中的文件。 文件名是完全限定的服務接口名稱。 文件路徑如下所示:

META-INF/services/<service-interface>

該文件的內容是服務提供程序實現類/接口的完全限定名稱的列表。 每個類名需要在一個單獨的行。 可以在文件中使用單行注釋。 從#字符開始的行上的文本被認為是一個注釋。

服務接口名稱為com.jdojo.prime.PrimeChecker,因此三個服務提供者的模塊化JAR將具有名為com.jdojo.prime.PrimeChecker的文件,並具有以下路徑:
META-INF/services/com.jdojo.prime.PrimeChecker

你需要將META-INF/services目錄添加到源代碼目錄的根目錄。 如果您使用的是NetBeans等IDE,那么IDE將會為你打包文件。下面包含三個素數服務提供者模塊的模塊化JAR的com.jdojo.prime.PrimeChecker文件的內容。

# The generic service provider implementation class name
com.jdojo.prime.generic.GenericPrimeChecker
# The faster service provider implementation class name
com.jdojo.prime.faster.FasterPrimeChecker
# The probable service provider implementation interface name
com.jdojo.prime.probable.ProbablePrimeChecker

重新編譯和重新封裝通用和更快速的素數檢查提供者的模塊化JAR。 運行以下命令:

C:\Java9Revealed>java --class-path lib\com.jdojo.prime.jar;lib\com.jdojo.prime.client.jar;lib\com.jdojo.prime.faster.jar;lib\com.jdojo.prime.generic.jar;lib\com.jdojo.prime.probable.jar com.jdojo.prime.client.Main

會出現以下錯誤:

Exception in thread "main" java.util.ServiceConfigurationError: com.jdojo.prime.PrimeChecker: com.jdojo.prime.faster.FasterPrimeChecker Unable to get public no-arg constructor
...
at com.jdojo.prime.client.Main.main(Main.java:13)
Caused by: java.lang.NoSuchMethodException: com.jdojo.prime.faster.FasterPrimeChecker.<init>()
...

上面顯示部分輸出。 當ServiceLoader類嘗試實例化較快的素數服務提供者時,輸出指示運行時異常。 當嘗試實例化可能的素數服務提供商時,將收到相同的錯誤。 在META-INF/services目錄中添加有關服務的信息是實現服務的傳統方式。 為了實現向后兼容,服務實現必須是具有public無參構造函數的類。 回想一下,我們為GenericPrimeChecker類提供了一個提供者構造函數。 所以,其他兩個主要服務將不會在傳統模式下運行。 可以向FasterPrimeChecker類添加提供者構造函數,使其成為可用。 但是,不可能向接口添加提供者構造函數,並且ProbablePrimeChecker將不會在類路徑模式下工作。 你必須從顯式模塊加載它以使其正常工作。

Tips
部署在類路徑上的服務提供者或模塊路徑上的自動模塊必須具有public 無參構造函數。

以下命令僅為通用素數服務提供程序添加了模塊化JAR,該提供程序提供了一個public 無參構造函數。 輸出顯示提供程序已成功定位,實例化和使用。

C:\Java9Revealed>java --class-path lib\com.jdojo.prime.jar;lib\com.jdojo.prime.client.jar;lib\com.jdojo.prime.generic.jar com.jdojo.prime.client.Main

輸出結果為:

Using jdojo.generic.primechecker:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.
A PrimeChecker service provider with the name 'jdojo.faster.primechecker' was not found.
A PrimeChecker service provider with the name 'jdojo.probable.primechecker' was not found.

九. 總結

應用程序(或類庫)提供的特定功能稱為服務。 提供服務實現的應用程序和類庫被稱為服務提供者。 使用這些服務提供者提供的服務的應用程序稱為服務使用者或客戶端。

在Java中,服務由一組接口和類定義。 該服務包含定義服務提供的功能的接口或抽象類,它被稱為服務提供者接口,服務接口或服務類型。 服務接口的具體實現被稱為服務提供者。 單個服務接口可以有多個服務提供者。 在JDK 9中,服務提供者可以是類或接口。

JDK包含一個java.util.ServiceLoader<S>類,其唯一目的是在運行時發現並加載類型為S的服務提供者,用於指定的服務接口。 如果包含服務提供者的JAR(模塊化或非模塊化)放置在類路徑上,ServiceLoader類將使用META-INF/services目錄來查找服務提供者。 該目錄中文件的名稱應與服務接口的完全限定名稱相同。 該文件包含服務提供程序實現類的完全限定名稱——每行一個類名。 該文件可以使用#字符作為單行注釋的開頭。 ServiceLoader類掃描類路徑上的所有META-INF/services目錄以發現服務提供者。

在JDK 9中,服務提供者發現機制已經改變。 使用ServiceLoader類來發現和加載服務提供者的模塊需要使用uses語句來指定服務接口。 在uses語句中指定的服務接口可以在當前模塊或當前模塊可訪問的任何模塊中聲明。 可以使用ServiceLoader類的iterator()方法遍歷所有服務提供程序。 stream()方法提供了一個作為ServiceLoader.Provider接口實例的元素流。 可以使用流根據提供程序的類名過濾和選擇特定類型的提供程序,而無需實例化所有提供程序。

包含服務提供者的模塊需要使用provides語句來指定服務接口及其實現類。 實現類必須在當前模塊中聲明。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM