SPI mechanism in springboot-starter

SPI mechanism in springboot-starter

The full name of SPI is Service Provider Interface, literally translated as "Service Provider Interface", it sounds awkward, so I tried to translate it as "Service Provider Interface".

We all know that an interface can be implemented in many ways. For example, search can be the hard disk of the search system or the search database. In order to reduce coupling, the designer of the system does not want to hard-code specific search methods. , But it is hoped that the service provider will choose which search method to use. At this time, you can choose to use the SPI mechanism.

The SPI mechanism is widely used in various open source frameworks, such as:

  1. The ExtensionLoader in dubbo that everyone is familiar with can add some custom plug-in functions through these extension points, such as adding filters to achieve whitelist access, implementing functions such as interface current limiting; or directly replacing its native protocol, transport, etc.
  2. When developing the idea intellij plug-in, you need to define a/META-INF/plugin.xml file. There are many places in this plugin.xml to configure serviceInterface and serviceImplementation. This is also an SPI mechanism. Through this mechanism, idea It enables plug-in developers to use the API provided by its underlying SDK, but also allows developers to have customized functions, and the coupling is quite low. Intellij's plug-in development directly uses the ServiceLoader in the JDK
  3. The SPI mechanism is also widely used in spring, and this article will analyze a part of it.

SPI in JDK

It is estimated that everyone knows about SPI, let us review the SPI mechanism in java through a very simple example.

  1. Define a search interface Search
 package com.north.spilat.service;
 import java.util.List;
 public interface Search {
     List<String> search(String keyword);
 }
 
  1. Implement the interface to query from the database
    package com.north.spilat.service.impl;
    import com.north.spilat.service.Search;
    import java.util.List;
   /**
     * @author lhh
     */
    public class DatabaseSearch implements Search {
        @Override
        public List<String> search(String keyword) {
            System.out.println("now use database search. keyword:" + keyword);
            return null;
        }
    
    }
 
  1. Implement the interface to query from the file system
 package com.north.spilat.service.impl;
 import com.north.spilat.service.Search;
 import java.util.List;
/**
  * @author lhh
  */
 public class FileSearch implements Search {
 
     @Override
     public List<String> search(String keyword) {
         System.out.println("now use file system search. keyword:" + keyword);
         return null;
     }
 
 }
 
  1. Create a directory META-INF\services\com.north.spilat.service.Search in src\main\resources, and then create two files under com.north.spilat.service.Search, based on the specific implementation class of the above interface The fully qualified name is the file name, namely:
    com.north.spilat.service.impl.DatabaseSearch
    com.north.spilat.service.impl.FileSearch The
    entire project directory is as follows:

  2. Create a new main method to test

 package com.north.spilat.main;
 import com.north.spilat.service.Search;
 import java.util.Iterator;
 import java.util.ServiceLoader;
 public class Main {
     public static void main(String[] args) {
         System.out.println("Hello World!");
         ServiceLoader<Search> s = ServiceLoader.load(Search.class);
         Iterator<Search> searchList = s.iterator();
         while (searchList.hasNext()) {
             Search curSearch = searchList.next();
             curSearch.search("test");
         }
     }
 }

 

Run it, the output is as follows:

Hello World!
now use database search. keyword:test
now use file system search. keyword:test
 

As you can see, the SPI mechanism has defined the process framework for loading services. You only need to create a folder (com.north.spilat) under the META-INF/services directory under the META-INF/services directory according to the convention. .service.Search), put the fully qualified name of the specific implementation class (com.north.spilat.service.impl.DatabaseSearch) under the folder, and the system can load different implementation classes according to these files. This is the SPI The general process.

ServiceLoader class analysis

Going back to the main method above, there is actually nothing special, except for the sentence
ServiceLoader.load(Search.class);

ServiceLoader.class is a tool class that loads specific implementation classes according to the file name under META-INF/services/xxxInterfaceName.

