深入浅出自定义创建 spring-boot-starter
快速入手
第一步:新建模块
第二步:修改依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>microservices</artifactId>
<groupId>com.example</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-spring-boot-starter</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
添加spring-boot-starter和spring-boot-configuration-processor这两个依赖。
第三步:新建Properties配置类
@ConfigurationProperties(prefix = "test")
public class TestProperties {
/**
* this is name
*/
private int name;
/**
* this is address
*/
private String address;
public int getName() {
return name;
}
public void setName(int name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "TestProperties{" +
"name=" + name +
", address='" + address + '\'' +
'}';
}
}
第四步:定义 AutoConfiguration,一个主配置,两个辅配置主要用来验证注解效果。
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "test", value = "enable", havingValue = "true", matchIfMissing = true)
@AutoConfigureAfter({AfterAutoConfiguration.class})
@AutoConfigureBefore({BeforeAutoConfiguration.class})
@EnableConfigurationProperties(TestProperties.class)
public class TestAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(TestAutoConfiguration.class);
public TestAutoConfiguration(TestProperties properties) {
logger.info("TEST INIT ...... " + properties.toString());
}
}
@Configuration
public class AfterAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(BeforeAutoConfiguration.class);
public AfterAutoConfiguration() {
logger.info("After INIT ...... ");
}
}
@Configuration
public class BeforeAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(BeforeAutoConfiguration.class);
public BeforeAutoConfiguration() {
logger.info("Before INIT ...... ");
}
}
第五步:在resources目录下新建META-INF文件夹和spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.config.TestAutoConfiguration, \
com.example.starter.config.BeforeAutoConfiguration, \
com.example.starter.config.AfterAutoConfiguration
整体结构图
第八步:maven clean install 本地仓库
第九步:在另一个模块使用starter
<dependency>
<groupId>com.example</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
第十步:配置yml
test:
address: abcd
name: 15
最后:查看效果
c.e.s.config.BeforeAutoConfiguration : After INIT ......
c.e.s.config.TestAutoConfiguration : TEST INIT ...... TestProperties{name=15, address='abcd'}
c.e.s.config.BeforeAutoConfiguration : Before INIT ......
AutoConfigureAfter和Before
@AutoConfigureAfter({AfterAutoConfiguration.class})
@AutoConfigureBefore({BeforeAutoConfiguration.class})
TestAutoConfiguration
简称为 A、B、T三个字母。
AutoConfigureAfter 源码注释
Hint for that an auto-configuration should be applied after other specified auto-configuration classes.
当前class 在指定类 后面加载
AutoConfigureBefore源码注释
Hint that an auto-configuration should be applied before other specified auto-configuration classes.
当前class 在指定类 前面加载
根据注释的意思我们得知,T在A后面,T在B前面。所以整个顺序为 A T B。
误区
首先AutoConfigureAfter和AutoConfigureBefore是作用于自动装配类的,普通的Configure无效。下面我们结合RocketMQAutoConfiguration 的源码来解释这个误区。
@Configuration
@EnableConfigurationProperties(RocketMQProperties.class)
@ConditionalOnClass({MQAdmin.class})
@ConditionalOnProperty(prefix = "rocketmq", value = "name-server", matchIfMissing = true)
@Import({MessageConverterConfiguration.class, ListenerContainerConfiguration.class,
ExtProducerResetConfiguration.class, ExtConsumerResetConfiguration.class,
RocketMQTransactionConfiguration.class})
@AutoConfigureAfter({MessageConverterConfiguration.class})
@AutoConfigureBefore({RocketMQTransactionConfiguration.class})
public class RocketMQAutoConfiguration implements ApplicationContextAware {}
依赖图
spring.factories内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration
在RocketMQAutoConfiguration中只有一个自动装配类,MessageConverterConfiguration和RocketMQTransactionConfiguration只是普通的配置类,所以我仿照这种写法实验后,这两个类不会进行加载,源码这里生效的原因是因为@Import(…)这个注解将普通配置类导入,其实这里是没有after和before的效果的。
如何验证:将spring.factories demo的其余两个自动装配去掉打印控制台输出,然后加入@import注解后在观察输出。
扩展
@Import 配合AutoConfiguration
通过import引入三个普通配置类
@Configuration(proxyBeanMethods = false)
public class Config1 {
private static final Logger logger = LoggerFactory.getLogger(Config1.class);
public Config1() {
logger.info("Config1 init...");
}
}
@Configuration(proxyBeanMethods = false)
public class Config2 {
private static final Logger logger = LoggerFactory.getLogger(Config2.class);
public Config2() {
logger.info("Config2 init...");
}
}
@Configuration(proxyBeanMethods = false)
public class Config3 {
private static final Logger logger = LoggerFactory.getLogger(Config3.class);
public Config3() {
logger.info("Config3 init...");
}
}
在 TestAutoConfiguration 这个自动配置类上通过 import 加入三个配置类。代码如下所示。
@Import({Config3.class, Config2.class, Config1.class})
输出
c.e.s.c.a.BeforeAutoConfiguration : After INIT ......
com.example.starter.config.Config3 : Config3 init...
com.example.starter.config.Config2 : Config2 init...
com.example.starter.config.Config1 : Config1 init...
c.e.s.c.a.TestAutoConfiguration : TEST INIT ...... TestProperties{name=15, address='abcd'}
c.e.s.c.a.BeforeAutoConfiguration : Before INIT ......
分析:通过@Import注解导入的配置类优先于自动装配类进行加载。到此为止整体顺序为 AutoConfigureAfter,@Import(),TestAutoConfiguration,AutoConfigureBefore。
ConditionalOnProperty 配合 AutoConfigureAfter 和 Before
在我们现在的实现中,通过ConditionalOnProperty仅仅控制TestAutoConfiguration的加载
@ConditionalOnProperty(prefix = "test", value = "enable", havingValue = "true", matchIfMissing = true)
AfterAutoConfiguration和BeforeAutoConfiguration会在enable = false的情况下加载。
例如在 NacosServiceRegistryAutoConfiguration 这个类中,其他的自动装配类也使用同一个条件控制。

