Derivatives from Dubbo Notes①: JDK SPI Mechanism

I. Introduction

This series is a derivative of personal Dubbo study notes. It is a derivative content outside of the main text. The content comes from "In-depth analysis of Apache Dubbo core technology insider" and is only used for personal note recording. The analysis of this article is based on Dubbo 2.7.0 version. Due to the limitations of personal understanding, if there is an error in the article, thank you for your correction.


SPI, the full name service provider interface, is a service discovery mechanism built into the JDK, and it is a dynamic replacement discovery mechanism. The SPI in the JDK is interface-oriented programming. The service rule provider will provide the access interface in the core of the JRE, and the specific implementation will be provided by other developers.

Human words: JDK provides some functional interface classes. Whoever wants to provide this functionality will implement this interface.


For example, for our commonly used database driver interface, we need to use different driver classes when connecting to different databases, and the specification developer defines the database driver interface java.sql.Driver in rt.jar. For different vendors (such as Mysql) And Oracle), their driver implementation is definitely different, then it is up to them to implement this interface by themselves. Developers just call, no matter how the underlying implementation is.

But how does the JDK know which class is the implementation class of java.sql.Driver? It is always impossible to scan and judge globally, which is time-consuming and labor-intensive. Therefore, the JDK provides a rule: the manufacturer that implements the driver class creates a file named SPI interface class (here, Java.sql.Driver) in the META-INF/services directory of their Jar package. The content of the file is the SPI interface class. The full path name of the implementation class (here refers to the implementation class of Mysql for the java.sql.Driver interface) .

For example, the following Mysql and Oracle:
Insert picture description hereInsert picture description here


There is also a similar SPI extension mechanism in Spring. The difference is that Spring is implemented through the META-INF/spring.factories file. This file is easily reminiscent of Springboot's automatic assembly mechanism. I personally think that Spring's automatic assembly is based on the SPI mechanism. The extended usage of the above.

2. SPI in JDK

The basic concepts of SPI are introduced above, let's write a simple Demo to demonstrate:

1 Simple to use

  1. Create the interface class that needs to be provided externally, and his two implementation classes

    // SPI 接口类
    public interface SpiDemoService {
          
          
        String sayName();
    }
    // 厂商A 对 SPI  接口的实现
    public class ASpiDemoServiceImpl implements SpiDemoService {
          
          
        @Override
        public String sayName() {
          
          
            return "ASpiDemoServiceImpl.sayName";
        }
    }
    // 厂商B 对 SPI 接口的实现
    public class BSpiDemoServiceImpl implements SpiDemoService {
          
          
        @Override
        public String sayName() {
          
          
            return "BSpiDemoServiceImpl.sayName";
        }
    }
    
  2. Create a file in the META-INF/services directory. The file name is the full path name of the interface class provided to the outside world, and the content is the full path name of the implementation class you choose to use. That is, we have specified the implementation method ASpiDemoServiceImpl using vendor A here
    Insert picture description here

  3. Load and use through ServiceLoader

    public class SpiApplication {
          
          
    	
        public static void main(String[] args) {
          
          
          	/** 
             * 输出
             * s = ASpiDemoServiceImpl.sayName
             */
            ServiceLoader<SpiDemoService> load = ServiceLoader.load(SpiDemoService.class);
            for (SpiDemoService spiDemoService : load) {
          
          
                String s = spiDemoService.sayName();
                System.out.println("s = " + s);
            }
        }
    }
    

2 The realization principle of SPI

Let's take the above Demo as an example. What needs attention is ServiceLoader<SpiDemoService> load = ServiceLoader.load(SpiDemoService.class);that the code is as follows:

    public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
    	// 1. 获取当前线程的类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
      	// 2. 创建ServiceLoader 对象
        return ServiceLoader.load(service, cl);
    }
	
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
    
    
        return new ServiceLoader<>(service, loader);
    }
    
    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();
    }
    
    public void reload() {
    
    
    	// 清空缓存信息
        providers.clear();
        // 初始化懒加载迭代器
        lookupIterator = new LazyIterator(service, loader);
    }