Go in from load(Search.class), let's take a look at this class, the following is mainly to paste the code, and the analysis is in the code comments.

  1. As you can see, there is not a lot of logic inside, and the main logic is handed over to LazyIterator.
/*
 * ,  , 
 */
 public static <S> ServiceLoader<S> load(Class<S> service) {
     ClassLoader cl = Thread.currentThread().getContextClassLoader();
     return ServiceLoader.load(service, cl);
 }
/*
 * , 
 */
 public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)
 {
     return new ServiceLoader<>(service, loader);
 }
/**
 *  , reload
 */
 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);
 }
 
  1. The iterator of LazyIterator only needs to care about hasNext() and next(), and hasNext() simply calls hasNextService(). Needless to say, next() must also simply call nextService();
 private boolean hasNextService() {
     if (nextName != null) {
        //nextName , ,  
         return true;
     }
    //configs PREFIX + service.getName() 
     if (configs == null) {
         try {
            //PREFIX /META-INF/services
            //service.getName()  
             String fullName = PREFIX + service.getName();
            //loader == null,  bootstrap 
             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;
             }
             
            //parse parseLine, :
            //1.  PREFIX + service.getName()  
            //2.  java , add pending 
             pending = parse(service, configs.nextElement());
     }
    // , 
     nextName = pending.next();
     return true;
 }
 
  1. Let's take a look at what nextService does
 private S nextService() {
    // 
     if (!hasNextService())
             throw new NoSuchElementException();
     String cn = nextName;
     nextName = null;
     Class<?> c = null;
     try {
        // 
         c = Class.forName(cn, false, loader);
     } catch (ClassNotFoundException x) {
         fail(service,"Provider " + cn + " not found");
     }
    // service , 
     if (!service.isAssignableFrom(c)) {
         fail(service,"Provider " + cn  + " not a subtype");
     }
     try {
        // ,  
         S p = service.cast(c.newInstance());
        // , 
         providers.put(cn, p);
         return p;
     } catch (Throwable x) {
         fail(service,"Provider " + cn + " could not be instantiated",x);
     }
     throw new Error();         //This cannot happen
 }
 

As can be seen from the above code, the so-called lazy loading is to wait until hasNext() is called to find the service, and then next() is called to instantiate the service class.

JDK's SPI is probably such a logic. The service provider puts the specific implementation class name under/META-INF/services/xxx according to the agreement, and ServiceLoader can load different implementations according to the wishes of the service provider. Avoid hard-coding hard-coded logic, so as to achieve the purpose of decoupling.

Of course, from the simple example above, you may not be able to see how SPI achieves the decoupling effect. So let's take a look at how to use the SPI mechanism to decouple in the open source framework. Experience SPI Charm.

SPI in springboot

As a programmer, there is nothing you can do more research on open source frameworks, because these open source codes are not known to be used several times every day, so their code is very good from design to implementation, and we can learn a lot from it. .

In the past few years, the spring framework has basically been the leader of the open source industry, and no one knows it. Its source code design is also famous for its elegance, ultra-high scalability and ultra-low coupling.

How does it decouple? The extension point mechanism is one of the magic weapons

Speaking from the magical starter

When I first came into contact with springboot, I really felt that all kinds of spring-xx-starter and xx-spring-starter are very magical. Why can I add a dependency to the pom file to introduce a complex plug-in? With this question, I started It's my journey into science.

The dubbo framework is used by many companies in China, so here, let's take dubbo-spring-boot-starter as an example to see how springboot is efficiently decoupled.

Recall, if we want to introduce the dubbo module into the springboot project, what we need to do.

  1. Introduce the dependency of dubbo-spring-boot-starter in the pom file.
        <dependency>
            <groupId>com.alibaba.spring.boot</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
 
  1. Configure the relevant parameters of dubbo in the application.properties file
spring.dubbo.server=true
spring.dubbo.application.name=north-spilat-server

