What is the SPI

SPI, or Service Provider Interface, is a Service discovery mechanism. It provides an externally extensible capability for the framework.

How does it differ from the RPC approach provided by the interface class – implementation class?

  • The traditional interface class implementation form is as follows
public interface AdOpFromApolloService {}
public class AdOpFromDbServiceImpl implements AdOpFromDbService {}
Copy the code

Suppose we need to implement RPC, how do we do it?

RPC adds an annotation to the corresponding interface class AdOpFromApolloService to indicate that it is an RPC class, and then puts the current class in a dependency package for other projects to call through the interface class

In short: Just provide interface classes in RPC calls, and then let third parties call (third parties only call, don’t write implementation)

So what does RPC have to do with SPI?

  • SPI: Provides an interface that can be overridden or implemented using the default SPI interface after a third party references the package.
  • RPC: Provides the interface, but the implementation is private and does not open the rewriting capability to the public.

Application scenarios of SPI

  1. Framework implementation cases:
  1. Spring extension plugin implementation. Such as JDCB
  2. Middleware extension plug-in implementation. Such as Dubbo, Apollo
  3. Examples in the development process: implement an interceptor and use SPI mechanism to extend its interception methods (such as full interception, how many intercepts per minute, what proportion of intercepts, whether the intercepted logs are log printed or dropped, es)

How do YOU implement an SPI?

Next, two project modules are used to explain the usage of SPI. First, the project structure diagram is as follows

Next is the implementation process

Step 1: create a spi – demo – contract project, the following new directory in the resources directory (MATE – INF/services) and file (com) example. Spidemocontract. Spi. SpiTestDemoService)

  • Modify the com. Example. Spidemocontract. Spi. SpiTestDemoService file
  • Add the interface class in the same position as step 2, and the corresponding implementation class

Step 2: Create the SPI-Demo project, and then introduce the SPI-Demo-Contract dependencies

  • New directory as follows in the resources directory (MATE – INF/services) and file (com) example. Spidemocontract. Spi. SpiTestDemoService), the filename with spi – demo – contract
  • Modify com. Example. Spidemocontract. Spi. SpiTestDemoService (and rely on the package file name completely consistent, but custom content to the current project implementation class) files
  • Implement the SPI interface, customize the implementation class of the SPI-Demo project (here you can set the priority to the highest)

Step 3: Load the SPI interface with the ServiceLoader in the SPI-Demo project

Note: We can override the priority of the declaration class to determine which implementation class needs to be handled. For example, if we override a custom implementation class whose priority =0 is the highest, and then load only the first one with the highest priority by default, the custom implementation class we override will override the default SPI implementation class

The detailed steps are divided as follows

  • Step 1: create a spi – demo – contract project, the following new directory in the resources directory (MATE – INF/services) and file (com) example. Spidemocontract. Spi. SpiTestDemoService)
-- resources
---- META-INF
-------- services
------------ com.example.spidemocontract.spi.SpiTestDemoService
Copy the code
    • Modify the com. Example. Spidemocontract. Spi. SpiTestDemoService file is as follows
com.example.spidemocontract.spi.impl.DefaultSpiTestDemoService
Copy the code
    • Add the interface class in the same position as step 2, and the corresponding implementation class
/ * * * the interface class completely corresponding to the resources/meta-inf/services/com. Example. Spidemocontract. Spi. The impl. DefaultSpiTestDemoService * * /
public interface SpiTestDemoService {
    void printLog(a);
    int getOrder(a);
}

/** * sets the default to the lowest priority, which is the default SPI interface implementation class */
public class DefaultSpiTestDemoService implements SpiTestDemoService {
    @Override
    public int getOrder(a) {
        return Integer.MAX_VALUE;
    }
    @Override
    public void printLog(a) {
        System.out.println("Output DefaultSpiTestDemoService log"); }}Copy the code
  • Step 2: Create the SPI-Demo project, and then introduce the SPI-Demo-Contract dependencies
<dependency>
    <groupId>com.example</groupId>
    <artifactId>spi-demo-contract</artifactId>
    <version>0.0.1 - the SNAPSHOT</version>
    <scope>compile</scope>
</dependency>
Copy the code
    • New directory as follows in the resources directory (MATE – INF/services) and file (com) example. Spidemocontract. Spi. SpiTestDemoService), the filename with spi – demo – contract
-- resources
---- META-INF
-------- services
------------ com.example.spidemocontract.spi.SpiTestDemoService
Copy the code
    • Modify com. Example. Spidemocontract. Spi. SpiTestDemoService (and rely on the package file name completely consistent, but custom content to the current project implementation class) file is as follows
com.example.spidemo.spi.OtherSpiTestDemoService
Copy the code
    • Implement SPI interface, custom SPI-Demo project implementation class
