Creating Your Own Auto-configuration

简介: SpringBoot的自动配置和其工作原理,自定义Starter

  根据Spring文档的介绍,如果想要做一个开源的或者商业的库,就需要使用到Auto-configuration。Auto-configuration可以关联到提供自动配置代码的“starter”以及将与之一起使用的典型库。

了解自动配置的Bean

Classes that implement auto-configuration are annotated with @AutoConfiguration. This annotation itself is meta-annotated with @Configuration, making auto-configurations standard @Configuration classes. Additional @Conditional annotations are used to constrain when the auto-configuration should apply. Usually, auto-configuration classes use @ConditionalOnClass and @ConditionalOnMissingBean annotations. This ensures that auto-configuration applies only when relevant classes are found and when you have not declared your own @Configuration.

上边的这段文字主要是有以下几点:

  1. 实现自动配置的类通过使用注解@AutoConfiguration进行注释
  2. 进入@AutoConfiguration的源码会发现其是被@Configuration进行注释的,是一个标准的配置类,@Conditional注解也会被用于约束自动配置的应用时间
  3. 自动配置类通常也会使用@ConditionalOnClass@ConditionalOnMissingBean进行注释,为了确保自动配置仅在能够找到相关类,并且尚未声明自己的@Configuration时适用

SpringBoot是如何定位自动配置文件的

Spring Boot checks for the presence of a META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file within your published jar.
Auto-configurations must be loaded only by being named in the imports file. Make sure that they are defined in a specific package space and that they are never the target of component scanning. Furthermore, auto-configuration classes should not enable component scanning to find additional components. Specific @Import annotations should be used instead.

以上内容主要介绍了:
SpringBoot会检查发布的jar包中是否存在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件是否存在,并且自动配置只能通过在导入文件中命名来加载。要确保它们是在特定的包空间中定义的,并且它们永远不会成为组件扫描的目标,自动配置类不应该启用组件扫描来查找其他组件。应该使用特定的@import注释

此外需要注意的是新版本的SpringBoot3.X和旧版本的SpringBoot2.7之前的自动配置文件是有一定的区别的,目前有两种不同的方式:

  • META-INF/sring.factories文件中添加org.springframework.boot.autoconfigure.EnableAutoConfiguration=XXAutoConfiguration
  • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

第一种方式支持SpringBoot3.X版本之前;第二种方式支持从SpringBoot2.7之后的版本。另外文件命名与格式需要完全保持一致,如果使用的是IDEA,那么文件应该会如下图所示
image.png

通过了解上边的内容,也解决了一个常见的疑问,SpringBoot是如何进行自动配置的,下面通过源码进行以下简单的分析

SpringBoot自动配置原理

最常见的注解@SpringBootApplication