#
spring.dubbo.registry.id=defaultRegistry
#
spring.dubbo.registry.address=127.0.0.1
#
spring.dubbo.registry.port=2181
#
spring.dubbo.registry.protocol=zookeeper
#
spring.dubbo.protocol.name=dubbo
#
spring.dubbo.protocol.port=20881
#
spring.dubbo.module.name=north-spilat-server
#
spring.dubbo.consumer.check=false
#
spring.dubbo.provider.timeout=3000
#
spring.dubbo.consumer.retries=0
#
spring.dubbo.consumer.timeout=3000
 
  1. Add corresponding annotations to the startup class of spring-boot
package com.north.spilat.main;

import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

/**
 * @author lhh
 */
@SpringBootApplication
@ComponentScan(basePackages = {"com.north.*"})
@EnableDubboConfiguration
public class SpringBootMain {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootMain.class, args);
    }
}
 
  1. Define the interface, implement and call

interface

package com.north.spilat.service;
/**
 * @author lhh
 */
public interface DubboDemoService {
    String test(String params);
}
 

Implement the interface

package com.north.spilat.service.impl;

import com.alibaba.dubbo.config.annotation.Service;
import com.north.spilat.service.DubboDemoService;
import org.springframework.stereotype.Repository;

/**
 * @author lhh
 */
@Service
@Repository("dubboDemoService") 
public class DubboDemoServiceImpl implements DubboDemoService {
    @Override
    public String test(String params) {
        return System.currentTimeMillis() + "-" + params ;
    }
}
 

Write a controller to call the dubbo interface

package com.north.spilat.controller;

import com.north.spilat.service.DubboDemoService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author lhh
 */
@RestController
public class HelloWorldController {
    @Resource
    private DubboDemoService dubboDemoService;

    @RequestMapping("/saveTheWorld")
    public String index(String name) {
        return   dubboDemoService.test(name);
    }
}
 

After completing the above 4 steps (install the zookeeper and other environments by yourself), start the SpringBootMain class, and a springboot project with dubbo module is set up, it's really that simple.

However, there are no years in the world that are quiet, it's just that someone is carrying the weight for you, and the person who is carrying the weight for you is "dubbo-spring-boot-starter"

The mystery of dubbo-spring-boot-starter

The picture above is the structure of the dubbo-spring-boot-starter.jar package. There is a lot of content, but you must be smart. Since we mentioned in the previous section that SPI is closely related to META-INF, then our The section must be the same.
Therefore, here we first look at what is under the META-INF directory.

dubbo/com.alibaba.dubbo.rpc.InvokerListener

dubbosubscribe=com.alibaba.dubbo.spring.boot.listener.ConsumerSubscribeListener
 

The file in this directory is only one line, and it looks like the SPI of the jdk above. Yes, this is indeed an extension point. It is an extension point convention in dubbo, which is the ExtensionLoader we said at the beginning.

  1. spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.dubbo.spring.boot.DubboAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration

org.springframework.context.ApplicationListener=\
com.alibaba.dubbo.spring.boot.context.event.DubboBannerApplicationListener

 

Wow, the file is named after spring, and the content of the file involves so many spring classes. After confirming my eyes, I met the right... file. But don t worry, there is a spring.providers file below

  1. spring.providers
provides: dubbo-spring-boot-starter
 

spring.providers is such a simple sentence, a bit disappointed. So let's pay attention to spring.factories.

Search for the spring.factories file in IDEA. I don't know if I don't search it. I was surprised when I searched. It turns out that there is such a file in every springboot-related jar package.

Before doing experiments, physicists always like to reason, get a predicted conclusion, and then use the experimental results to confirm or overturn the predicted conclusion.

Therefore, based on the SPI mechanism in the JDK, we can also make a bold prediction here: there must be a class similar to ServiceLoader in the spring framework, which specifically loads specific interfaces from the configuration in META-INF/spring.factories achieve.

Needless to say, the prediction must be accurate, otherwise I won t write so many words in vain. But how can we prove that our prediction is accurate? Let us also do an "experiment".

The startup process of springboot

