SpringBoot has many EVNet (events), such as: ApplicationStartingEvent, ApplicationEnvironmentPreparedEven, ApplicationContextInitializedEvent, ApplicationPreparedEvent, ApplicationStartedEvent, etc.

These events are triggered on different nodes where Spring is started, and we can use listeners to listen on these times to do some business logic on different nodes.

People usually use @Component or @bean to inject listeners, but some events are fired before the Spring context and cannot be fired this way.

Let’s take a closer look at SpringBoot listeners so you can understand how to use them.

Start with the boot portal

The Springboot listener starts working as soon as it starts, so let’s start with the entry:

@SpringBootApplication
public class SpringDemoApplication {
    public static void main(String[] args) { SpringApplication.run(SpringSecurityDemoApplication.class, args); }}Copy the code

This is the springBoot entry, and we go in from the run method, and we call one of the submethods of the current class.

public class SpringApplication {
	privateList<ApplicationContextInitializer<? >> initializers;privateList<ApplicationListener<? >> listeners;public static ConfigurableApplicationContext run(Class
        primarySource, String... args) {
		return run(newClass<? >[] { primarySource }, args); }public static ConfigurableApplicationContext run(Class
       [] primarySources, String[] args) {
		return newSpringApplication(primarySources).run(args); }}Copy the code

We need to remember that the class we’re talking about is SpringApplication. Then the run method has two operations in one line of code

New SpringApplication(primary resources) and then call the run method with the object of the current class.

PrimarySources is our business entry class, which does not require relationships. Let’s start with the new SpringApplication step

new SpringApplication

Click on that, and you’ll see that this method is called, still in the SpringApplication class.

	public SpringApplication(ResourceLoader resourceLoader, Class
       ... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
        // Record the project main class
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        / / determine webType
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		this.bootstrapRegistryInitializers = new ArrayList<>(
				getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}
Copy the code

In this method, the two lines I commented earlier are simple enough to look at for yourself (few lines of code).

The important thing is that line 8, 9, 10, 11, line 8 loads nothing, and line 11 loads the classes needed for SpringBoot listening.

That, in all three lines use the same methods: getSpringFactoriesInstances (XXX. Class). Here’s what this method does:

GetSpringFactoriesInstances from all the META – INFO/Spring. Spring factories file (your own code and introduce the jar package will scan), looking for the type as the key, the corresponding value of all the classes in And create objects for those classes that you find.

The following code, getSpringFactoriesInstances (ApplicationListener. Class) will find a BackgroundPreinitializer, and create the object.

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
Copy the code

GetSpringFactoriesInstances,

You can skip this section if you don’t want to learn more about the process.

In the current class SpringApplication, the key code is as follows:

public class SpringApplication {
	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
		return getSpringFactoriesInstances(type, newClass<? > [] {}); }private <T> Collection<T> getSpringFactoriesInstances(Class
       
         type, Class
        [] parameterTypes, Object... args)
        {
        // Get the class loader
		ClassLoader classLoader = getClassLoader();
		// Find all corresponding class names based on the incoming class
		Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        // Create an object for the found class
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}

	private <T> List<T> createSpringFactoriesInstances(Class
       
         type, Class
        [] parameterTypes, ClassLoader classLoader, Object[] args, Set
        
          names)
        
        {
		List<T> instances = new ArrayList<>(names.size());
        // loop through the constructor to create the object
		for (String name : names) {
			try{ Class<? > instanceClass = ClassUtils.forName(name, classLoader); Assert.isAssignable(type, instanceClass); Constructor<? > constructor = instanceClass.getDeclaredConstructor(parameterTypes); T instance = (T) BeanUtils.instantiateClass(constructor, args); instances.add(instance); }catch (Throwable ex) {
				throw new IllegalArgumentException("Cannot instantiate " + type + ":"+ name, ex); }}returninstances; }}Copy the code

The main idea is to go under meta-info/spring.Factories to find the class you want and then create the object.

LoadFactoryNames method

Find a class in the process of line 9, call the SpringFactoriesLoader. LoadFactoryNames method, has two parameters, one is the required type, one is the class loader. The code is as follows:

public final class SpringFactoriesLoader {
	public static List<String> loadFactoryNames(Class<? > factoryType,@Nullable ClassLoader classLoader) {
        // Make sure the classloader is not null
		ClassLoader classLoaderToUse = classLoader;
		if (classLoaderToUse == null) {
			classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
		}
        // Get the class name of the class
		String factoryTypeName = factoryType.getName();
		return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
	}

