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语句来指定服务接口及其实现类。 实现类必须在当前模块中声明。

posted @ 2017-06-19 20:23  林本托  阅读(4006)  评论(3编辑  收藏  举报