To figure out the startup process of springboot, the best way is to study its source code.

The springboot code is still very "humane", springboot tells you clearly, its entrance is the main method. Therefore, reading the springboot code is quite comfortable, just look at it all the way from the main method. .

The above picture is the startup process of a springboot project. 1. there are two consecutive overloaded static run methods. The static run method will call the constructor to instantiate the SpringApplication object, and the constructor will call initialiaze() for initialization and instantiation. Call another member method run() to officially start.

It can be seen that the main logic of the entire startup process is inside the initialiaze method and the member run method.

Take a look at the logic of initialiaze(), the following is also the old rule, the main code is posted, and the analysis is in the code comments

   @SuppressWarnings({ "unchecked", "rawtypes" })
   private void initialize(Object[] sources) {
      //sources Configuration main 
      // 
   	if (sources != null && sources.length > 0) {
   		this.sources.addAll(Arrays.asList(sources));
   	}
   	// web 
   	//classLoader 
   	//"javax.servlet.Servlet",
   	//	"org.springframework.web.context.ConfigurableWebApplicationContext"
   	// web 	
   	this.webEnvironment = deduceWebEnvironment();
   	// initializers  listeners
   	//getSpringFactoriesInstances ,
   	// ,
   	// "ServiceLoader" 
   	setInitializers((Collection) getSpringFactoriesInstances(
   			ApplicationContextInitializer.class));
   	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   	// main 
   	this.mainApplicationClass = deduceMainApplicationClass();
   }
 

Fortunately, the "suspect" getSpringFactoriesInstances is exposed, let s take a look at its logic

   /**
    *  type class
    */
    private <T> Collection<? extends T>
    getSpringFactoriesInstances(Class<T> type) {
       // getSpringFactoriesInstances
		return getSpringFactoriesInstances(type, new Class<?>[] {});
	}

	private <T> Collection<? extends T>
	        getSpringFactoriesInstances(Class<T> type,
			Class<?>[] parameterTypes, 
			Object... args) {
		// classLoader	
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		//Use names and ensure unique to protect against duplicates
		// names 
		// ,  "ServiceLoader" 
		// SpringFactoriesLoader
		Set<String> names = new LinkedHashSet<String>(
				SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		// java 		
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
				classLoader, args, names);
		// @Order 		
		AnnotationAwareOrderComparator.sort(instances);
		// 
		return instances;
	}
 

Then I quickly found the SpringFactoriesLoader we were looking for, and this class is very small, and the code is less than JDK ServiceLoader. Then let's take a closer look at what it contains.

  1. FACTORIES_RESOURCE_LOCATION points to the META-INF/spring.factories we mentioned above
  2. loadFactories, find the specified interface implementation class from META-INF/spring.factories and instantiate it, where the search is by calling loadFactoryNames
  3. loadFactoryNames finds the fully qualified name of the implementation class of a specific interface from a specified location
  4. instantiateFactory instantiation

This class is the "ServiceLoader" in springboot. It provides a mechanism that allows the service provider to specify the implementation of a certain interface (there can be multiple), such as the ApplicationContextInitializer.class and ApplicationListener.class interfaces above, if we want Specify our implementation in our module, or if you want to add one of our implementation to the existing code, you can specify it in/META-INF/spring.factories. Wait a minute I will write a specific example , Can let everyone understand better.

/**
*  import
**/
public abstract class SpringFactoriesLoader {

