SpringBoot auto configuration principle and how to create your own Starter

[copyright notice] for non-commercial purposes, indicate the source and can be reproduced freely
From: shusheng007

summary

Now, Spring is the absolute overlord of Java backend, and Spring boot based on Spring has become the preferred way to use Spring. When I first used SpringBoot, I thought it was amazing. I just added a Spring boot starter web dependency to the pom.xml file of maven project, so I can write the Rest API directly. But sometimes I'm confused: when I use a third-party library, some only need to add a starter to use it directly, but some need to add an annotation, and some need to configure properties in the application.properties file. The name of the property can't be wrong. How did all this happen?

Recently, I have time to summarize it. I hope I can give some help to those confused new students while improving my technical level. Remember my code: ShuSheng007

Convention over configuration

It is often heard that the idea that the Convention is greater than the configuration adopted by SpringBoot greatly simplifies the difficulty of building the project. So what is the Convention greater than the configuration?

definition:

Convention Over Configuration, also known as programming by convention, is a software design paradigm. The purpose is to reduce the number of decisions that software developers need to make, so as to obtain the ability of simplicity and flexibility

People talk is to give the default value. All places that can be configured are given the default value. If you do not configure it specifically, we will use the agreed default value. This created the wonder of zero configuration startup of SpringBoot project. In fact, the developers of SpringBoot helped us make a decision.

How to realize automatic assembly in SpringBoot

How did SpringBoot make decisions for us? This involves its automatic assembly principle. Let's uncover its mystery together

principle

When SpringBoot starts, it will scan the META-INF/spring.factories file in the external reference jar package, load the type information configured in the file into the Spring container (that is, throw the new object into the Spring container according to the configured type information), and perform various operations defined in the class.

Therefore, if you want SpringBoot to automatically drop an instance of a class of your library into the Spring container, you need to provide the META-INF/spring.factories file as required.

The contents of the file are as follows:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\

org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
...

The first line is fixed, indicating that the automatic assembly classes listed below are to be scanned, and then automatically assembled according to conditions.

Concrete implementation

The principle is very simple, but if you don't look at the specific implementation, I believe you are still in a state of ignorance. When you are interviewed, you will be asked. Everyone is like this, not just you, so don't be embarrassed, ha ha.

The automatic configuration code of SpringBoot is in the org.springframework.boot: spring boot autoconfigure: XXXX dependency in your project External Libraries

Where xxxx is the version number of springboot you use, and I use 2.5.4.

configuration file

We can find a spring.factories file in the META-INF folder of its jar package, and we can see that there are many classes to be configured automatically

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\

org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
...
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
...

Many of these automatically configured classes are those in the starter officially provided by SpringBoot. For example, the familiar MongoDB configuration classes are as follows

org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\

We can see that there are many configuration classes here. Should SpringBoot instantiate the @ beans in these classes into the Spring container when starting? Is he that stupid? You don't even use MongoDb. Does he configure you with one that you can't swear?

How does SpringBoot determine which configuration classes should be used and which should be ignored? The answer is to use various ConditionalOnXXX annotations, that is, automatic assembly only when certain conditions are met.

Let's take MongoDB's auto configuration class for a specific look. The following is part of its code. The key is to look at the annotations above this class.

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDatabaseFactory")
public class MongoAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(MongoClient.class)
	public MongoClient mongo(ObjectProvider<MongoClientSettingsBuilderCustomizer> builderCustomizers,
			MongoClientSettings settings) {
		return new MongoClientFactory(builderCustomizers.orderedStream().collect(Collectors.toList()))
				.createMongoClient(settings);
	}
	...
}

Two of these notes are particularly striking

