9

我正在阅读 Bloch 的 Effective java book [1]并遇到了以下 SPI 示例:

//Service interface
public interface Service {
  //Service specific methods here
}

//Service provider interface
public interface Provider {
  Service newService();
}

//Class for service registration and access
public class Services {
  private Services(){}

  private static final Map<String, Provider> providers =
    new ConcurrentHashMap<String, Provider>();
  public static final String DEFAULT_PROVIDER_NAME = "<def>";

  //Registration
  public static void registerDefaultProvider(Provider p) {
    registerProvider(DEFAULT_PROVIDER_NAME, p);
  }
  public static void registerProvider(String name, Provider p) {
    providers.put(name, p);
  }

  //Access
  public static Service newInstance() {
    return newInstance(DEFAULT_PROVIDER_NAME);
  }
  public static Service newInstance(String name) {
     // you get the point..lookup in the map the provider by name
     // and return provider.newService();
  }

这是我的问题:为什么需要提供者接口?难道我们不能自己轻松地注册服务吗?例如,维护服务实现的映射,然后在查找时返回实例?为什么要增加额外的抽象层?

也许这个例子太笼统了——任何“更好”的例子来说明这一点也很好。


[1] 第二版,第 2 章。第一版示例未提及服务提供者接口。

4

6 回答 6

6

为什么需要提供者接口?难道我们不能自己轻松地注册服务吗?例如,维护服务实现的映射,然后在查找时返回实例?

正如其他人所说,Provider 的目的是拥有一个可以创建Service实例的 AbstractFactory。您并不总是希望保留对所有服务实现的引用,因为它们可能是短暂的和/或在执行后可能无法重用。

但是提供者的目的是什么?如果没有提供者,如何使用“提供者注册 API”?

拥有 Provider 接口的最有力的原因之一是您不需要在编译时实现。API 的用户可以稍后添加他们自己的实现。

让我们以 JDBC 为例,就像在另一个答案中使用的 Ajay 一样,但让我们更进一步:

有许多不同类型的数据库和数据库供应商,它们管理和实现数据库的方式(以及查询它们的方式)略有不同。Java 的创建者不可能创建所有这些不同可能方式的实现,原因有很多:

  • 最初编写 Java 时,许多这样的数据库公司或系统还不存在。
  • 并非所有这些数据库供应商都是开源的,因此 Java 的创建者不知道如何与他们沟通,即使他们愿意。
  • 用户可能想要编写自己的自定义数据库

那么你如何解决这个问题呢?通过使用Service Provider.

  • 驱动程序Provider接口是. 它提供了与特定供应商的数据库交互的方法。其中一种方法Driver是工厂方法,用于在给定 url 和其他属性(如用户名和密码等)的情况下为数据库创建Connection实例(即Service)。

每个数据库供应商都编写了自己的Driver实现,以了解如何与自己的数据库系统进行通信。这些不包含在 JDK 中;您必须访问公司网站或其他代码存储库并将它们作为单独的 jar 下载。

要使用这些驱动程序,您必须将 jar 添加到类路径中,然后使用 JDKDriverManager类来注册驱动程序。

DriverManager 类有一个方法registerDriver(Driver),用于在服务注册中注册一个 Driver 实例,以便可以使用它。按照惯例,大多数Driver实现在类加载时注册,因此您在代码中所要做的就是编写

Class.forname("foo.bar.Driver"); 

为供应商“foo.bar”注册驱动程序(假设您的类路径中有该类的 jar。)

注册数据库驱动程序后,您可以获得连接到数据库的服务实现实例。

例如,如果您在本地计算机上有一个名为“test”的 mysql 数据库,并且您的用户帐户的用户名为“monty”,密码为“greatsqldb”,那么您可以像这样创建一个服务实现:

Connection conn =
   DriverManager.getConnection("jdbc:mysql://localhost/test?" +
                               "user=monty&password=greatsqldb");

DriverManager 类看到你传入的 String 并找到可以理解其含义的注册驱动程序。(这实际上是Chain of Responsibility通过遍历所有注册的驱动程序并调用它们的Driver.acceptsUrl(Stirng)方法直到 url 被接受来使用 Pattern 完成的)

请注意,JDK 中没有特定于 mysql 的代码。您所要做的就是注册某个供应商的驱动程序,然后将格式正确的字符串传递给服务提供者。如果我们稍后决定使用不同的数据库供应商(如 oracle 或 sybase),那么我们只需交换 jar 并修改我们的连接字符串。DriverManager 中的代码不会改变。

为什么我们不只是建立一次联系并保持联系呢?为什么我们需要服务?

我们可能希望在每次操作后连接/断开连接。或者我们可能希望将连接保持更长时间。拥有该服务允许我们随时创建新的连接,并且不妨碍我们保留对它的引用以供以后重复使用。

这是一个非常强大的概念,框架使用它来允许许多可能的排列和扩展,而不会弄乱核心代码库。

编辑

使用多个提供者和提供多个的提供者Services

没有什么可以阻止您拥有多个提供者。您可以同时连接到使用不同数据库供应商软件创建的多个数据库。您还可以同时连接到同一供应商生产的多个数据库。

多种服务 - 一些提供者甚至可能Service根据连接 url 提供不同的实现。例如,H2 可以创建基于文件系统或基于内存的数据库。告诉 H2 你想使用哪个的方法是不同的 url 格式。我没有看过 H2 代码,但我假设基于文件和基于内存是不同的服务实现。

为什么 DriverManager 不只管理 Connections 而 Oracle 可以实现 OracleConnectionWrapper?没有供应商!

这还需要您知道您有一个 Oracle 连接。那是非常紧密的耦合,如果我改变供应商,我将不得不改变很多代码。

Service Registration只需要一个字符串。请记住,它使用chain of Responsiblity来查找第一个知道如何处理 url 的注册提供程序。应用程序可以是供应商中立的,它可以从属性文件中获取连接 url 和驱动程序类名称。这样,如果我更改供应商,我就不必重新编译我的代码。但是,如果我硬编码对“OracleConnectionWrapper”的引用,然后我更改了供应商,我将不得不重写部分代码,然后重新编译。

如果他们愿意,没有什么可以阻止某人支持多种数据库供应商 url 格式。因此,如果我愿意,我可以制作一个可以处理 mysql 和 oracle 的 GenericDriver。

于 2014-03-28T15:16:34.523 回答
1

If you might need more than one service of each type, you can't just reuse the old Services. (Additionally, tests and the like might want to create fresh services for each test, rather than reusing services that might have been modified or updated by previous tests.)

于 2012-07-07T16:04:46.417 回答
1

Effective Java我认为答案是与一个例子一起提到的。

服务提供者框架的第四个可选组件是服务提供者接口,提供者实现该接口以创建其服务实现的实例。在没有服务提供者接口的情况下,实现由类名注册并反射地实例化(第 53 条)。

在这种情况下JDBC
Connection扮演服务接口的角色,
DriverManager.registerDriver提供者注册APIDriverManager.getConnection服务访问API
Driver服务提供者接口

因此,正如您正确指出的那样,拥有 Provider 接口不是必须的,而只是一种更简洁的方法。

于 2014-03-26T22:10:28.640 回答
0

因此,您似乎可以有多个Provider相同的 s,Service并且基于特定的 Provider 名称,您可能会获得同一服务的不同实例。所以我想说每个 Provider 有点像工厂,可以适当地创建服务。

例如假设class PaymentService implements Service它需要一个Gateway. 你有 PayPal 和 Chase 网关来处理这些支付处理器。现在,您创建了一个 PayPalProvider 和 ChaseProvider,每个都知道如何使用正确的网关创建正确的 PaymentService 实例。

但我同意,似乎做作。

于 2014-03-26T21:54:20.577 回答
0

作为其他答案的综合(第四部分是文本原因),我认为这是为了限制编译依赖性。使用 SPI,您可以使用所有工具来排除对实现的显式引用:

