【问题标题】:How to implement the API/SPI Pattern in Java?如何在 Java 中实现 API/SPI 模式?
【发布时间】:2012-10-22 23:44:40
【问题描述】:

我正在创建一个公开 API 供开发人员使用的框架:

public interface MyAPI {
    public void doSomeStuff();

    public int getWidgets(boolean hasRun);
}

所有开发人员所要做的就是根据这些 API 方法对他们的项目进行编码。我还希望他们能够在运行时类路径上放置不同的“驱动程序”/“API 绑定”(与 JDBC 或 SLF4J 的工作方式相同)并让 API 方法调用(doSomeStuff() 等)在不同的第 3 方上运行资源(文件、服务器等)。因此,相同的代码和 API 调用将映射到不同资源上的操作,具体取决于运行时类路径看到的驱动程序/绑定(即myapi-ftpmyapi-sshmyapi-teleportation)。

我如何编写(和打包)允许此类运行时绑定的 SPI,然后将 MyAPI 调用映射到正确的(具体)实现?换句话说,如果myapi-ftp 允许您从 FTP 服务器访问getWidgets(boolean),我将如何实现(同时使用 API 和 SPI)?

具体、有效的 Java 代码示例加分!提前致谢!

【问题讨论】:

    标签: java api design-patterns slf4j


    【解决方案1】:

    看看 java.util.ServiceLoader 类。

    总的来说,想法是这样的:

    API 罐子

  • 提供接口
  • 使用 ServiceLoader 类查找实现

    绑定/驱动 Jar

  • 实现接口
  • 创建文件 META-INF/ 并指定实现它的类名

    在 javadocs 中有一个很好的例子:

    http://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html

    API 罐子

    package com.foo;
    
    public interface FooInterface { ... }
    
    public class FooInterfaceFactory {
      public static FooInterface newFooInstance() {
        ServiceLoader<FooInterface> loader = ServiceLoader.load(FooInterface.class);
        Iterator<FooInterface> it = loader.iterator();
        // pick one of the provided implementations and return it.
      }
    

    绑定罐

    package com.myfoo;
    public class MyFooImpl implements FooInterface { ... }
    
    META-INF/com.foo.FooInterface
        com.myfoo.MyFooImpl
    

    编辑

    SPI 示例

    public interface FooSpi { 
       void accepts(String url);
       FooInterface getFooInstance();
    }
    
    
    public class FooInterfaceFactory {
      public static FooInterface getFooInterfaceInstance(String url) {
        ServiceLoader<FooSpi> loader = ServiceLoader.load(FooSpi.class);
        Iterator<FooSpi> it = loader.iterator();
        while (it.hasNext()) {
           FooSpi fooSpi = it.next();
           if (fooSpi .accepts(url)) {
             return fooSpi.getFooInstance();
           }
        }
    
        return null;
      }
    }
    

    当然,将文件名更改为 com.foo.FooSpi 并提供 FooSpi 的实现。这将允许您将公共 API 与 Spi 接口分离。

    如果你想隐藏 accept 方法,你总是可以有第二个接口,即你的公共 API,并且 t

  • 【讨论】:

    • 感谢@Matt (+1) 的精彩回答 - 快速提问:SPI 将在哪里实施?我想现在我对哪些类/方法代表 API 与哪些类/方法代表 SPI 感到困惑。再次感谢!
    • 您可以使用您查找的“FooInterface”方法做任何您想做的事情。 JDBC 不是以这种方式完成的,它是通过在每个驱动程序类中的静态初始化块中调用 DriverManager.registerDrive 来完成的(因此需要 Class.forName 以便您加载驱动程序,并且它的静态块将其注册到 DriverManager)。没有什么可以说工厂方法不能采用其他类型的参数来帮助您决定选择哪种实现。查看编辑。
    【解决方案2】:

    您知道 API 是客户端使用的,而 SPI 是您的库内部使用的。

    您应该拥有实现 API 类的类,这些类依赖于 SPI 接口,并为您的 SPI 提供一些实现。

    大多数时候 SPI 接口包含低级方法(在您的示例中直接使用 FTP、SSH 和...的抽象),并且您的库为您的客户端提供更高级别的操作。

    也许你的 SPI 接口是这样的:

    public interface ProtocolSPI {
        boolean isCompatibleWithUrl(String url);
        Handle connect(String url, Map<String, Object> parameters);
        int readData(Handle handle, byte[] bytes);
        void writeData(Handle handle, byte[] bytes, int startIndex, int length);
        void closeHandle(Handle handle);
    }
    

    并且您的代码依赖于此接口来处理可替换部件。

    您可能有一个 ProtocolSPIFactory,它使用 java.util.ServiceLoader 查找您的 ProtocolSPI 的可用实现(在类路径中),然后实例化它们并通过调用 isCompatibleWithUrl 找出用​​于特定 url 的实现。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-08-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多