2.1 Get the class loader of the current thread

Java core APIs (such as rt.jar) are loaded through the Bootstrap ClassLoader class loader. And ServiceLoader is the class provided by rt.jar, but a class is loaded by the class loader, then the class that this class depends on is also loaded by the same class loader . According to this reason, the SPI extension implementation class provided by the user should also pass Bootstrap ClassLoader is loaded by the class loader. However, users should use AppClassLoader class is loaded . Therefore, a method that violates the parental delegation model is adopted at this time: JDK solves this problem by obtaining the current thread context class loader. And you can see that cl is passed along with ServiceLoader.load(service, cl). Specific usage scenarios, we will talk about it below.

2.2 Create a ServiceLoader object

The above can be seen ServiceLoader.load(service, cl)through a multilayer jump, eventually fell lookupIterator = new LazyIterator(service, loader);in.

LazyIterator is a lazy-loaded iterator by looking at the name. The guess is that it will only be initialized when the object in the iterator is actually obtained. Let’s click here first.
Let’s take a look at the ServiceLoader. SpiApplication will become the following code after compilation (because the for loop of the iterator is essentially just a kind of syntactic sugar, and it will be "exactly revealed" after compilation), and

public class SpiApplication {
    
    
    public SpiApplication() {
    
    
    }

    public static void main(String[] args) {
    
    
        ServiceLoader<SpiDemoService> load = ServiceLoader.load(SpiDemoService.class);
        Iterator var2 = load.iterator();
        while(var2.hasNext()) {
    
    
            SpiDemoService spiDemoService = (SpiDemoService)var2.next();
            String s = spiDemoService.sayName();
            System.out.println("s = " + s);
        }

    }
}

The ServiceLoader implements the Iterable interface, so the actual implementation of Iterator here is the return of the ServiceLoader#iterator method. As follows, we only look at the implementation of the ServiceLoader#iterator method. You can see that the ServiceLoader#iterator method delegates all the logic to lookupIterator for processing. , And lookupIterator is the LazyIterator we initialized at the beginning.

   public Iterator<S> iterator() {
    
    
        return new Iterator<S>() {
    
    
			// 将缓存的信息,转换成迭代器。
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
    
    
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
    
    
            	// 首先从缓存中尝试获取。
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // 这里的 lookupIterator 就是上面 初始化时的  LazyIterator
                return lookupIterator.next();
            }

            public void remove() {
    
    
                throw new UnsupportedOperationException();
            }

        };
    }