@ConditionalOnClass(MongoClient.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDatabaseFactory")
  • @ConditionalOnClass(MongoClient.class)

Indicates that this configuration class will be automatically assembled only when MongoClient exists in the classpath of the SpringBoot project. What does that mean? This means that you have to introduce MongoDB's starter to introduce MongoClient, and SpringBoot will automatically assemble this configuration class.

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-mongodb</artifactId>
  </dependency>

If MongoDB's starter is not introduced, MongoClient will become popular, but it's no problem. Have you ever thought about why? Because @ ConditionalOnClass loads this Class through bytecode, it is allowed not to exist in the Class path.

  • @ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDatabaseFactory")

What does this annotation mean? This means that when you not only introduce MongoDB starter, but also use your own MongoDatabaseFactory, SpringBoot will give up automatic assembly and use your own configuration. This is the more advanced usage. You are awesome. You know how to configure MongoDB to work better, so you don't need SpringBoot to help you make decisions. It's the so-called: my life is up to me, not heaven!

/**
 * Created by ShuSheng007
 * <p>
 * Author      shusheng007
 * Date        2021/9/17 19:08
 * Description
 */
@Configuration
public class MyMongoDbConfig {
    @Bean
    public MongoDatabaseFactory mongoDatabaseFactory(){        
        ...
    }
}
  • @EnableConfigurationProperties(MongoProperties.class)

This annotation is actually more meaningful to most of us, because we spend most of our time using SpringBoot rather than writing SpringBoot. Remember when you use SpringBoot, you often need to configure the library in the application.properties file? I don't know about you. Anyway, when I first came into contact with SB, I often complained: I'm C. how do I know what their key is? Why does it affect my MongoDB after I configure it here? All this is done by this annotation.

See Mongo properties? This is the class we often write to parse the configuration of the application.properties file

@ConfigurationProperties(prefix = "spring.data.mongodb")
public class MongoProperties {

	/**
	 * Default port used when the configured port is {@code null}.
	 */
	public static final int DEFAULT_PORT = 27017;

	/**
	 * Default URI used when the configured URI is {@code null}.
	 */
	public static final String DEFAULT_URI = "mongodb://localhost/test";

	/**
	 * Mongo server host. Cannot be set with URI.
	 */
	private String host;

	/**
	 * Mongo server port. Cannot be set with URI.
	 */
	private Integer port = null;
	...
}

Did you find a familiar taste? See @ ConfigurationProperties(prefix = "spring.data.mongodb") to explain that all MongoDB configurations must start with spring.data.mongodb,
As for the specific key name, please look at the attribute name of this class! Here you can also retrieve which attribute a function uses to configure!

critical code

The key code of automatic assembly is in the following methods of org.springframework.boot.autoconfigure.AutoConfigurationImportSelector class

	protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		configurations = removeDuplicates(configurations);
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

This method obtains a list of classes to be automatically assembled after reading, filtering and other operations.

How to write your own SpringBoot Starter

With the above theoretical knowledge, it has become very easy to write a starter, but it is still challenging to write a starter well. Overall, two steps are required

  • Create your own auto assembly class, including the profile class for customization
  • Add classes that need to be automatically assembled in META-INF/spring.factories file

Generally, our starter includes many interdependent projects. They work together to make a library work normally. For example, MongoDB may depend on other class libraries, and other class libraries will also enter the starter. For example, the dependency of MongoDB's starter mentioned above is shown in the figure below. They jointly support the normal use of MongoDB.

Suppose we have a library whose function is to output a greeting, then we write a starter for this class library so that it can be integrated into the SpringBoot project.

Create autoconfiguration item

Create a maven project named Hello spring boot autoconfigure. The final directory structure is as follows:

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── top
│   │   │       └── ss007
│   │   │           └── hellospringbootautoconfigure
│   │   │               ├── config
│   │   │               │   ├── HelloAutoConfiguration.java
│   │   │               │   └── HelloProperties.java
│   │   │               └── library
│   │   │                   └── ShuSheng007.java
│   │   └── resources
│   │       └── META-INF
│   │           ├── additional-spring-configuration-metadata.json
│   │           └── spring.factories
│   └── test
│       └── java
│           └── top
│               └── ss007
│                   └── hellospringbootautoconfigure

In fact, library/ShuSheng007.java should be in another library, that is, the library we want to use. Suppose it is called library, and then this library will be introduced with this project in the starter project later. Since we are just a demonstration here, we will put it directly in the autoconfigure project.

Step 1: introduce the following dependencies into the pom.xml file. The latter two are optional and will be used later

    <groupId>top.ss007</groupId>
    <artifactId>hello-spring-boot-autoconfigure</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.5.4</version>
        </dependency>
<!--        stay META-INF Generate metadata for attribute configuration in IDE,Code prompt for editing property file, optional-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
            <version>2.5.4</version>
        </dependency>
<!--        stay META-INF Ignore configuration metadata generated in to speed up the initialization of automatic configuration. Optional-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure-processor</artifactId>
            <optional>true</optional>
            <version>2.5.4</version>
        </dependency>
    </dependencies>

Step 2: build attribute configuration class (optional)

This step is mainly to enable our starter to be customized, that is, we can configure it through the application.properties file

@ConfigurationProperties(prefix = "ss007.hello")
public class HelloProperties {
    /**
     * Speaker name
     */
    private String name = "ShuSheng007";
    private String content = "Hello Spring Starter";

	//Omit getter s and setter s
}

Here we give the ability to customize the speaker and speech content, and both values are given default values.

Step 3: build automatic configuration class

/**
 * Created by Ben.Wang
 * <p>
 * Author      Ben.Wang
 * Date        2021/9/17 23:13
 * Description
 */

@Configuration
@ConditionalOnProperty(value = "ss007.hello.enabled",havingValue = "true")
@ConditionalOnClass(ShuSheng007.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
    private final HelloProperties helloProperties;
    public HelloAutoConfiguration(HelloProperties helloProperties) {
        this.helloProperties = helloProperties;
    }

    @Bean
    @ConditionalOnMissingBean
    public ShuSheng007 shuSheng007(){
        return new ShuSheng007(helloProperties.getName(),helloProperties.getContent());
    }
}

It is worth noting here that I use @ ConditionalOnProperty(value = "ss007.hello.enabled",havingValue = "true") as the condition, so to enable automatic configuration, I also need to configure ss007.hello.enabled=true in the application.properties file

Step 3: add the auto configuration class to META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
top.ss007.hellospringbootautoconfigure.config.HelloAutoConfiguration

After the above three steps, the automatic configuration item is finished. Next, create the starter item and add the dependent libraries to the automatic configuration item.

Create starter project

The starter project is relatively simple. It only contains a pom.xml file. It just introduces the Hello spring boot autoconfigure established by the used library and our new key into the POM file as a dependency.

The project structure is as follows:

hello-spring-boot-starter
└── pom.xml

pom.xml is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=...>
    <modelVersion>4.0.0</modelVersion>

    <groupId>top.ss007</groupId>
    <artifactId>hello-spring-boot-starter</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>
    <name>hello-spring-boot-starter</name>
    <description>hello-spring-boot-starter</description>

    <dependencies>
        <dependency>
            <groupId>top.ss007</groupId>
            <artifactId>hello-spring-boot-autoconfigure</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

Because we merged the library into Hello spring boot autoconfigure, it is enough to introduce only one dependency

How to use

I don't have to say this. After all, I use starter every day.

  • Introducing dependency in pom.xml
 <dependency>
     <groupId>top.ss007</groupId>
     <artifactId>hello-spring-boot-starter</artifactId>
     <version>1.0.0</version>
 </dependency>
  • Start starter (optional)

In order to demonstrate, I added a switch attribute, so I need to add the configuration of ss007.hello.enabled=true in the application.properties file, which is not required for many starter s.

  • use

Now you can directly inject an instance of ShuSheng007 class and use its methods, because it has been automatically assembled when SpringBoot starts. We implement CommandLineRunner to call our starter when the application runs.

@SpringBootApplication
public class HelloSbStarterUserApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(HelloSbStarterUserApplication.class, args);
    }

    @Autowired
    private ShuSheng007 shuSheng007;

    @Override
    public void run(String... args) throws Exception {
        shuSheng007.say();
    }
}

