Java SPI mechanism of JDK source code parsing

1. What is SPI

The full name of SPI is Service Provider Interface, which is a set of API s provided by Java to be implemented or extended by a third party. It can be used to enable framework extension and replace components.

In object-oriented design, it is generally recommended to program modules based on interfaces, and there is no hard coding between modules. Once a specific implementation class is involved in the code, it violates the opening and closing principle. Java SPI is the mechanism to find a service implementation for an interface. The core idea of Java SPI is decoupling.

The overall mechanism is as follows:

 

 

In fact, Java SPI is a dynamic loading mechanism realized by the combination of "interface based programming + policy mode + configuration file".

In summary, the implementation strategy of the framework is enabled, extended or replaced by the caller according to the actual needs

2. Application scenario

  • Database driven loading interface implements class loading

    JDBC drivers for loading different types of databases

  • Log facade interface implementation class loading

    SLF4J loads the log implementation classes of different suppliers

  • Spring

    Servlet container startup initialization org.springframework.web.SpringServletContainerInitializer

  • Spring Boot

    During the automatic assembly process, load the META-INF/spring.factories file and parse the properties file

  • Dubbo

    Dubbo uses a lot of SPI technology, and there are many components in it. Each component is abstracted by the formation of interface in the framework

    For example, Protocol interface

3. Operation steps

Take payment services for example:

  • Create a PayService and add a pay method

    package com.imooc.spi;
    
    import java.math.BigDecimal;
    
    public interface PayService {
    
        void pay(BigDecimal price);
    }
    

      

  1. Create AlipayService and WechatPayService to implement PayService

    ⚠ the implementation class of SPI must carry a construction method without parameters;

    package com.imooc.spi;
    
    import java.math.BigDecimal;
    
    public class AlipayService implements PayService{
    
        public void pay(BigDecimal price) {
            System.out.println("Use Alipay payment");
        }
    }
    
    package com.imooc.spi;
    
    import java.math.BigDecimal;
    
    public class WechatPayService implements PayService{
    
        public void pay(BigDecimal price) {
            System.out.println("Use wechat payment");
        }
    }
    
  2. Create the directory META-INF/services under the resources directory

  3. Create com.imooc.spi.PayService file in META-INF/services

  4. First, take AlipayService as an example: add the file content of com.imooc.spi.AlipayService at com.imooc.spi.PayService

  5. Create test class

    package com.imooc.spi;
    
    import com.util.ServiceLoader;
    
    import java.math.BigDecimal;
    
    public class PayTests {
    
        public static void main(String[] args) {
            ServiceLoader<PayService> payServices = ServiceLoader.load(PayService.class);
            for (PayService payService : payServices) {
                payService.pay(new BigDecimal(1));
            }
        }
    }
    
  6. Run the test class to see the returned results

4. Principle analysis

First, let's open serviceloader < s >


  public final class ServiceLoader<S> implements Iterable<S> {
    // Prefix for SPI file path
    private static final String PREFIX = "META-INF/services/";
  
    // The class or interface of the service to be loaded
    private Class<S> service;
  
    // Classloader for locating, loading, and instantiating providers
    private ClassLoader loader;
  
    // Access control context obtained when creating ServiceLoader
    private final AccessControlContext acc;
  
    // Cache providers in instantiation order
    private LinkedHashMap<String, S> providers = new LinkedHashMap();
  
    // Lazy load iterator 
    private LazyIterator lookupIterator;
  
  	......
}

  

 

