IAmYourFaja
IAmYourFaja

Reputation: 56934

How to implement the API/SPI Pattern in Java?

I am creating a framework that exposes an API for developers to use:

public interface MyAPI {
    public void doSomeStuff();

    public int getWidgets(boolean hasRun);
}

All the developers should have to do is code their projects against these API methods. I also want them to be able to place different "drivers"/"API bindings" on the runtime classpath (the same way JDBC or SLF4J work) and have the API method calls (doSomeStuff(), etc.) operate on different 3rd party resources (files, servers, whatever). Thus the same code and API calls will map to operations on different resources depending on what driver/binding the runtime classpath sees (i.e. myapi-ftp, myapi-ssh, myapi-teleportation).

How do I write (and package) an SPI that allows for such runtime binding, and then maps MyAPI calls to the correct (concrete) implementation? In other words, if myapi-ftp allows you to getWidgets(boolean) from an FTP server, how would I could this up (to make use of both the API and SPI)?

Bonus points for concrete, working Java code example! Thanks in advance!

Upvotes: 10

Views: 11463

Answers (2)

Matt
Matt

Reputation: 11805

Take a look at the java.util.ServiceLoader class.

In general, the idea is this:

API Jar

  • Supply the interface
  • Use the ServiceLoader class to lookup the implementation

    Binding/Driver Jar

  • Implement the interface
  • Create the file META-INF/ and specify the classname that implements it

    There's a good example right in the javadocs:

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

    API Jar

    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.
      }
    

    Binding Jar

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

    EDIT SPI Example

    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;
      }
    }
    

    And of course, change the filename to com.foo.FooSpi and provide an implementation of FooSpi. That will allow you to segregate the public API from the Spi interface.

    If you want to hide the accepts method, you could always have a second interface which is your public API.

    Upvotes: 17

  • Amir Pashazadeh
    Amir Pashazadeh

    Reputation: 7322

    Well you know that API are what clients use, and SPI is what your library use internally.

    You shall have classes which implement your API classes, which depend on SPI interfaces, and have some implementations for your SPI.

    Most of the time SPI interfaces contain low level methods (abstraction for direct working with FTP, SSH and... in your example), and your library provides higher level operations for your clients.

    Maybe your SPI interfaces would be such as:

    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);
    }
    

    and you have code which depends on this interface for working with replaceable part.

    You may have a ProtocolSPIFactory which uses java.util.ServiceLoader to find available implementations of your ProtocolSPI (in the class-path), then instantiates them and by calling isCompatibleWithUrl finds out which implementation to use for a specific url.

    Upvotes: 3

    Related Questions