Output:

ShuSheng007 Say: Hello Spring Starter

So far, a simple starter has been written, but it is not professional. We can make it more professional.

How to improve

  • Add attribute file code prompt

Have you noticed that when you edit the application.properties file, your IDE will give you special tips. In other words, there is no prompt. Did you write the code?
How to make our own starter also have the following tips?

First: generate the metadata of the attribute class modified by the @ ConfigurationProperties annotation.

This is relatively simple. Just introduce the following libraries into our Hello spring boot autoconfigure project

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
            <version>2.5.4</version>
        </dependency>

This library will automatically generate metadata for the IDE based on the class modified by @ ConfigurationProperties. The generated metadata file is target/classes/META-INF/spring-configuration-metadata.json.

{
  "groups": [
    {
      "name": "ss007.hello",
      "type": "top.ss007.hellospringbootautoconfigure.config.HelloProperties",
      "sourceType": "top.ss007.hellospringbootautoconfigure.config.HelloProperties"
    }
  ],
  "properties": [
    {
      "name": "ss007.hello.content",
      "type": "java.lang.String",
      "sourceType": "top.ss007.hellospringbootautoconfigure.config.HelloProperties"
    },
    {
      "name": "ss007.hello.name",
      "type": "java.lang.String",
      "description": "Speaker name",
      "sourceType": "top.ss007.hellospringbootautoconfigure.config.HelloProperties"
    },
    // The following is merged from additional-spring-configuration-metadata.json
   {
      "name": "ss007.hello.enabled",
      "type": "java.lang.Boolean",
      "description": "Start Hello Automatic assembly function of."
    }
  ],
  "hints": []
}