  • META-INF/services/目录包含提及可用服务提供者实现的文件
  • ServiceLoader标准类允许解析可用的实现名称,顺便说一下动态构造[1]

第一版中没有提到 SPI。将它包含在有关静态工厂的项目中可能不是正确的地方。文中提到的DriverManager是一个提示,但布洛赫并没有深入。在某种程度上,平台实现了一种ServiceLocator 模式来减少编译依赖,具体取决于环境。在您的抽象工厂中使用 SPI,它在 ServiceLoader 的帮助下成为 ServiceLocator 的 ServiceFactory 以实现模块化。

ServiceLoader 迭代器可用于动态填充示例的服务映射。


[1]在 OSGi 环境中,这是一个微妙的操作

于 2014-12-17T13:56:43.480 回答
-1

没有提供者的服务提供者接口

让我们看看没有提供者的情况。

//Service interface
public interface Service {
  //Service specific methods here
}

//Class for service registration and access
public class Services {
  private Services(){}

  private static final Map<String, Service> services =
    new ConcurrentHashMap<String, Service>();
  public static final String DEFAULT_SERVICE_NAME = "<def>";

  //Registration
  public static void registerDefaultService(Provider p) {
    registerService(DEFAULT_SERVICE_NAME, p);
  }
  public static void registerService(String name, Provider p) {
    services.put(name, p);
  }

  //Access
  public static Service getInstance() {
    return newInstance(DEFAULT_SERVICE_NAME);
  }
  public static Service getInstance(String name) {
     // you get the point..lookup in the map the service by name
     // and return it;
  }

如您所见,可以在没有Provider接口的情况下创建 Service Provider Interface 。最终的调用者#getInstance(..)不会注意到差异。

那我们为什么需要提供者?

Provider接口是一个抽象工厂Services#newInstance(String)是一个工厂方法。两种设计模式都具有将服务实现与服务注册分离的优点。

单一责任原则

您无需在注册所有服务的启动事件处理程序中实现服务实例化,而是为每个服务创建一个提供程序。这使它松散耦合并且更容易重构,因为服务和服务提供者可以彼此靠近,例如放在另一个 JAR 文件中。

“工厂方法在工具包和框架中很常见,其中库代码需要创建类型的对象,这些对象可能被使用框架的应用程序子类化。” [1]

终身管理

您可能已经在没有提供者的上层代码中意识到,我们正在注册服务实例而不是提供者,提供者可以决定实例化一个新的服务实例。

这种方法有一些缺点:

1. 必须在第一次服务调用之前创建服务实例。延迟初始化是不可能的。这将延迟启动并将资源绑定到很少使用甚至从不使用的服务。

1b。您“不能”在使用后关闭服务,因为无法重新实例化它们。(使用提供者,您可以以调用者必须调用的方式设计服务接口#close(),这会通知提供者并且提供者决定保留或完成服务实例。)

2. 所有调用者都将使用相同的服务实例,因此您必须确保它是线程安全的。但是使其线程安全会使其变慢。相反,提供者可能会选择创建几个服务实例来减少保留时间。

结论

提供者接口不是必需的,但它封装了特定于服务的实例化逻辑并优化了资源分配。

于 2014-03-27T08:35:00.257 回答