	private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);

	/**
	 * The location to look for factories.
	 *   
	 * <p>Can be present in multiple JAR files.
	 *    jar 
	 *  META-INF/spring.factories 
	 *  
	 */
	public static final String FACTORIES_RESOURCE_LOCATION =
	"META-INF/spring.factories";


	/**
	 *  
	 */
	public static <T> List<T> loadFactories(Class<T>
	factoryClass, ClassLoader classLoader) {
		Assert.notNull(factoryClass, "'factoryClass' 
		must not be null");
		ClassLoader classLoaderToUse = classLoader;
		if (classLoaderToUse == null) {
			classLoaderToUse =
			SpringFactoriesLoader.class.getClassLoader();
		}
		// loadFactoryNames
		List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
		if (logger.isTraceEnabled()) {
			logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
		}
		List<T> result = new ArrayList<T>(factoryNames.size());
		for (String factoryName : factoryNames) {
		   // 
			result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
		}
		// 
		AnnotationAwareOrderComparator.sort(result);
		return result;
	}

	/**
	 *  META-INF/spring.factories 
	 *  
	 */
	public static List<String> loadFactoryNames(
	Class<?> factoryClass, ClassLoader classLoader) {
	   // 
		String factoryClassName = factoryClass.getName();
		try {
		   //META-INF/spring.factories 
			Enumeration<URL> urls = 
			(classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
			ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			List<String> result = new ArrayList<String>();
			while (urls.hasMoreElements()) {
			   // url spring.factories 
				URL url = urls.nextElement();
				// ,   xxx =impl1,impl2  
				Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
				// "impl1,impl2" 
				String factoryClassNames = properties.getProperty(factoryClassName)
				// , 
				result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
			}
			// 
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
					"] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}

   /**
    *  
    */
	@SuppressWarnings("unchecked")
	private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
		try {
		   // 
			Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
			// 
			if (!factoryClass.isAssignableFrom(instanceClass)) {
				throw new IllegalArgumentException(
						"Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
			}
			Constructor<?> constructor = instanceClass.getDeclaredConstructor();
			ReflectionUtils.makeAccessible(constructor);
			// 
			return (T) constructor.newInstance();
		}
		catch (Throwable ex) {
			throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
		}
	}

}

 

After reading the SpringFactoriesLoader class, the logic of the initialize() method has also been read. Then look at another important method run(String...args)

/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
	   // 
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		//springboot 
		ConfigurableApplicationContext context = null;
		FailureAnalyzers analyzers = null;
		// headless 
		configureHeadlessProperty();
		// ,  spring.factories 
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
		   // 
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			//	 environment	
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			// banner		
			Banner printedBanner = printBanner(environment);
			// 
			context = createApplicationContext();
			analyzers = new FailureAnalyzers(context);
			// 
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			//spring  refresh() ,  
			// ,  
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			listeners.finished(context, null);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			return context;
		}
		catch (Throwable ex) {
			handleRunFailure(context, listeners, analyzers, ex);
			throw new IllegalStateException(ex);
		}
	}
 

This method is the main logic of springboot startup. There is a lot of content. If you want to make it all clear, I am afraid that I will not be able to write the article a few times. The whole framework, people don t want to face it). So I won t go further here. For this article, just know that the run() method is the main logic of startup. Also remember that
context = createApplicationContext();
refreshContext(context) ;
These two lines of code, we will see it later.

The principle of dubbo-spring-boot-starter

A lot has been said above, but why springboot introduces a starter dependency and can introduce a complex module. Here, let's study it through dubbo-spring-boot-starter.

Let's check the spring.factories in dubbo-spring-boot-starter. We can find that there are two interfaces configured, one is EnableAutoConfiguration and the other is ApplicationListener.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.dubbo.spring.boot.DubboAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,\
com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration

org.springframework.context.ApplicationListener=\
com.alibaba.dubbo.spring.boot.context.event.DubboBannerApplicationListener
 

The listener looks at the name and knows that it is used to print the banner when it starts, so I won't look at it here for the time being, let's take a look at where EnableAutoConfiguration is used.

Debugging all the way from the main method, finally found a line of code in the AutoConfigurationImportSelector class:
SpringFactoriesLoader.loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())

Among them, getSpringFactoriesLoaderFactoryClass() is hard-coded and returns EnableAutoConfiguration.class

 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
 		AnnotationAttributes attributes) {
 	List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
 			getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
 	Assert.notEmpty(configurations,
 			"No auto configuration classes found in META-INF/spring.factories. If you "
 					+ "are using a custom packaging, make sure that file is correct.");
 	return configurations;
 }