Refer to the specific source code of ServiceLoader. There is not much code. The implementation process is as follows:

  1. The application calls the ServiceLoader.load method

    // 1. Get ClassLoad
    public static <S> ServiceLoader<S> load(Class<S> service) {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
    }
    
    // 2. Call the construction method
    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
      return new ServiceLoader<>(service, loader);
    }
    
    // 3. Verify parameters and ClassLoad
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
      service = Objects.requireNonNull(svc, "Service interface cannot be null");
      loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
      acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
      reload();
    }
    
    //4. Clean the cache container, and the instance is lazy to load the iterator
    public void reload() {
      providers.clear();
      lookupIterator = new LazyIterator(service, loader);
    }
    

      

     
  2. Let's take a look at this lazy load iterator

    // Private inner classes that implement a completely lazy provider lookup
    private class LazyIterator implements Iterator<S>{
    
      // The class or interface of the service to be loaded
      Class<S> service;
      // Classloader for locating, loading, and instantiating providers
      ClassLoader loader;
      // Resource path of enumeration type
      Enumeration<URL> configs = null;
      // iterator
      Iterator<String> pending = null;
      // className on the next line in the configuration file
      String nextName = null;
    
      private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
      }
    
      private boolean hasNextService() {
        if (nextName != null) {
          return true;
        }
        // Load the file to configure PREFIX + service.getName()
        if (configs == null) {
          try {
            String fullName = PREFIX + service.getName();
            if (loader == null)
              configs = ClassLoader.getSystemResources(fullName);
            else
              configs = loader.getResources(fullName);
          } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
          }
        }
        // Loop to get next line
        while ((pending == null) || !pending.hasNext()) {
          // Determine if there are elements
          if (!configs.hasMoreElements()) {
            return false;
          }
          pending = parse(service, configs.nextElement());
        }
        // Get class name
        nextName = pending.next();
        return true;
      }
    
      // Get next Service implementation
      private S nextService() {
        if (!hasNextService())
          throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
          // Loading class
          c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
          fail(service,
               "Provider " + cn + " not found");
        }
        // Super class judgment
        if (!service.isAssignableFrom(c)) {
          fail(service,
               "Provider " + cn  + " not a subtype");
        }
        try {
          // Instantiate and class transform
          S p = service.cast(c.newInstance());
          // Put in cache container
          providers.put(cn, p);
          return p;
        } catch (Throwable x) {
          fail(service,
               "Provider " + cn + " could not be instantiated",
               x);
        }
        throw new Error();          // This cannot happen
      }
    
      // for loop traversal
      public boolean hasNext() {
        if (acc == null) {
          return hasNextService();
        } else {
          PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
          };
          return AccessController.doPrivileged(action, acc);
        }
      }
    
      public S next() {
        if (acc == null) {
          return nextService();
        } else {
          PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
          };
          return AccessController.doPrivileged(action, acc);
        }
      }
    
      // No deletion
      public void remove() {
        throw new UnsupportedOperationException();
      }
    
    }
    

      

     
  3. Analyzes the content of the given URL as a provider profile.

    private Iterator<String> parse(Class<?> service, URL u)
            throws ServiceConfigurationError
        {
            InputStream in = null;
            BufferedReader r = null;
            ArrayList<String> names = new ArrayList<>();
            try {
                in = u.openStream();
                r = new BufferedReader(new InputStreamReader(in, "utf-8"));
                int lc = 1;
                while ((lc = parseLine(service, u, r, lc, names)) >= 0);
            } catch (IOException x) {
                fail(service, "Error reading configuration file", x);
            } finally {
                try {
                    if (r != null) r.close();
                    if (in != null) in.close();
                } catch (IOException y) {
                    fail(service, "Error closing configuration file", y);
                }
            }
            return names.iterator();
        }
    

      

     
  4. Parse the configuration file by line and save it in the names list

    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                              List<String> names)
            throws IOException, ServiceConfigurationError
        {
            String ln = r.readLine();
            if (ln == null) {
                return -1;
            }
            int ci = ln.indexOf('#');
            if (ci >= 0) ln = ln.substring(0, ci);
            ln = ln.trim();
            int n = ln.length();
            if (n != 0) {
                if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                    fail(service, u, lc, "Illegal configuration-file syntax");
                int cp = ln.codePointAt(0);
                if (!Character.isJavaIdentifierStart(cp))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
                for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                    cp = ln.codePointAt(i);
                    if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                        fail(service, u, lc, "Illegal provider-class name: " + ln);
                }
                // To determine whether the provider container contains or not, add the classname to the names list
                if (!providers.containsKey(ln) && !names.contains(ln))
                    names.add(ln);
            }
            return lc + 1;
        }
    

     

5. summary

Advantage: the advantage of using Java SPI mechanism is to realize decoupling, so that the assembly control logic of the third-party service module is separated from the business code of the caller, rather than coupled together. The application can enable framework extension or replace framework components according to the actual business situation.

Disadvantages: thread is not safe. Although ServiceLoader is also a delayed load, it can only be obtained by traversal, that is, the implementation classes of the interface are loaded and instantiated once. If you don't want to use some implementation classes, it's also loaded and instantiated, which is wasteful. The way to get an implementation class is not flexible enough. It can only be obtained in the form of Iterator, not based on a parameter.

Keywords: Java Spring Dubbo Programming

Added by legio on Wed, 27 Nov 2019 10:40:22 +0200