/** * other, set it to a higher priority */
public class OtherSpiTestDemoService implements SpiTestDemoService {
    // When we use SPI class loader, we will sort it according to order, the smaller the higher the priority
    @Override
    public int getOrder(a) {
        return 0;
    }
    @Override
    public void printLog(a) {
        System.out.println("Output OtherSpiTestDemoService log"); }}Copy the code
  • Step 3: Load the SPI interface with the ServiceLoader in the SPI-Demo project
public static void main(String[] args) {
    / / load SPI
    Iterator<SpiTestDemoService> iterator = ServiceLoader.load(SpiTestDemoService.class).iterator();
    // Implement ordered, the order will be based on the ordered value, the higher the priority, the more first fetched
    List<SpiTestDemoService> list = Lists.newArrayList(iterator)
            .stream().sorted(Comparator.comparing(SpiTestDemoService::getOrder))
            .collect(Collectors.toList());
    for(SpiTestDemoService item : list) { item.printLog(); }}Copy the code

How does middleware implement SPI?

Apollo-client implementation

  • Apollo – Client initialization process, there is a ConfigPropertySourcesProcessorHelper SPI interface
/ / the current interface in the resource/meta-inf/services directory file com. Ctrip. Framework. Apollo. Spring. The spi. ConfigPropertySourcesProcessorHelper
public interface ConfigPropertySourcesProcessorHelper extends Ordered {
  void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}
Copy the code
  • The default implementation in SPI currently is
public class DefaultConfigPropertySourcesProcessorHelper implements ConfigPropertySourcesProcessorHelper {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        / /... Various registered beans, initialization process
    }
    @Override
    public int getOrder(a) {
        // Set the priority to the lowest. Convenient for other custom SPI implementation classes to override
        returnOrdered.LOWEST_PRECEDENCE; }}Copy the code
  • How do I choose to load a custom SPI implementation class

The serviceloader.load (xxxx.class) is used to load all instances of the serviceloader.load (xxxx.class) and then order the instances of the serviceloader.load (xxxx.class) and order the instances of the serviceloader.load (xxxx.class). Because the custom SPI implementation priority can be set high, it can override the default implementation

public static <S extends Ordered> S loadPrimary(Class<S> clazz) {
    List<S> candidates = loadAllOrdered(clazz);
    return candidates.get(0);
}
public static <S extends Ordered> List<S> loadAllOrdered(Class<S> clazz) {
    Iterator<S> iterator = loadAll(clazz);

    if(! iterator.hasNext()) {throw new IllegalStateException(String.format(
          "No implementation defined in /META-INF/services/%s, please check whether the file exists and has the right implementation class!",
          clazz.getName()));
    }
    // Get all SPI implementation instances in the iteration and sort them, taking the one with the highest priority
    List<S> candidates = Lists.newArrayList(iterator);
    Collections.sort(candidates, new Comparator<S>() {
      @Override
      public int compare(S o1, S o2) {
        // the smaller order has higher priority
        returnInteger.compare(o1.getOrder(), o2.getOrder()); }});return candidates;
}
Copy the code

Implementation in JDBC

  • The JDBC SPI in the configuration file resources/meta-inf/services/Java, SQL, Driver, and set the parameters as follows
com.example.app.driver.MyDriver
Copy the code
  • Inherit SPI interface java.sql.Driver to achieve MyDriver
public class MyDriver extends NonRegisteringDriver implements Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new MyDriver());
        } catch (SQLException e) {
            throw new RuntimeException("Can't register driver!", e); }}public MyDriver(a) throws SQLException {}
    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("MyDriver - Ready to create database connection. Url :" + url);
        System.out.println("JDBC configuration Information:" + info);
        info.setProperty("user"."root");
        Connection connection = super.connect(url, info);
        System.out.println("MyDriver - Database connection created!" + connection.toString());
        returnconnection; }}Copy the code
  • The write main method call executes the custom SPI implementation you just implemented
String url = "jdbc:mysql://localhost:3306/test? serverTimezone=UTC";
String user = "root";
String password = "root";
Class.forName("com.example.app.driver.MyDriver");
Connection connection = DriverManager.getConnection(url, user, password);
Copy the code
  • How does JDBC use SPI in a simple analysis

Obtain all registered drivers (each driver is traversed, in fact, is the latest registered one to use that, if failed to find the next)

// Obtain all registered drivers (each driver is iterated, in fact, the latest registered one is used, if the failure to find the next)
 for(DriverInfo aDriver : registeredDrivers) {
    // If the caller does not have permission to load the driver then
    // skip it.
    if(isDriverAllowed(aDriver.driver, callerCL)) {
        try {
            println(" trying " + aDriver.driver.getClass().getName());
            Connection con = aDriver.driver.connect(url, info);
            if(con ! =null) {
                // Success!
                println("getConnection returning " + aDriver.driver.getClass().getName());
                return(con); }}catch (SQLException ex) {
            if (reason == null) { reason = ex; }}}else {
        println(" skipping: "+ aDriver.getClass().getName()); }}Copy the code