SpringBoot自动配置原理及如何创建自己的Starter

Posted ShuSheng007

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot自动配置原理及如何创建自己的Starter相关的知识,希望对你有一定的参考价值。

[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007

概述

现在Java后端Spring是绝对的霸主,而基于Spring的SpringBoot已经成为使用Spring的首选方式。我第一次使用SpringBoot的时候我觉得它很神奇,我只是在maven项目的pom.xml文件里面加个spring-boot-starter-web的依赖,我就可以直接写Rest API了。但是有时我也很困惑:当我使用三方库时,有的只需要加上starter就可以直接使用了,但有的又需要加上某个注解,有的还需要在application.properties文件中配置属性,属性的名字还不能错,这一切都是怎么发生的呢?

最近有时间就总结一下吧,希望在提高自己技术水平的同时可以给那些迷茫的刚入行的同学们一些帮助,记住我的代号:ShuSheng007

约定优于配置

经常听到SpringBoot采用的约定大于配置的思想,极大的简化了项目搭建的难度,那么什么是约定大于配置呢?

定义:

约定优于配置(Convention Over Configuration)也称作按约定编程,是一种软件设计范式。目的在于减少软件开发人员所需要做出的决定的数量,从而获得简单而又不失其中的灵活性的能力

说人话就是给默认值,所有可以配置的地方都给默认值,如果你不特别配置,那么我们就使用约定的默认值。这样就造就了SpringBoot项目零配置启动的奇观,其实是SpringBoot的开发者帮我们做了决定。

SpringBoot 如何实现自动装配

那SpringBoot是怎么为我们做决定的呢?这就要涉及到它的自动装配原理,让我们一起揭开它的神秘的面纱吧

原理

SpringBoot在启动时会扫描外部引用 jar 包中的META-INF/spring.factories文件,将文件中配置的类型信息加载到 Spring 容器中(就是按照配置的类型信息,new 对象丢到Spring容器中),并执行类中定义的各种操作。

所以说,如果你想让SpringBoot自动将你的库的某个类的实例丢到Spring容器中,就需要按照其要求提供META-INF/spring.factories文件。

文件内容长下面这样:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\

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

第一行是固定的,表示下面列出的自动装配类都是要被扫描的,然后根据条件进行自动装配。

具体实现

原理非常简单,但如果不看一下具体的实现,相信你还是处于懵逼状态,面试的时候一下就让人家问住了,大家都这样不只是你,所以不要不好意思,哈哈。

SpringBoot的自动配置代码都在你项目External Libraries里的org.springframework.boot:spring-boot-autoconfigure:xxxx依赖里

其中xxxx为你使用的springboot的版本号,我使用的是2.5.4

配置文件

我们可以在其jar包的META-INF文件夹中找到一个spring.factories文件,可以看到里面有非常多要自动配置的类

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

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

这些自动配置的类很多都是SpringBoot官方提供的那些starter里的配置类,例如我们熟悉的MongoDB的配置类如下

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

我们可以看到,这里有非常多的配置类,难道SpringBoot在启动的时候都要将这些类里的@Bean实例化到Spring容器中吗?人家有那么傻吗?你都不使用MongoDb他给你配置一个你不得骂娘吗?

那SpringBoot是如何确定哪些配置类该使用,哪些该忽略呢?答案就是使用各种 ConditionalOnXXX 注解,也就是说当符合某些条件时才自动装配。

让我们以MongoDB的自动配置类来具体查看一下,下面是其部分代码,关键看此类上面的那些注解。

@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);
	}
	...
}

其中两个注解格外引人注目

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

表示当SpringBoot项目的类路径中存在MongoClient时才会自动装配此配置类。这什么意思呢?意思就是你得引入MongoDB的的starter才能引入MongoClient,SpringBoot才会自动装配此配置类。

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

如果不引入MongoDB的starter,MongoClient 会爆红,但是却没问题,有没有想过为什么?因为@ConditionalOnClass通过字节码来加载此Class的,允许其不存在类路径里。

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

