深入浅出自定义创建spring-boot-starter

深入浅出自定义创建 spring-boot-starter

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration

手把手带你开发 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,


版权声明:本文为qq_37151886原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。