Official account: Mufeng Technical Notes

A true master always has the heart of an apprentice

The introduction

In daily project development, in order to improve the program’s expansibility, we often use the interface oriented programming idea to program. This not only embodies the program design closed for modification, open for expansion of the program design principle, but also realizes the program pluggable. So the SPI mechanism described in this paper is the embodiment of this programming idea. Today I’m going to tell you what SPI really is. By the way, take a look at how the SPI mechanism is used in the Seata framework to implement framework extensions.

What is the SPI

In the general development logic, it is the service provider to define the interface and different implementations, and the service caller to complete a business call through API. However, this approach lacks flexibility for service callers to load different implementations according to their own needs. So is there a mechanism to give callers more decision-making power? That’s when SPI, the main character of the day, makes a grand entrance.

Service Provider Interface (SPI) is a Service Provider Interface. I don’t know what it means. As I understand it, it is a service discovery mechanism. The essence is to decouple the interface from the implementation. Unlike the API approach where the service implementers provide the interface definition, SPI requires the service caller to declare the interface, and the implementation is implemented by a third party. Simply put, SPI is the party A of life. You, the party B, have to do what I want you to do if you want to work with me. In this way, the caller has more flexibility to load qualified implementations based on his actual needs. This improves the extensibility of the program and allows service providers to program toward the interface.

How do you use such a great extension mechanism? We only need to create a file named after the service interface in the META-INF/services/ directory of the JAR package. This file contains the name of the implementation class that implements the service interface. When an external application assembs this module, it can use the jar package meta-INF /services/ config file to find the specific implementation class name and load the instantiation to complete the loading and injection of the implementation class.

The user provides the rule specification, and the actual service provider does the implementation. In fact, this idea is similar to component scanning in Spring, in that the service provider first specifies the rules, and the framework automatically discovers the service according to the specification.

Here we go. Here we go. Here we go. Since then, we can find that both the SPI mentioned in this article and the automatic configuration principle in SpringBoot are actually a development idea of convention over configuration. Specific implementation is carried out through the agreed content in advance, so as to improve the scalability of the program. So hope everyone looking at a technology, in addition to pay attention to technical details, longitudinal understanding, also want to focus on the horizontal contrast technology, so as to find these technologies in common, the understanding of the design ideas behind it, I always feel this is very important, after all, style has always been in change, but internal work practice is more important. This is probably in the book of leaning on heaven and killing dragons.

SPI implementation Analysis

1, SPI use

Take the Mysql Driver loading as an example, define the template interface to be extended, that is, the java.sql.Driver interface. Each database vendor can have the characteristics of their own database for the corresponding driver development, but to comply with this template interface.

In the Mysql driver package, in the meta-INF /services/ directory of the Classpath path, create a file with exactly the same name as the service interface. In this file, store the fully qualified name of the implementation class of the template interface.

Implement concrete classes in the corresponding directories that implement the java.sql.Driver interface.

Specific code implementation, through the ServiceLoader to load the corresponding implementation class, complete the class instantiation operation. The ServiceLoader can also be customized, as frameworks like Dubbo and Seata define their own class loaders.


public final class ServiceLoader<S>
    implements 可迭代<S>
{
  
   private static final String PREFIX = "META-INF/services/"; .public static <S> ServiceLoader<S> load(Class service, ClassLoader loader)
{
        return new ServiceLoader<>(service, loader);
    }

  public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

   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(); }... }Copy the code

Let’s take a look at the workflow of the ServiceLoader, starting with serviceloader.load (). Obtain the ClassLoader bound to the current thread. If the ClassLoader bound to the current thread is null, use SystemClassLoader instead. Then clear the Provider cache and create a LazyIterator. LazyIterator LazyIterator

private class LazyIterator implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null; .public boolean hasNext(a) {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run(a) { returnhasNextService(); }};returnAccessController.doPrivileged(action, acc); }}...private boolean hasNextService(a) {
            if(nextName ! =null) {
                return true;
            }
            if (configs == null) {
                try {
                  //key: gets the fully qualified name
                    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); }}while ((pending == null) | |! pending.hasNext()) {if(! configs.hasMoreElements()) {return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true; }... }Copy the code

Key: Specifies the address of the class with a predefined directory address and the name of the class. The class loader loads the specific implementation class from this address.

The general SPI loading process is as follows:

How does Seata use SPI

Seata is a distributed transaction framework, the specific use of which is not described here, can be devoted to it when there is time. This section focuses on how Seata leverages SPI to extend the framework’s capabilities.

The Seata framework implements service loading using EnhancedServiceLoader, which by its name is an EnhancedServiceLoader. So how is it better than the JDK’s own ServiceLoader?

As you can see from the figure below, EnhancedServiceLoader supports not only Java native service discovery directories, but also its own custom meta-inf /seata/ directory.

In addition, there are @loadLevel annotations on the specific interface implementation class. If there are multiple configuration center implementation classes loaded, then the corresponding annotation can be sorted according to the attribute order. Load the class with the highest actual priority.

We all know that a registry is an essential component of a microservice architecture. It records the address information of a service provider. In Seata, the clients of Seata, such as transaction manager TM and resource manager RM, need to communicate with transaction coordinator TC, so they need to obtain the address information of the server through the registry. Seata Registry supports multiple third party registries such as Consul, Apollo, Etcd3, and more. Let’s take a look at how Seata uses the SPI mechanism to extend support for multiple registries.

First, define an interface for ConfigurationProvider. If you smell something familiar, you need to set the rules for your buddies.

Then the corresponding package meta-inf/services/defined in the specific implementation class, so the Consul defines ConsulConfigurationProvider configuration center.

We can see ConsulConfigurationProvider realized ConfigurationProvider interface.


I’m Mufeng. Thanks for your likes, favorites and comments. I’ll see you next time!

Wechat search: Mufeng technical notes, quality articles continue to update, we have learning punch group can pull you in, together with the impact of the big factory, in addition to a lot of learning and interview materials to provide you.