这个注解又是什么意思呢?意思就是当你不仅引入了MongoDB的starter,但同时你使用了自己的MongoDatabaseFactory 那么SpringBoot也会放弃帮你自动装配,而使用你自己的配置。这就是比较高级的用法了,你比较牛逼,你知道如何配置使MongoDB可以更好的工作,所以你不需要SpringBoot帮你做决定,正所谓:我命由我不由天!

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

这个注解其实对于我们大部分人意义更为巨大,因为我们大部分时间是在使用SpringBoot,而不是写SpringBoot。还记得你在使用SpringBoot时,经常要在application.properties文件中对使用的库进行配置吗?不知道你们怎么样,反正我在刚接触SB的时候会经常抱怨:我C,我怎么知道他们的key是啥呢?为什么我只是在这配置了一下就影响到我的MongoDB了呢?这一切都是这个注解做的妖。

看到MongoProperties那个类了吗?这个就是我们经常写的解析application.properties文件配置的那个类

@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;
	...
}

是不是找到了熟悉的味道?看@ConfigurationProperties(prefix = "spring.data.mongodb") 说明MongoDB的配置都必须以spring.data.mongodb开头,
至于具体的key叫什么,自己看下这个类的属性名称呗!在这里你还可以检索到某个功能使用哪个属性来配置!

关键代码

自动装配的关键代码在org.springframework.boot.autoconfigure.AutoConfigurationImportSelector类的如下方法中

	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);
	}

这个方法通过读取过滤等操作后获得了一个要自动装配类的列表。

如何写一个自己的SpringBoot Starter

有了上面的理论知识,写一个starter已经变得很容易了,但是写好一个starter仍然有一定的挑战性。总体需要两步

  • 创建一个自己的自动装配类,包括用于自定义的配置文件类
  • META-INF/spring.factories文件中加入需要自动装配的类

一般情况下,我们的starter包括很多个互相依赖的项目,他们共同工作使得某个库可以正常工作。例如MongoDB可能要依赖其他类库,那么其他类库也会进入这个starter。例如上文提到的MongoDB的starter的依赖关系如下图所示,他们共同支撑了MongoDB的正常使用。

假设我们有一个库,这个库的功能是可以对外输出一句问候语,那我们就为这个类库写个starter,使得它可以集成到SpringBoot项目中。

创建自动配置项目

创建一个名为 hello-spring-boot-autoconfigure的maven项目 ,最终的目录结构如下:

├── 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

其中,library/ShuSheng007.java实际情况下应是在另一个库中的,就是我们要使用的那个库,假设叫library,然后这个库是要在稍后的starter项目中与此项目一起引入的。由于我们这里只是演示所以就直接放在autoconfigure项目中了。

第一步:在pom.xml文件中引入如下依赖,后两个是可选的,其作用一会再说

    <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>
<!--        在META-INF中生成属性配置的元数据给IDE,用于编辑属性文件时的代码提示,可选-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
            <version>2.5.4</version>
        </dependency>
<!--        在META-INF中生成忽略配置元数据,加快自动配置的初始化速度,可选-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure-processor</artifactId>
            <optional>true</optional>
            <version>2.5.4</version>
        </dependency>
    </dependencies>

第二步:构建属性配置类(可选)

这一步主要是为了使我们的starter具有自定义的能力,即我们可以通过application.properties文件对其进行配置

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

	//省略getter和setter
}

这里我们给出对讲话者与讲话内容进行自定义的能力,这两个值都给了默认值。

第三步:构建自动配置类

