SpringCloud Alibaba practice from scratch (72) -- principle and application of Enable * annotation for automatic configuration of springboot core

preface

SpringBoot provides many annotations starting with Enable. These annotations are used to dynamically Enable some functions, and its underlying principle is to use the @ Import annotation to Import some configuration classes, such as realizing the dynamic loading of beans. This sentence sounds confused, so let's think about a question: can the SpringBoot project directly obtain the beans defined in the jar package? With questions, let's analyze the source code of @ Enable * annotation of SpringBoot automatic configuration.

verification

1. First, take a brief look at the startup annotation @ SpringBootApplication
Click in the source code as follows:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ...
}

Explanation:

@Target({ElementType.TYPE}) --------- declare that the scope of annotation is class;
@Retention(RetentionPolicy.RUNTIME) - declares that the annotation action time is runtime;
@Documented -------------------------------------- generate javaDoc document by declaration;
@SpringBootConfiguration ----------------- click in and the source code is as follows. It shows that @ SpringBootConfiguration is a composite annotation. In fact, it is a @ Configuration, that is, a tag of a Configuration class. This is the fundamental reason why beans can be created in the main Configuration class of SpringBoot:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration    //The core of this annotation
@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

@EnableAutoConfiguration ----------------- Click to see the source code and find that it is also a composite annotation with an @ Import annotation

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})    //This annotation is the core
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

Question: can the SpringBoot project directly obtain the beans defined in the jar package?

1. Create two module presentations
This paper creates two modules: springBoot enable and springBoot embedded. The former represents our springBoot project, and the latter simulates a three-party jar package

Write springboot embedded

1. Define Bean class User.java

 
public class User {
}

2. Define the configuration class UserConfig.java

 

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class UserConfig {
 
    @Bean
    public User user(){
        return new User();
    }
}

3. Write springboot enable
1. Introducing the coordinate dependency of springboot embedded into pom.xml

<dependency>
    <groupId>com.test</groupId>
    <artifactId>springboot-embedded</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

2. Startup class

 

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
@SpringBootApplication
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}

After startup, an error is reported
nosuch bean

It shows that the SpringBoot project cannot directly obtain the beans defined in the jar package, so the problem comes again. Why can we directly use an object named redisTemplate after introducing Redis coordinates in pom.xml? The root cause is the @ Import annotation

The SpringBoot implementation uses beans defined in third-party jar packages

It has been verified that the SpringBoot project cannot directly obtain the beans defined in the jar package because @ ComponentScan, one of the internal combinations of the @ SpringBootApplication annotation on the startup class, is a package scanning scope annotation. The default package scanning scope is to scan the peer packages and sub packages of the startup class annotated by @ SpringBootApplication. We found that the package of the startup class is com.itlean, The package of UserConfig.java is com.embedded.config, which has no peer or inclusion relationship. If you want to scan UserConfig.java during startup, there are three solutions, as follows:

1. The first method (scanning range)

Modify the startup class, add the annotation of package scanning range, and manually scan the package of UserConfig.java:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
 
@SpringBootApplication
@ComponentScan("com.test.config") //UserConfig.java manually scans and joins IOC container
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}

But is this method too extensive? I use an object. After that, I have to know the package in which this object is located. Then I need to use redisTemplate object of Redis. I have to find out which package this thing is in, and then add package scanning in @ ComponentScan annotation of startup class. It must be unrealistic

2. The second method is @ Import annotation

    First look at the source code and find that the input parameter is Class<?>[] value();That is, an array of classes. One more mouth, ha, in SpringBoot Used in@Import These classes introduced by the annotation are loaded into the IOC In the container.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    Class<?>[] value();
}

As mentioned above, let's modify the startup class annotation again as follows:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
 
@SpringBootApplication
@Import(UserConfig.class)//Directly introduce the User's configuration class into
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}

This method is much more indirect than method 1. You don't need to know the package path of the used Bean, but you still need to remember the name of the configuration class. I use an object and need to know the name of the configuration class corresponding to this object. Obviously, it's not friendly enough. The next method 3 is the ultimate optimization of @ Import usage.

3. The third method is @ Import annotation encapsulation

    In a third party jar Create a new annotation class in@Import Encapsulate with annotations:
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
 
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)// If UserConfig.class is directly introduced into the third party, the caller does not need to know the package path and configuration class name
public @interface EnableUser {
}

The caller startup class is modified as follows:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
@SpringBootApplication
@EnableUser //This is already very simple to use
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}

In actual development, the last method is used.

@ Enable * annotation in SpringBoot

    The third way is SpringBoot The principle of bottom automatic assembly, so@Enable*This kind of annotation is the annotation that enables some functions, and the underlying is the use of@Import Way to achieve Bean Dynamic loading, not used@EnableUser I can't use this annotation User This object. Let's go back to the annotation of the startup class@SpringBootAppliction Annotation, there is one in the internal annotation combination@EnableAutoConfiguration Notes, as follows:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ...
}

@The EnableAutoConfiguration annotation uses the @ Import annotation internally, as follows:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

Tags: Java Spring Spring Boot

Posted on Fri, 19 Nov 2021 04:06:38 -0500 by Paul Arnold