So we are here to look at the ServiceLoader.LazyIteratorconcrete realization:

    private static final String PREFIX = "META-INF/services/";
    
    private class LazyIterator
        implements Iterator<S>
    {
    
    
		// SPI 接口类
        Class<S> service;
        // 上面获取到的 当前上下文线程的类加载器。
        ClassLoader loader;
        Enumeration<URL> configs = null;
        // 用来保存SPI 文件解析出来的 SPI 实现类的全路径名
        Iterator<String> pending = null;
        String nextName = null;
		
        private LazyIterator(Class<S> service, ClassLoader loader) {
    
    
            this.service = service;
            this.loader = loader;
        }
		
        private boolean hasNextService() {
    
    
            if (nextName != null) {
    
    
                return true;
            }
            if (configs == null) {
    
    
                try {
    
    
                	// 拼接 META-INF/services/ 路径,获取到SPI 接口文件路径,并进行加载获取
                    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);
                }
            }
            // 如果 pending 还没有加载过,或者不存在元素,则进行加载
            while ((pending == null) || !pending.hasNext()) {
    
    
                if (!configs.hasMoreElements()) {
    
    
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            // 保存下一个 SPI 实现类的 全路径名
            nextName = pending.next();
            return true;
        }
        
		// 返回SPI 实现类
        private S nextService() {
    
    
        	// 判断是否存在下一个实现类,这里给 nextName 进行赋值
            if (!hasNextService())
                throw new NoSuchElementException();
            // 获取下一个SPI 实现类的全路径类名
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
    
    
            	// 通过反射获取到SPI 实现类的实例,这里需要注意的是,这里指定了类加载器为线程上下文的类加载器,也就是 AppClassLoader
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
    
    
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
    
    
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
    
    
                S p = service.cast(c.newInstance());
                // 将SPI 实现类缓存到providers中,providers 是一个 LinkedHashMap
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
    
    
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

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

        public void remove() {
    
    
            throw new UnsupportedOperationException();
        }

    }

After analyzing LazyIterator, we can basically guess the whole logic.
which is:

  1. ServiceLoader.load After specifying the SPI interface class, get the class loader of the current thread context
  2. Rely on generating a ServiceLoader return based on the SPI interface class. At this point ServiceLoaderin the initialization of a lazy loading iterator LazyIterator.
  3. When we call ServiceLoaderupon iteration, ServiceLoadercalls LazyIteratorto iterate.
  4. LazyIterator When judging whether there is an element, it will load the SPI interface file under META-INF/services to obtain the SPI implementation class and cache it (the second judgment will not be reloaded).
  5. Only when the SPI implementation class is obtained through the next method will the implementation class be created through reflection (this is why it is called lazy loading iterator).

3. Loading the Driver

Combined with the above analysis, let's take a brief look at the loading of the database-driven Driver.


We vaguely remember the original driver loading:

// 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
Connection connection = DriverManager.getConnection("url", "user", "password");

Here we look directly at DriverManager, the following code is simplified

public class DriverManager {
    
    
	.... 
	// 静态代码块
   	static {
    
    
       loadInitialDrivers();
       println("JDBC DriverManager initialized");
   	}
	
	....
	
    private static void loadInitialDrivers() {
    
    
    	..... 
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
    
    
            public Void run() {
    
    
				// 通过SPI 加载 Driver 
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

             	// 因为是懒加载迭代器,所以这里需要通过next 将其实例化。
                try{
    
    
                    while(driversIterator.hasNext()) {
    
    
                        driversIterator.next();
                    }
                } catch(Throwable t) {
    
    
                // Do nothing
                }
                return null;
            }
        });

     .....
    }

Five, Dubbo's enhanced SPI

For the SPI of the SDK, Spring has been enhanced through the spring.factories file, but this is not the focus of this article, so I will not list it for the time being. If necessary, please refer to: Spring source code analysis 11: Springboot automatic assembly .


Dubbo's extension point mechanism is enhanced based on the SPI in the SDK and solves the following problems:

  1. The JDK standard SPI will instantiate all implementations of extension points at one time. If some extension points are time-consuming to initialize but are useless, loading is a waste of resources. For example, the Mysql and Oracle database drivers mentioned above. When these two packages are introduced, even if we only need to use one of them, the implementation of the other driver will be initialized.
  2. If the extension point fails to load, the user will not be kindly notified of the specific exception, and the exception message may be incorrect.
  3. Added support for extension points Ioc and Aop. An extension point can directly use the setter() method to inject other extension points, or use the Wrapper class to enhance the function of the extension point.

Due to space limitations, please refer to: Dubbo Notes Derivatives ②: Dubbo SPI Principle


Above: For the content, refer to
"In-Depth Analysis of Apache Dubbo Core Technology Insider"
https://my.oschina.net/kipeng/blog/1789849
https://www.cnblogs.com/helloz/p/10961026.html
https://
If www.cnblogs.com/LUA123/p/12460869.html is intrusive, please contact to delete it. The content is only used for self-record learning. If there is any error, please correct me

Guess you like

Origin blog.csdn.net/qq_36882793/article/details/114639136