这样就实现了 配置文件中的 一个 false控制多个自动装配类的加载。
模仿
DataSourceAutoConfiguration
源码
@Configuration(proxyBeanMethods = false) //1
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) //2
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") //3
@EnableConfigurationProperties(DataSourceProperties.class) //4
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class }) //5
public class DataSourceAutoConfiguration {}
第一行声明为一个配置类。
第二行DataSource和EmbeddedDatabaseType两个类存在才会自动装配。
第三行没有ConnectionFactory这个类型的bean的时候才会自动装配。
第四行开启配置文件DataSourceProperties。
第五行通过@Import导入两个普通的配置类。
总结:
- 自动装配类是否开启可以通过ConditionalOnXXX来控制。更多的条件可以看包下其他注解。
- EnableConfigurationProperties来开启(也可以称为关联或者生效)一个配置文件类。
接下来分析类中其他 Bean,例如EmbeddedDatabaseConfiguration
@Configuration(proxyBeanMethods = false) //1
@Conditional(EmbeddedDatabaseCondition.class) //2
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) //3
@Import(EmbeddedDataSourceConfiguration.class) //4
protected static class EmbeddedDatabaseConfiguration {
}
化繁为简,首先去掉条件注解2和3,核心就是1和4。
要看懂这段代码首先要回顾一下静态内部类的知识点。在TestAutoConfiguration内部创建一个静态内部类。
protected static class Test1 {
static {
logger.info("TEST : Test1 INIT ... ");
}
}
重新启动服务,Test1这个内部类并没有加载。那么这里的静态内部类在什么时候会进行加载? 还是说这个类根本不需要加载只是通过一些条件注解和 @Import 来导入配置类。修改为下面这段代码。
@Configuration(proxyBeanMethods = false)
protected static class Test1 {
static {
logger.info("TEST : Test1 class INIT ... ");
}
public Test1() {
logger.info("TEST : Test1 Constructor INIT ... ");
}
}
protected static class Test2 {
static {
logger.info("TEST : Test2 INIT ... ");
}
public Test2() {
logger.info("TEST : Test2 Constructor INIT ... ");
}
}
输出
c.e.s.c.a.BeforeAutoConfiguration : After INIT ......
c.e.s.c.a.TestAutoConfiguration : TEST : Test1 class INIT ...
c.e.s.c.a.TestAutoConfiguration : TEST : Test1 Constructor INIT ...
com.example.starter.config.Config3 : Config3 init...
com.example.starter.config.Config2 : Config2 init...
com.example.starter.config.Config1 : Config1 init...
c.e.s.c.a.TestAutoConfiguration : TEST INIT ...... TestProperties{name=15, address='abcd'}
c.e.s.c.a.BeforeAutoConfiguration : Before INIT ......
分析:
- Test1 加载了而Test2 说明@Configuration修饰的内部类会被Spring接管从而加载。
- 被Spring接管后,可以使用条件注解来控制其是否生效。
- Test1 优先于TestAutoConfiguration上@Import注解加载和调用构造函数。
下面我们来分析Test1为什么优先于Config3 加载。我们在静态代码块打一个断点追踪加载过程,在DefaultListableBeanFactory这个类的preInstantiateSingletons方法中观察beanNames这个list,由于后续是采用循环处理,所以研究为什么list中静态内部类在先。

在preInstantiateSingletons这个方法打一个断点观察调用栈

主要涉及这三个方法,涉及到Spring Boot的加载原理,我们后续分篇讲解,这里先记住在先即可。
写到这里,可以将 Test2 这一段删掉了,相信经过上面这一段的讲解,你已经知道了静态内部类在这里面的作用和效果。
spring.factories

通过 spring.factories可以导入其他类型的东西例如:
# Initializers
org.springframework.context.ApplicationContextInitializer=\
# Application Listeners
org.springframework.context.ApplicationListener=\
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
RocketMQAutoConfiguration
starter中的Enable
我们经常会在各种各样的starter中见到见到EnableXXX的注解,通常起到一个开关的作用,那么它是如何实现的呢
@EnableFeignClients(clients = UserClient.class)
我们以EnableFeignClients为例来分析它的作用,定义一个注解EnableTest
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(TestRegistrar.class)
public @interface EnableTest {
}
定义一个TestRegistrar ,并在构造函数处打上一个断点。
public class TestRegistrar {
private static final Logger logger = LoggerFactory.getLogger(TestRegistrar.class);
static {
logger.info("TestRegistrar static");
}
public TestRegistrar() {
logger.info("TestRegistrar constructor");
}
}
经过简单的测试,主启动类上使用EnableTest后输出构造函数内容,否则不输出。
@Import(TestRegistrar.class)
注解的作用就是向容器中导入一个类。其中FeignClientsRegistrar这个类有一个重要的接口ImportBeanDefinitionRegistrar,通过这个接口向容器中导入BeanDefinition,