	private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        // Get it from the cache. It must be empty at first
		Map<String, List<String>> result = cache.get(classLoader);
		if(result ! =null) {
			return result;
		}
        // If the cache does not exist, initialize the cache
		result = new HashMap<>();
		try {
            // The classloader loads all meta-info /spring.factories
			Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
            / / loop
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
                // Get a single spring.facories file resource
				UrlResource resource = new UrlResource(url);
                // Parse all key value pairs in the file
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                // Loop through each key, one key will correspond to multiple values
				for(Map.Entry<? ,? > entry : properties.entrySet()) {// Type name
					String factoryTypeName = ((String) entry.getKey()).trim();
                    / / the value of the array
					String[] factoryImplementationNames =
							StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                    // Add multiple values to the map
					for (String factoryImplementationName : factoryImplementationNames) {
						result.computeIfAbsent(factoryTypeName, key -> newArrayList<>()) .add(factoryImplementationName.trim()); }}}// Cache map values removed and replaced with unmodifiable list
			result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
					.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
			cache.put(classLoader, result);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
		returnresult; }}Copy the code

The whole process of loading the file has been marked out in comments. If you look at it carefully, it is not difficult to understand.

The files and results are compared as follows:

 

As shown, the end result is one-to-many by key-value file format. The first half of the method, loadSpringFactories, first reads all the files into the cache. Then find the List we need based on the type we need.

return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
Copy the code

Then there is the upper method with the name of the class to create objects, to this, the getSpringFactoriesInstances is finished.

Load listener

Let’s go back to the original method of the SpringApplication class at the beginning of this article:

Now you can see that the listeners list assigned to the properties of the current class were just loaded from meta-info/spring.Factories.

To understand listeners, we don’t need to know which objects are loaded, just that they implement the interface:

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
	void onApplicationEvent(E event);
}
Copy the code

At this point we also know that we can add listeners in the same way we just described adding files, just implement this interface.

Run method

new SpringApplication(primarySources).run(args); Now that we’ve finished the first half of “Run”, let’s look at the run method.

	private SpringApplicationRunListeners getRunListeners(String[] args) { Class<? >[] types =newClass<? >[] { SpringApplication.class, String[].class };return new SpringApplicationRunListeners(logger,
				getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
				this.applicationStartup);
	}
Copy the code

We only see these two lines, the first line, the method of simple, still spring from file. The factories in loading SpringApplicationRunListeners corresponding value object creation, Then new SpringApplicationRunListeners as parameters.

A EventPublishingRunListener SpringApplicationRunListeners corresponding only

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
Copy the code

Translate the code:

	private SpringApplicationRunListeners getRunListeners(String[] args) { Class<? >[] types =newClass<? >[] { SpringApplication.class, String[].class };return new SpringApplicationRunListeners(logger,
				[new EventPublishingRunListener(types, this)].this.applicationStartup);
	}
Copy the code

The class relationships involved in the whole method are as follows:

SpringApplicationRunListeners contains only one of the listeners EventPublishingRunListener,

In the EventPublishingRunListener attribute points to the next class. And so on. Finally, the applicationListeners in the DefaultListenerRetriever contain all the listeners that need to be notified that were loaded at the beginning of the article. DefaultListenerRetriever let’s call it the listening retriever.

Starting the event

The run method calls the starting method, which loops through the class of listeners and calls each listener’s starting method.

class SpringApplicationRunListeners {
	void starting(ConfigurableBootstrapContext bootstrapContext, Class
        mainApplicationClass) {
		doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
				(step) -> {
					if(mainApplicationClass ! =null) {
						step.tag("mainApplicationClass", mainApplicationClass.getName()); }}); }private void doWithListeners(String stepName, Consumer
       
         listenerAction, Consumer
        
          stepAction)
        
        {
		StartupStep step = this.applicationStartup.start(stepName);
		this.listeners.forEach(listenerAction);
		if(stepAction ! =null) { stepAction.accept(step); } step.end(); }}Copy the code

From the previous class diagram, as we know, is called the EventPublishingRunListener starting method.

public class EventPublishingRunListener implements SpringApplicationRunListener.Ordered {
	@Override
	public void starting(ConfigurableBootstrapContext bootstrapContext) {
		this.initialMulticaster
				.multicastEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args)); }}Copy the code

And then you use the classes in the class diagram.

Among them, create ApplicationStartingEvent EventPublishingRunListener event types, then using listening to retrieve the list of maintenance cycle, with each listener to judge whether to support the event (each listener the implementation of a unified method), The listener’s onApplicationEvent method is called if it is supported.

Conclusion:

This article uses the ApplicationStartingEvent event as an example.

Spring starts by loading listeners from a file into the class (listener finder) indicated in step 3 in the picture, and then, in the first step of the Spring start process, calling the starting method to create the ApplicationStartingEvent event for the listener to listen on.

At this point, Spring has not started the process of initialization, so listeners for this event cannot be injected via @Component or @bean, but via files, or by calling addListeners provided by SpringApplication.

Welcome to leave comments and discuss