@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 {
   

在这个注解上边我们会发现一个类似以@AutoConfiguration的注解@EnableAutoConfiguration我们可以继续深挖以下这个注解,就会发现


@Target({
   ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({
   AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
   

在这个注解上边我们会发现一个很厉害的注解@import,使用@Import导入的类会被Spring加载到 IOC 容器中,既然专门加载了AutoConfigurationImportSelector.class这个类的对象到IOC容器中,证明这个类也十分不简单。再探再报,进入到这个类中,我们会发现有一个名为getCandidateConfigurations的方法,代码如下

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   
        List<String> configurations = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).getCandidates();
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

当然由于版本不同看到的可能不一样

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

上边的我使用的是SpringBoot3.0.0,下边的则是2.6.3,不过它们的功能都是一样的,都是要加载META-INF中的配置文件。

SpringBoot就是通过以上的方式进行加载自动配置类的,但是SpringBoot并非把所有的配置类都加载进去,SpringBoot会有一个过滤的行为,主要有两种方式:

  • 通过使用@SpringBootApplication(exclude = xxxAutoConfiguration.class)的方式进行选择
  • 使用SpringBoot的条件注解进行过滤

条件注解

Class Conditions

The @ConditionalOnClass and @ConditionalOnMissingClass annotations let @Configuration classes be included based on the presence or absence of specific classes.Due to the fact that annotation metadata is parsed by using ASM, you can use the value attribute to refer to the real class, even though that class might not actually appear on the running application classpath. You can also use the name attribute if you prefer to specify the class name by using a String value.

  • @ConditionalOnClass:当类路径下有指定的类的条件下
  • @ConditionalOnMissingClass:当容器里没有指定类的情况下
  • 注解元数据是通过ASM进行解析的,所以可以通过使用使用@Configuration注解的valueString属性指定真正的类

Bean Conditions

The @ConditionalOnBean and @ConditionalOnMissingBean annotations let a bean be included based on the presence or absence of specific beans.

  • @ConditionalOnBean:当容器里有指定Bean的条件下
  • @ConditionalOnMissingBean:当容器里没有指定Bean的情况下

Property Conditions

The @ConditionalOnProperty annotation lets configuration be included based on a Spring Environment property. Use the prefix and name attributes to specify the property that should be checked. By default, any property that exists and is not equal to false is matched. You can also create more advanced checks by using the havingValue and matchIfMissing attributes.
If multiple names are given in the name attribute, all of the properties have to pass the test for the condition to match.

  • @ConditionalOnProperty:指定的属性是否有指定的值

Resource Conditions

The @ConditionalOnResource annotation lets configuration be included only when a specific resource is present. Resources can be specified by using the usual Spring conventions, as shown in the following example: file:/home/user/test.dat.

  • @ConditionalOnResource注解允许只在出现特定资源时才包含配置

Web Application Conditions

The @ConditionalOnWebApplication and @ConditionalOnNotWebApplication annotations let configuration be included depending on whether the application is a web application. A servlet-based web application is any application that uses a Spring WebApplicationContext, defines a session scope, or has a ConfigurableWebEnvironment. A reactive web application is any application that uses a ReactiveWebApplicationContext, or has a ConfigurableReactiveWebEnvironment.

  • @ConditionalOnWebApplication:当前项目是Web项目的条件下
  • @ConditionalOnNotWebApplication:当前项目不是Web项目的条件下

SpEL Expression Conditions

The @ConditionalOnExpression annotation lets configuration be included based on the result of a SpEL expression.

  • @ConditionalOnExpression:基于SpEL表达式为true的时候作为判断条件才去实例化

创建自己的Starter

A typical Spring Boot starter contains code to auto-configure and customize the infrastructure of a given technology, let’s call that "acme". To make it easily extensible, a number of configuration keys in a dedicated namespace can be exposed to the environment. Finally, a single "starter" dependency is provided to help users get started as easily as possible.

  • 典型的Spring Boot启动器包含用于自动配置和自定义给定技术的基础架构的代码,我们称之为“acme”。
  • 为了使其易于扩展,可以将专用命名空间中的许多配置键暴露给环境。
  • 提供了一个Starter依赖项,以帮助用户尽可能轻松地获得Starter
  • 自定义启动器可以包含以下内容:
    • 包含acme的自动配置代码的auto-configuration模块。
    • starter模块提供了对autoconfigure模块的依赖,以及“acme”和其它的附加依赖。

命名规范

  • 官方的Starter包规范:spring-boot-starter-xxx
  • 自定义Starter包规范:xxx-spring-boot-starter

不要使用官方的命名规范

配置键

如果启动器提供了配置键,请为它们使用唯一的命名空间。特别是,不要将键包含在Spring Boot使用的命名空间中(例如server, management, spring等)。如果使用相同的命名空间,将来可能会以破坏您的模块的方式修改这些命名空间。根据经验,在您的所有键前加上您拥有的命名空间(例如acme)。

@ConfigurationProperties("acme")
public class AcmeProperties {
   

    /**
     * Whether to check the location of acme resources.
     */
    private boolean checkLocation = true;

    /**
     * Timeout for establishing a connection to the acme server.
     */
    private Duration loginTimeout = Duration.ofSeconds(3);

    // getters/setters ...
}

Spring Boot内部针对配置key描述遵循一下规则:

  • 不要以“The”或“A”开头描述
  • 对于布尔类型,以 "Whether"或 "Enable"开始描述。
  • 对于基于集合的类型,以“逗号分隔列表”开始描述
  • 使用java. time.Duration而不是long并描述默认单位是否与毫秒不同,例如“如果未指定持续时间后缀,则将使用秒”
  • 不要在描述中提供默认值,除非必须在运行时确定。

Starter实战

此自定义Starter实战为模拟不同短信平台发送信息的Starter,最终需要达到的效果是,根据不同的配置,选择默认或者指定的短信平台给用户发送信息。该实战使用的是SpringBoot3.0.2和JDK17

1、项目准备

创建SpringBoot的项目,项目名称随意,最好是符合官方文档所定义的格式,虽然是练习,认真对待总是没错的,例:sms-spring-boot-starter。然后添加autoconfigure依赖,如下图所示
image.png

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>3.0.2</version>
</dependency>

2、创建配置类

  • 创建短信运营商的类型枚举类
public enum SmsTypeEnum {
   
    //阿里云
    ALI_CLOUD,
    //腾讯云
    TX_CLOUD,

    private String type;

    SmsTypeEnum() {
   }

}
  • 创建用于绑定application.yml配置文件的SmsProperties,用@ConfigurationProperties来绑定配置属性
@ConfigurationProperties(prefix = "sms.server.achieve")
public class SmsProperties {
   

    /**
     * 发送短信类型
     */
    private String type;

    public String getType() {
   
        if(type == null || "".equals(type)){
   
            type = SmsTypeEnum.ALI_CLOUD.name();
        }
        return type;
    }

    public void setType(String type) {
   
        this.type = type;
    }
}

3.创建SmsService接口,封装各个厂商的发送短信实现。

public interface SmsService {
   
    String send(String fromPhone, String toPhone, String content);
}
@Service("ALI_CLOUD")
public class AliCloudSmsServiceImpl implements SmsService {
   

    @Override
    public String send(String fromPhone, String toPhone, String content) {
   
        System.out.println("------------------当前SMS厂商为阿里云------------------");
        System.out.println("----"+fromPhone+" 向 "+toPhone +" 发送了一条短信。"+"----");
        System.out.println("短信内容为:"+content);
        System.out.println("----------------------------------------------------");
        return "success";
    }
}
@Service("TX_CLOUD")
public class TxCloudSmsServiceImpl implements SmsService {
   
    @Override
    public String send(String fromPhone, String toPhone, String content) {
   
        System.out.println("------------------当前SMS厂商为腾讯云------------------");
        System.out.println("----"+fromPhone+" 向 "+toPhone +" 发送了一条短信。"+"----");
        System.out.println("短信内容为:"+content);
        System.out.println("----------------------------------------------------");
        return "success";
    }
}

4、定义SmsTemplate用于统一提供服务

public class SmsTemplate {
   

    @Autowired
    private SmsProperties smsProperties;

    @Autowired
    private ApplicationContext context;

    public String send(String fromPhone,String toPhone,String content){
   
        //获取云厂商的业务实现类
        String type = smsProperties.getType();
        SmsService smsService = (SmsService)context.getBean(type);
        return smsService.send(fromPhone,toPhone,content);
    }
}

5、定义SmsAutoConfiguration类

这个类在使用的时候与原博主的有些许差别,我在使用原博主的代码的时候,在测试项目中使用该Starter会出现在IOC容器中找不到对象的情况,在原博主的评论区找到一个解决方式。不知道是否还有其他方式解决。

@ComponentScan("com.leemuzi.service")

@AutoConfiguration
@ConditionalOnClass(SmsTemplate.class)
@EnableConfigurationProperties(value = SmsProperties.class)
public class SmsAutoConfiguration {
   

    @Bean
    @ConditionalOnMissingBean
    public SmsTemplate smsTemplate(){
   
        return new SmsTemplate();
    }

}

6、创建imports配置文件,把需要自动装载的类配置上。

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

image.png

7、打包入库

两种打包入库方式

  • IDEA使用Maven
    image.png
  • 使用Maven命令进行打包mvn clean install
    image.png

7、自定义Starter验证

创建项目,引入自定义Starter的依赖

<dependency>
    <groupId>com.leemuzi</groupId>
    <artifactId>sms-spring-boot-starter</artifactId>
    <version>0.2.7</version>
</dependency>

测试代码

@RestController
public class TestController {
   

    @Autowired
    private SmsTemplate smsTemplate;

    @Autowired
    private ApplicationContext applicationContext;

    @RequestMapping("/sms")
    public String sms(){
   
        String fromPhone = "15522834580";
        String toPhone = "13820345839";
        String content = "李祥,李祥,今晚王者峡谷 六点 五缺一,收到请回复,over!";
        String send = smsTemplate.send(fromPhone, toPhone, content);
        return send;
    }

}

image.png

测试成功,然后我们尝试修改配置文件的配置属性,然后重启测试
image.png
SMS厂商正常修改
image.png

结束语

由于本人才疏学浅,所以学习过程中难免存错误或疏漏,还请各位指正

致谢

感谢以下前辈的分享
[1] 互联网小阿祥.【案例实战】SpringBoot3.x自定义封装starter实战,https://blog.csdn.net/weixin_47533244/article/details/130703512
[2] java金融.保姆级教程,手把手教你实现一个SpringBoot的starter,https://mp.weixin.qq.com/s?__biz=MzIyMjQwMTgyNA==&mid=2247484449&idx=1&sn=630e8098208c223a0949f3c42742c46d&chksm=e82f406edf58c978d650ebf047fe1980c649f621c2b9542eee8195ca5f2b6fb030169017d309&cur_album_id=1800531143058849794&scene=189#wechat_redirect
[3] laopeng301.Spring Boot 3.x特性-自动配置和自定义Starter,https://blog.csdn.net/renpeng301/article/details/124357138#:~:text=%E5%9C%A8%E9%A1%B9%E7%9B%AE%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%EF%BC%88%E6%AF%94
[4] 程序码农.springboot3.0学习-自定义starter,https://www.bilibili.com/video/BV1dM411i7Kq/?spm_id_from=333.880.my_history.page.click&vd_source=6667d627fb3985854782c2cdec118eda
[5] 请回答1024.SpringBoot自动装配原理之@Import注解解析,https://developer.aliyun.com/article/1492682
[6] https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html#features.developing-auto-configuration.locating-auto-configuration-candidates
[7] https://springdoc.cn/configuration-properties-in-spring-boot/

目录
相关文章
|
Java 数据库连接 mybatis
Consider defining a bean of type ‘com.example.democrud.democurd.usermapper.DaoMapper‘ in your config
Consider defining a bean of type ‘com.example.democrud.democurd.usermapper.DaoMapper‘ in your config
206 0
|
NoSQL Redis
Consider defining a bean of type ‘com.bsj.system.service.RedisService‘ in your configuration
Consider defining a bean of type ‘com.bsj.system.service.RedisService‘ in your configuration
570 0
成功解决ProxyError: Conda cannot proceed due to an error in your proxy configuration.Check for typos an
成功解决ProxyError: Conda cannot proceed due to an error in your proxy configuration.Check for typos an
成功解决ProxyError: Conda cannot proceed due to an error in your proxy configuration.Check for typos an
|
3月前
Error creating bean with name 'eurekaAutoServiceRegistration': Singleton bean creation not allowed while singletons
Error creating bean with name 'eurekaAutoServiceRegistration': Singleton bean creation not allowed while singletons
106 3
Error starting ApplicationContext. To display the auto-configuration report re-run your application
Error starting ApplicationContext. To display the auto-configuration report re-run your application
|
Java Apache Spring
解决required a single bean, but 2 were found问题
背景:springboot整合shiro中自定义Realm时出现 错误描述 Parameter 0 of method getDefaultWebSecurityManager in cn.ken.springboot_shiro.config.ShiroConfig required a single bean, but 2 were foun
|
druid
A bean with that name has already been defined in class path resource and overriding is disabled.
A bean with that name has already been defined in class path resource and overriding is disabled.
219 0
|
Nacos
Nacos报错:Error creating bean with name ‘authFilterRegistration‘ defined in class path resource
Nacos报错:Error creating bean with name ‘authFilterRegistration‘ defined in class path resource
484 0
|
Java 数据库连接 网络安全
Error creating bean with name ‘attrAttrgroupRelationController‘
Error creating bean with name ‘attrAttrgroupRelationController‘
Error creating bean with name ‘attrAttrgroupRelationController‘
|
Java Maven
No valid Maven installation found. Either set the home directory in the configuration dialog or set
No valid Maven installation found. Either set the home directory in the configuration dialog or set
991 0
No valid Maven installation found. Either set the home directory in the configuration dialog or set