/**
  * Return the class used by {@link SpringFactoriesLoader} to load configuration
  * candidates.
  * @return the factory class
  */
 protected Class<?> getSpringFactoriesLoaderFactoryClass() {
 	return EnableAutoConfiguration.class;
 }
 

As shown in the figure below, there will be many implementations of EnableAutoConfiguration.class, as long as you configure it in spring.fatories, it will be loaded for you

After loading, what are you doing? Looking down, you can find that the approximate process is like this:

  1. this.reader.loadBeanDefinitions(configClasses); configClasses are all implementation classes, read these classes in to prepare for analysis
  2. registerBeanDefinition registered to beanDefinitionNames
  3. In spring's refresh() operation, the last step is finishBeanFactoryInitialization(beanFactory), this step will initialize all singleton objects, and finally all BeanDefinitions will be read from beanDefinitionNames, including all the EnableAutoConfiguration implementations above, and then perform examples Transform
  4. When the specific implementation of EnableAutoConfiguration is instantiated, the specific logic in these implementation classes will be executed. Taking Dubbo as an example, it will initialize com.alibaba.dubbo.spring.boot.DubboAutoConfiguration,
    com.alibaba.dubbo.spring.boot.DubboProviderAutoConfiguration,
    The three implementation classes com.alibaba.dubbo.spring.boot.DubboConsumerAutoConfiguration start dubbo and register it in the spring container.

Implement a spring-boot-starter

After understanding the principle, it is very simple to implement your own starter.

Suppose I have a component that is very awesome and has the ability to save the world. After your system is connected, it also has the ability to save the world. Then how can your spring-boot system be quickly connected to this awesome Components. I will implement a starter, you can rely on my starter

First define an interface to save the world

package com.north.lat.service;

/**
* @author lhh
*/
public interface SaveTheWorldService {
/**
  *   
  * @param name  
  * @return
  */
 String saveTheWorld(String name);
}
 

Abstract class

package com.north.lat.service;

import lombok.extern.log4j.Log4j;

import java.util.Random;

/**
 * @author lhh
 */
@Log4j
public abstract  class AbstractSaveTheWorldService implements SaveTheWorldService {
    private final static Random RANDOM = new Random();
    private final static String SUCCESS_MSG = "WAOOOOOOO!  ";
    private final static String FAIL_MSG = " ";

    @Override
    public String saveTheWorld(String name) {
        int randomInt = RANDOM.nextInt(100);
        String msg;
        if((randomInt +  1) > getDieRate()){
            msg = SUCCESS_MSG +"," + name + " !";
        }else{
            msg = FAIL_MSG + "," + name + ", , ";

        }
        log.info(msg);
        return msg;
    }

   /**
     *  
     * @return
     */
    public abstract int getDieRate();
}
 

Ordinary people go to save the world, the general failure rate is 99%

package com.north.lat.service.impl;


import com.north.lat.service.AbstractSaveTheWorldService;

/**
 *  
 * @author lhh
 */
public class CommonSaveTheWorldServiceImpl extends AbstractSaveTheWorldService {
    private final static int DIE_RATE = 99;

    @Override
    public int getDieRate() {
        return DIE_RATE;
    }
}

 

Save the world as a hero, the success rate is 99%

package com.north.lat.service.impl;

import com.north.lat.service.AbstractSaveTheWorldService;

/**
 *  
 * @author lhh
 */
public class HeroSaveTheWorldImpl extends AbstractSaveTheWorldService {
    private final static int DIE_RATE = 1;
    @Override
    public int getDieRate() {
        return DIE_RATE;
    }
}

 

Well, our super awesome component was born. Let's prepare for access to springboot, and implement a NbAutoConfiguration as follows:

package com.north.lat;

import com.north.lat.service.SaveTheWorldService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.SpringFactoriesLoader;

import java.util.List;

/**
 * @author lhh
 *  environment applicationContext  
 */