/**
 * 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());
    }
}

这里值得注意的是,我使用了@ConditionalOnProperty(value = "ss007.hello.enabled",havingValue = "true")作为条件,所以要想启用自动配置,还需要在application.properties文件中配置ss007.hello.enabled=true

第三步:将自动配置类加入META-INF/spring.factories

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

经过以上3步,自动配置项目就写完了,下一步创建starter项目,将依赖的各种库已经这个自动配置项加入即可。

创建starter项目

starter项目就比较简单了,只包含一个pom.xml文件,它只是将使用到的library与我们新键建立的hello-spring-boot-autoconfigure作为依赖引入到pom文件中。

项目结构如下所示:

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

pom.xml内容如下:

<?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>

因为我们将library合并到了hello-spring-boot-autoconfigure,所以这里只引入它一个依赖就够了

如何使用

这个不用我说了吧,毕竟天天都在用starter。

  • pom.xml中引入依赖
 <dependency>
     <groupId>top.ss007</groupId>
     <artifactId>hello-spring-boot-starter</artifactId>
     <version>1.0.0</version>
 </dependency>
  • 开启starter (可选)

为了演示我加了一个开关属性,所以需要在application.properties文件中加入ss007.hello.enabled=true的配置,很多starter不需要。

  • 使用

现在可以直接注入ShuSheng007 类的实例,然后使用其方法了,因为其已经被SpringBoot启动时自动装配好了。我们实现CommandLineRunner,让应用run起来时就调用我们的starter。

@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();
    }
}

输出:

ShuSheng007说:Hello Spring Starter

至此,一个简单的starter就已经写好了,但是还不专业,我们还可以使其更专业一点。

如何改进

  • 增加属性文件代码提示

你是否注意到,当你在编辑application.properties文件时,你的IDE会特别贴心的奉上提示。话说现在没有提示你还写的了代码吗?
如何让我们自己写的starter也具有如下的提示呢?

第一: 生成由@ConfigurationProperties注解修饰的属性类的元数据。

这个比较简单,只要在我们的hello-spring-boot-autoconfigure项目中引入如下库即可

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

这个库会自动根据@ConfigurationProperties修饰的类生成metadata给IDE使用。这个生成的元数据文件是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": "讲话者姓名",
      "sourceType": "top.ss007.hellospringbootautoconfigure.config.HelloProperties"
    },
    // 下面这个是从additional-spring-configuration-metadata.json合并过来的
   {
      "name": "ss007.hello.enabled",
      "type": "java.lang.Boolean",
      "description": "是否启动Hello的自动装配功能."
    }
  ],
  "hints": []
}

可见,我们在HelloProperties类的java doc (讲话者姓名)也会被包含在元数据中给IDE读取。

第二: 生成其他属性的元数据

我们的项目中,除了HelloProperties类以外,还有一个属性ss007.hello.enabled,这个属性通过上面的方式是无法产生元数据的,进而导致IDE不提示,那么有没有办法解决这个问题呢?有,我们需要提供一个自定义的配置元数据,让spring-boot-configuration-processor帮我们加到spring-configuration-metadata.json文件中去。

hello-spring-boot-autoconfigure项目的resources/META-INF中添加一个additional-spring-configuration-metadata.json文件

{"properties": [
    {
      "name": "ss007.hello.enabled",
      "type": "java.lang.Boolean",
      "description": "是否启动Hello的自动装配功能."
    }
  ]
}

即可。

  • 提供自动装配过滤条件元数据

我们知道,SpringBoot在自动装配时会通过各种过滤条件来确定某个类是否需要装配,这在装配类太多的时候会影响启动数据,我们可以将过滤条件提供给IDE,进而加快这个流程。

只需要在hello-spring-boot-autoconfigure项目中引入如下工具即可:

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

它会在target/classes/META-INF文件中生成一个spring-autoconfigure-metadata.json

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

总结

不知不觉文章变得很长了,通过此文相信你已经彻底脱了SpringBoot Starter的底裤了…值此中秋佳节,竟然阴雨绵绵,只能窝在家中写文章。

首发及源码地址

以上是关于SpringBoot自动配置原理及如何创建自己的Starter的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot核心原理实现及核心注解类

Springboot定时任务原理及如何动态创建定时任务

面试题: SpringBoot 的自动配置原理及定制starter

浅析SpringBoot自动配置的原理及实现

SpringBoot自动配置的原理及实现/SpringBoot之@Import注解正确使用方式

SpringBoot启动及自动装配原理