It can be seen that the java doc (speaker name) of the HelloProperties class will also be included in the metadata for the IDE to read.

Second: generate metadata for other attributes

In our project, in addition to the HelloProperties class, there is also a property ss007.hello.enabled. This property cannot generate metadata through the above methods, resulting in no prompt in the IDE. Is there a way to solve this problem? Yes, we need to provide a custom configuration metadata and let spring boot configuration processor add it to the spring-configuration-metadata.json file for us.

Add an additional-spring-configuration-metadata.json file in the resources/META-INF of the Hello spring boot autoconfigure project

{"properties": [
    {
      "name": "ss007.hello.enabled",
      "type": "java.lang.Boolean",
      "description": "Start Hello Automatic assembly function of."
    }
  ]
}

Just.

  • Provide metadata for automatic assembly filter conditions

We know that during automatic assembly, SpringBoot will determine whether a class needs to be assembled through various filter conditions, which will affect the startup data when there are too many assembled classes. We can provide the filter conditions to the IDE to speed up the process.

You only need to introduce the following tools in the Hello spring boot autoconfigure project:

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure-processor</artifactId>
      <optional>true</optional>
      <version>2.5.4</version>
  </dependency>

It will generate a spring-autoconfigure-metadata.json in the target/classes/META-INF file

top.ss007.hellospringbootautoconfigure.config.HelloAutoConfiguration=
top.ss007.hellospringbootautoconfigure.config.HelloAutoConfiguration.ConditionalOnClass=top.ss007.hellospringbootautoconfigure.library.ShuSheng007

summary

Unknowingly, the article has become very long. Through this article, I believe you have completely taken off your SpringBoot Starter pants... On the occasion of the Mid Autumn Festival, it is rainy and can only write articles at home.

Launch and source address

Tags: Java Spring Spring Boot

Posted on Thu, 23 Sep 2021 09:41:44 -0400 by nevynev