@Configuration
@ConditionalOnClass(SaveTheWorldService.class)
public class NbAutoConfiguration implements EnvironmentAware,ApplicationContextAware,BeanDefinitionRegistryPostProcessor {
    private Environment environment;
    private ApplicationContext applicationContext;

    @Override
    public void setEnvironment(Environment environment) {
            this.environment = environment;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
           this.applicationContext = applicationContext;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
       // spring.factories SaveTheWorldService ,
        List<SaveTheWorldService> saveTheWorldServices = SpringFactoriesLoader.loadFactories(SaveTheWorldService.class, this.getClass().getClassLoader());
       // BeanDefinitionRegistry  BeanDefinitions
        saveTheWorldServices.forEach(saveTheWorldService->{
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(saveTheWorldService.getClass());
            beanDefinition.setLazyInit(false);
            beanDefinition.setAbstract(false);
            beanDefinition.setAutowireCandidate(true);
            beanDefinition.setScope("singleton");
            registry.registerBeanDefinition(saveTheWorldService.getClass().getSimpleName(), beanDefinition);
        });
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}
 

Configure spring.factories.
In the early stage of component development, the hero has not been found, so I can only send an ordinary person to it, so the spring.factories of niubility-spring-starter-1.0-SNAPSHOT.jar is like this

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.north.lat.NbAutoConfiguration
com.north.lat.service.SaveTheWorldService=\
com.north.lat.service.impl.CommonSaveTheWorldServiceImpl
 

Later, after countless days and nights of overtime by the developers, the hero was finally found, so the spring.factories of niubility-spring-starter-2.0-SNAPSHOT.jar became like this

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.north.lat.NbAutoConfiguration
com.north.lat.service.SaveTheWorldService=\
com.north.lat.service.impl.HeroSaveTheWorldImpl
 

This is complete, and the project structure is shown in the following figure:

How to access it? Let's try it in the spilat project just now:

Depends on the jar package, this time it is connected to version 1.0; this completes the connection

        <dependency>
            <groupId>com.north.lat</groupId>
            <artifactId>niubility-spring-starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
 

The so-called complete access means that all implementations of SaveTheWorldService have been registered in spring, that is, CommonSaveTheWorldServiceImpl (version 1.0) or HeroSaveTheWorldImpl (version 2.0).

Let's inject the call in the controller

package com.north.spilat.controller;

import com.north.lat.service.SaveTheWorldService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author lhh
 */
@RestController
public class HelloWorldController {
    @Resource
    private SaveTheWorldService saveTheWorldService;


    @RequestMapping("/saveTheWorld")
    public String index(String name) {
        return  saveTheWorldService.saveTheWorld(name);
    }
}
 

When using version 1.0, it turned out that the failure rate was 99%, and the running results are as follows:

After version 2.0 comes out, quickly switch to version 2.0 and update the version number in pom.xml:

 
    <dependency>
        <groupId>com.north.lat</groupId>
        <artifactId>niubility-spring-starter</artifactId>
        <version>2.0-SNAPSHOT</version>
    </dependency>
 
 ,  
 

In the above example, whether we access or upgrade the components, we simply rely on the jar package, which truly achieves pluggability and low coupling. Of course, in actual application scenarios, we may also need to increase A few configurations, such as the spring-boot-starter-dubbo above, and the druid-spring-boot-starter, spring-boot-starter-disconf, etc. we often use

summary

Decoupling can be said to be something that several generations of programmers have been pursuing for their entire lives. In the past few years, countless tools and ideas have been proposed and implemented. SPI is one of the precipitates.

The SPI mechanism is very common in various open source frameworks, and the SPI mechanisms of various frameworks are different, and there are more or less evolutions; but in fact, the principles behind them are similar.

Therefore, understanding these mechanisms, on the one hand, can make us more aware of the operating principles of open source frameworks and avoid detours; on the other hand, it can also be used as a reference for our daily code writing and system design, so as to write more elegant code. .