从零开始的Spring Boot自动配置学习和Starter制作教程

1,842 阅读30分钟

现在的Java后端开发中,Spring Boot早已被广泛使用,使用它,我们轻轻松松地就可以搭建起一个后端服务,发挥出你无限的创造力。

为什么Spring Boot可以这么方便呢?在Spring Boot问世之前,Spring为什么又会让人觉得繁琐呢?

这很大程度得益于Spring Boot的自动配置机制,并且在Spring Boot生态中,有着非常多的Starter。

Spring Boot的Starter指的是利用Spring Boot自动配置机制,完成一些依赖的Bean的预先配置的一种依赖,在帮助我们一键引入所有所需依赖的同时,还能自动完成某些Bean的配置。

所以什么是自动配置机制?Starter到底做了什么?Spring Boot到底方便在哪里?

可能刚刚开始学习后端开发,仅仅接触过Spring Boot而没有从事单纯的Spring开发的同学,脑袋里会有这些问号。

没关系,今天我们从零开始,了解一下Spring框架是如何配置各种外部依赖的,以及Spring Boot的Starter到底省略了那些事情。

在这之前,大家需要先搞清楚Spring框架的一些核心概念例如依赖注入、控制反转、IoC容器是什么、Spring Bean是什么等等。

1,从编写一个外部库为例开始

无论是使用Spring框架开发,还是Spring Boot,我们都需要引入很多外部库依赖例如连接数据库的、安全框架等等。引入依赖之后,要想将依赖中需要的类作为Bean交给IoC容器托管,就需要做一些配置。

这里我们自己开发一个简单的外部库,用作简单的日志打印功能,这个外部库有以下功能:

  • 输出infowarn类型的日志
  • 允许用户配置输出日志时是否显示时间

开发了这个外部库之后,我们来对比一下通过Spring引用并配置这个外部库,以及将其做成Starter后在Spring Boot引用,这两种情景下有什么区别。

言归正传,我们开始第一步吧。

先创建一个空的Maven项目,不需要任何依赖,编写存放日志配置的类LogConfig

package com.gitee.swsk33.logcoredemo.config;

/**
 * 日志功能的配置类
 */
public class LogConfig {

	/**
	 * 是否显示时间
	 */
	private boolean showTime;

	// 对应getter和setter方法
	public boolean isShowTime() {
		return showTime;
	}

	public void setShowTime(boolean showTime) {
		this.showTime = showTime;
	}

}

这就是一个简单的POJO类。

然后创建我们的核心逻辑功能类LogService

package com.gitee.swsk33.logcoredemo.service;

import com.gitee.swsk33.logcoredemo.config.LogConfig;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 日志功能类
 */
public class LogService {

	/**
	 * 日志配置字段(需要用户注入,因此这个字段要有Setter方法)
	 */
	private LogConfig config;

	// config字段的setter方法
	public void setConfig(LogConfig config) {
		this.config = config;
	}

	/**
	 * 工具类:获取当前时间字符串
	 *
	 * @return 当前时间字符串
	 */
	private String getTimeString() {
		// 自定义时间格式:年/月/日-时/分/秒
		DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy/MM/dd-HH:mm:ss");
		// 时间对象转换成自定义的字符串形式
		return LocalDateTime.now().format(format);
	}

	/**
	 * 输出告示类消息
	 *
	 * @param message 日志消息
	 */
	public void info(String message) {
		// 根据配置判断是否输出时间
		String messageString = config.isShowTime() ? "[INFO] " + getTimeString() + " " + message : "[INFO] " + message;
		System.out.println(messageString);
	}

	/**
	 * 输出警告类消息
	 *
	 * @param message 日志消息
	 */
	public void warn(String message) {
		// 根据配置判断是否输出时间
		String messageString = config.isShowTime() ? "[WARN] " + getTimeString() + " " + message : "[WARN] " + message;
		System.out.println(messageString);
	}

}

里面的代码很简单,这里不再详细介绍了。

好的,我们的外部库就开发完成了!现在在项目目录下执行mvn clean install命令将其安装至本地Maven仓库,使得待会可以引用这个外部库。

在这里这个外部库的groupIdcom.gitee.swsk33artifactIdlog-core-demoversion1.0.0,这里大家自己在pom.xml设定好即可。

2,在Spring项目中引用并配置这个外部库

好的,假设现在有一个一个使用Spring框架的开发者(下文将这个开发者称作外部库使用者),需要使用我们的日志外部库,并将其中需要使用的服务类LogService交给Spring的IoC容器托管,这样除了引用这个外部库之外,还需要定义一些Bean的配置。

再创建一个空的Maven项目,引入Spring依赖以及我们的日志外部库依赖等等:

<!-- Spring 上下文 -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-context</artifactId>
	<version>6.1.3</version>
</dependency>

<!-- 调用我们的日志核心外部库 -->
<dependency>
	<groupId>com.gitee.swsk33</groupId>
	<artifactId>log-core-demo</artifactId>
	<version>1.0.0</version>
</dependency>

仅仅是引入依赖,依赖中的服务类并不会被Spring框架实例化为Bean并放入IoC容器,因为外部库中的类不仅没有标注@Component等等注解,也没有说包含XML文件。

所以使用Spring框架的开发者在这时还需要手动地配置一下Bean,才能在后续开发时通过IoC容器取出对应的服务类的Bean并正常使用。

这位开发者可能使用XML的方式配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	<!-- 定义日志库中的配置类的Bean -->
	<bean id="logConfig" class="com.gitee.swsk33.logcoredemo.config.LogConfig">
		<!-- 配置为显示时间 -->
		<property name="showTime" value="true"/>
	</bean>

	<!-- 定义日志库中的服务类的Bean,并注入配置 -->
	<bean id="logService" class="com.gitee.swsk33.logcoredemo.service.LogService">
		<!-- 将上述的配置Bean注入进来 -->
		<property name="config" ref="logConfig"/>
	</bean>
</beans>

也可以是通过注解的方式,创建配置类进行配置:

package com.gitee.swsk33.springannotationbased.config;

import com.gitee.swsk33.logcoredemo.config.LogConfig;
import com.gitee.swsk33.logcoredemo.service.LogService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 用于日志外部库的配置类:将日志库中需要的类注册为Bean
 */
@Configuration
public class LogServiceConfig {

	/**
	 * 实例化一个日志配置类,并设定配置,然后注册为Bean
	 */
	@Bean
	public LogConfig logConfig() {
		LogConfig config = new LogConfig();
		// 设定显示时间
		config.setShowTime(true);
		return config;
	}

	/**
	 * 将LogService类实例化,并注册为Bean,并注入配置对象依赖
	 */
	@Bean
	public LogService logService(LogConfig logConfig) {
		LogService logService = new LogService();
		logService.setConfig(logConfig);
		return logService;
	}

}

无论如何,也就是说如果要将对应的对象交给Spring框架托管,那么开发者需要为外部库中的类编写Bean配置,才能够使用

以注解的方式为例,配置完成后,才能从IoC容器中取出LogService类的Bean并使用:

package com.gitee.swsk33.springannotationbased;

import com.gitee.swsk33.logcoredemo.service.LogService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class Main {

	public static void main(String[] args) {
		// 创建IoC容器,基于注解的
		ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
		// 从容器中取出日志服务对象并调用
		LogService logService = context.getBean(LogService.class);
		logService.info("调用日志服务!");
		logService.warn("调用日志服务!");
	}

}

image.png

我们这里的日志外部库中的类比较简单,实际开发中许多外部库中的类,及其依赖关系都是很复杂的,因此开发者在利用Spring框架开发并引用它们的时候,都需要为这些外部库的对应类编写Bean配置,可见这是比较麻烦的。

毕竟你不知道你所使用的外部库中的类是否都标注了@Component等注解,也不知道外部库的开发者是否编写了XML配置,所以你需要自己为外部库中的类配置Bean的定义。

3,使用Spring Boot的自动配置机制解决上述麻烦

引入依赖后还要写配置,这实在是太麻烦了!能不能引入依赖后就直接使用呢?

当然,Spring Boot的自动配置机制就实现了这一点,当然,这是借助Starter依赖完成的。

(1) 自动配置机制概述

Spring Boot自动配置机制是尝试根据开发者添加的jar依赖项,自动配置Spring应用程序。

例如前面我们引入了日志外部库,那么自动配置机制就会自动地将这个外部库中的类初始化为Bean,而无需像前面一样先手动配置Bean。

自动配置是如何完成的呢?

可以说,要想自动配置一个外部库中的类,至少需要下列两个东西:

  • 自动配置类
  • 自动配置候选类配置文件

这两样东西,通常就放在一个称作Starter的依赖中,然后Starter就会被打包发布,外部库的使用者引用即可。

下面,我们先单独看看Starter中的这两个东西是什么。

1. 自动配置类

在上述使用Spring框架引入外部库时,要手动地给外部库中的类LogServiceLogConfig编写Bean的定义配置,那么外部库的开发者能不能预先编写好这些Bean的配置呢而不是我们使用者去编写呢?

当然可以,根据这个思路,外部库开发者可以定义一个配置类,在其中通过@Bean标注的方法,创建对应的类的Bean对象,以及约定好默认配置,然后交给IoC容器托管。外部库开发者完成了Bean的定义编写,是不是就不需要我们外部库使用者去编写Bean的配置了呢?

这里所说的配置类,就是Starter中的自动配置类。自动配置类就是一个普通的标注了@Configuration的类,其中使用标注了@Bean的方法完成对Bean的定义,这就是自动配置类完成的工作,自动配置类由外部库开发者编写并放在Starter中。

2. 自动配置候选类配置文件

到这里又有一个问题了,开发者确实先定义好了一个自动配置类,但是我们知道Spring框架并不是会扫描所有的类的,那是不是说明我们还要通过@ComponentScan注解配置一下外部库的包路径呢?

当然不是了!不然怎么体现出自动配置中的“自动”这个特点呢?

所以外部库的开发者除了编写完成自动配置类之外,还需要编写一个自动配置候选类配置文件放在Starter中,这个配置文件中就是声明哪些类是自动配置类,这样Spring Boot的自动配置机制会去先读取这些自动配置候选类的配置文件,找到所有的自动配置类后,再去加载这些自动配置类,完成自动配置。

自动配置候选类配置文件也是包含在Starter中的,并且放在固定的位置。

在Spring Boot启动时,会扫描所有的外部库的classpath下所有的META-INF/spring.factories文件(Spring Boot 2.x版本),在这个文件中读取哪些类需要被读取以进行自动配置,可见这个META-INF/spring.factories文件就是我们所说的自动配置候选类配置文件。

Spring Boot应用程序默认开启了自动配置功能,因为Spring Boot的主类上通常有@SpringBootApplication这个注解,而这个注解中包含了@EnableAutoConfiguration注解,这个注解就是用于开启自动配置功能的,至于其底层原理,就不在此赘述了!

这里说明一下,Spring Boot 2.x版本和3.x版本的自动配置候选类的配置文件是不一样的:

  • Spring Boot 2.x启动时,是扫描所有的外部库classpath下所有的META-INF/spring.factories文件
  • Spring Boot 3.x启动时,是扫描所有的外部库classpath下所有的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件

classpath表示类路径,可以理解为Java程序编译并打包为jar文件后jar包内的路径,在Maven项目中,项目目录下的src/main/resources目录就对应着classpath的根路径/

外部库开发者完成了自动配置类的编写以及自动配置候选类配置文件的编写,就可以将其打包为Starter并发布,我们引入Starter即可!这样Spring Boot启动时,扫描到Starter中的自动配置候选类配置文件并读取到需要加载的配置类,就能够完成配置类加载。外部库的使用者只需要引入Starter作为依赖,然后直接就可以从IoC容器中获取外部库中需要用的类的Bean了!

(2) 为我们的日志外部库制作一个Starter

讲解了这么多的自动配置机制,大家可能还是不知道Starter里面到底装着啥,所以我们现在就为我们上述的日志外部库编写一个Starter。

1. 创建Starter工程

首先创建一个新的Spring Boot依赖,并勾选Spring Configuration Processor依赖:

image.png

然后在pom.xml中,删除spring-boot-starter-test依赖,以及build部分,这些是不需要的:

image.png

然后把项目中的主类和resources目录下的配置文件也删掉,这也是用不着的:

image.png

这样,一个空的Starter工程就创建完成了!

在这里,Starter工程中通常有两个关键依赖大家可以看一下:

<!-- Spring Boot Starter 支持 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- Spring Boot Starter 配置生成器 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

2. 加入日志外部库依赖

这个Starter是为我们的日志外部库制作的,当然要在这个工程中加入日志外部库作为依赖了!当然主要目的是我们可以引用到外部库中的类并实例化为Bean然后交给IoC容器。

<!-- 引入我们的外部库 -->
<dependency>
	<groupId>com.gitee.swsk33</groupId>
	<artifactId>log-core-demo</artifactId>
	<version>1.0.0</version>
</dependency>

3. 编写配置属性读取类

在上述日志外部库中,有LogConfig类专门用于存放用户的配置信息,这个类中的配置值是可以由外部库使用者自定义的。

我们也知道在Spring Boot中可以让使用者把配置写在application.properties配置文件中,然后我们读取,现在我们就创建一个这样的配置读取类

package com.gitee.swsk33.logspringboot2starter.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 用于读取配置文件(application.properties或application.yml)中的配置的属性配置类
 */
@Data
@ConfigurationProperties(prefix = "com.gitee.swsk33.log-core-demo")
public class LogConfigProperties {

	/**
	 * 是否显示时间(默认为false)
	 */
	private boolean showTime = false;

}

利用@ConfigurationProperties注解,即可将Spring Boot配置文件中的对应配置读取并赋予到这个类的对应属性中,以实现我们自定义配置值,这里就不再过多赘述这个注解的作用了!

4. 编写自动配置类

这里就是Starter的核心了!创建上述所说的自动配置类,这个类就是用于在其中约定好对应的Bean对象,并交给IoC容器托管:

package com.gitee.swsk33.logspringboot2starter.autoconfigure;

import com.gitee.swsk33.logcoredemo.config.LogConfig;
import com.gitee.swsk33.logcoredemo.service.LogService;
import com.gitee.swsk33.logspringboot2starter.properties.LogConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 用于自动配置日志库中的服务类的自动配置类
 */
// 该类标记为配置类
@Configuration
// 通过@EnableConfigurationProperties注解指定我们的属性配置类,才能在这个类中使用自动装配获取到属性配置类的Bean并读取配置
@EnableConfigurationProperties(LogConfigProperties.class)
public class LogServiceAutoConfiguration {

	/**
	 * 获取属性配置类以读取配置文件中的配置值
	 */
	@Autowired
	private LogConfigProperties logConfigProperties;

	/**
	 * 在这里创建服务类LogService的实例,设定配置并注册为Bean
	 */
	@Bean
	public LogService logService() {
		// 以读取的配置值创建配置对象
		LogConfig config = new LogConfig();
		config.setShowTime(logConfigProperties.isShowTime());
		// 实例化日志服务类并设定配置
		LogService service = new LogService();
		service.setConfig(config);
		// 输出一个提示语
		System.out.println("------- LogService自动配置完成!-------");
		return service;
	}

}

这个类并不难,我们来看一下其中的一些要点:

  • @EnableConfigurationProperties注解:表示加载一个配置属性读取类(标注了@ConfigurationProperties注解用于读取配置文件值的类),并将其实例化为Bean注册到IoC容器,这样就可以在该配置类中使用自动装配得到配置属性读取类,获取配置值
  • 在其中我们写了一个带有@Bean的方法,@Bean注解的作用相信大家都知道了,方法中我们完成了最开始在Spring开发中使用者的Bean的定义工作,即创建好外部库的LogService类型的Bean并放入IoC容器中去

5. 编写自动配置候选类配置文件

上述了解了自动配置过程,我们知道要想Spring Boot能够加载到上述的自动配置类LogServiceAutoConfiguration,还需要编写自动配置候选类配置文件并放在指定位置。

这个配置文件的编写和位置在Spring Boot 2.x版本和3.x版本是有区别的,下面分别来讲解。

① Spring Boot 2.x版本

假设你要制作Spring Boot 2.x的Starter,那就在resources目录下创建META-INF/spring.factories文件:

image.png

在里面声明上述的自动配置类的全限定类名:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gitee.swsk33.logspringboot2starter.autoconfigure.LogServiceAutoConfiguration

如果你有多个自动配置类,则以逗号,隔开,例如:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.mycorp.libx.autoconfigure.LibXAutoConfiguration,com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

当然,这样写成一行不太美观,可以借助\换行,如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
② Spring Boot 3.x版本

如果你是制作Spring Boot 3.x的Starter,那就在resources目录下创建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件:

image.png

在其中直接声明自动配置类的全限定类名即可:

com.gitee.swsk33.logspringboot3starter.autoconfigure.LogServiceAutoConfiguration

多个自动配置类则每行一个,例如:

com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
③ 同时兼容Spring Boot 2.x和3.x

可见上述2.x版本和3.x版本的Starter,其自动配置候选类配置文件的写法和位置都有所区别,那么能否开发一个Starter同时适配Spring Boot 2.x和3.x版本呢?事实上是可以的。

从Spring Boot 2.7.x版本开始,官方就开始建议开发者往3.x版本过渡了!因此使用Spring Boot 2.7.x时,在扫描自动配置类候选位置时,会同时扫描Spring Boot 2.x的META-INF/spring.factories和Spring Boot 3.x的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports配置文件来确定自动配置的类,大家可以查看官方2.7.x的Release Notes以了解详情:传送门

image.png

因此,我们可以基于Spring Boot的2.7.0以上以及3.0之前的版本制作Starter,实现这个Starter能够同时兼容Spring Boot 2.x版本和3.x版本。

这里我们先设定Starter的Spring Boot版本,以2.7.11为例:

image.png

配置好依赖、编写好自动配置类、配置属性读取类后,我们就来编写自动配置候选类配置文件了!我们同时编写2.x3.x的配置文件并放在对应位置即可,首先是3.x对应的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件:

com.gitee.swsk33.logspringbootstarter.autoconfigure.LogServiceAutoConfiguration

然后是2.x对应的META-INF/spring.factories文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gitee.swsk33.logspringbootstarter.autoconfigure.LogServiceAutoConfiguration

最终,该Starter同时存在两个自动配置候选类配置文件

image.png

可见制作一个同时兼容2.x3.x版本的Starter更加实用,总的来说我们要注意下列要点:

  • 最好是设定Starter项目Java版本为1.8
  • 最好是基于Spring Boot的2.7.11版本制作Starter
  • 同时编写2.x3.x对应的自动配置候选类配置文件

好的,到此我们的Starter就制作完成了!同样地,执行mvn clean install命令将其安装至本地Maven仓库。

(3) 使用Spring Boot调用我们的Starter

在Spring Boot项目中直接引入我们的Starter的工件坐标作为依赖即可,然后就可以在我们需要使用的地方,直接通过@Autowired注解注入LogService类的对象即可使用!

package com.gitee.swsk33.springboot3use;

import com.gitee.swsk33.logcoredemo.service.LogService;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBoot3UseApplication {

	/**
	 * 在需要使用的地方自动装配一下即可!
	 */
	@Autowired
	private LogService logService;

	@PostConstruct
	private void init() {
		// 测试调用
		logService.info("调用日志服务!");
	}

	public static void main(String[] args) {
		SpringApplication.run(SpringBoot3UseApplication.class, args);
	}

}

这里直接在主类调用,然后运行试试:

image.png

可见控制台输出了对应消息,说明我们制作的Starter自动配置成功!

除此之外,使用者还可以在配置文件application.properties中进行对应配置:

# 配置显示时间
com.gitee.swsk33.log-core-demo.show-time=true

相信到这里,大家就知道Starter是什么了!可见Starter帮我们完成了下列工作:

  • 导入所有所需的依赖:上述Starter中引入了所有需要的依赖,包括日志外部库,这样开发者只需要引入Starter作为依赖即可,不需要手动配置所有依赖
  • 完成了Bean的定义:Starter中已经完成了对外部库中使用的类的Bean的定义,而不需要使用者像最开始使用Spring框架开发时自己编写外部库中的Bean定义
  • 抽离出用户可自定义的配置部分:例如上述日志的配置部分,即配置是否显示时间的部分,是可以由使用者自定义的,在Starter中我们用配置属性读取类LogConfigProperties抽离出了自定义的部分,使得使用者在Spring Boot的配置文件中定义自定义的配置值即可

除此之外,我们也学习到了Spring Boot中导入Starter作为依赖时,自动配置的大致过程如下:

  1. 应用程序启动,Spring Boot扫描所有依赖的classpath路径下的自动配置候选类配置文件,在里面读取到哪些类是用于自动配置的类,其中:

    1. Spring Boot 2.x扫描的是classpath下所有的META-INF/spring.factories文件
    2. Spring Boot 3.x扫描的是classpath下所有的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件
  2. 读取到所有自动配置类后,就会将这些类实例化为Bean并放入IoC容器(自动配置类要标注@Configuration注解),这个过程中,这些自动配置类中的所有@Bean方法也会被执行,这样开发者预先约定的Bean就被初始化好了并注册到IoC容器,后续开发者只需通过自动装配获取对应的类的Bean即可

上述自动配置类被加载并初始化为Bean的过程,我们就称之为完成自动配置,也可以叫做配置生效

可见,Spring Boot的自动配置机制并不是很难,制作Starter也不是一件难事,对于Spring Boot 2.x和3.x版本的区别,也就是自动配置候选类配置文件的位置和表示方式不同而已。

(4) 讨论一下@EnableConfigurationProperties

在这里我们来讨论一下为什么要在上述的自动配置类LogServiceAutoConfiguration上标注@EnableConfigurationProperties(LogConfigProperties.class)注解,我们在LogConfigProperties上面标注@Component注解,然后直接在需要的地方进行@Autowired不行吗?

这样的话,在我们自己的项目中是可以的,但是制作成Starter,让别人去引用那就不行了。

这还是可以回到上述自动配置机制概述部分中,为什么需要自动配置候选类配置文件的讨论。因为Starter作为依赖引入其它项目的时候,Starter下的类所在的包并不在项目的扫描路径下(Spring Boot扫描Bean通常是从主类开始向下扫描其所在的包及其子包)。

而我们上述的自动配置候选类中只声明了LogServiceAutoConfiguration,也因此Starter被其它项目引入时,只会将LogServiceAutoConfiguration类初始化为Bean,如果不使用@EnableConfigurationProperties,那么读取配置文件的类LogConfigProperties.class不会被初始化为Bean,我们就无法通过自动装配获取它,毕竟它不在扫描范围内。

因此在LogServiceAutoConfiguration类上面标注@EnableConfigurationProperties(LogConfigProperties.class),这样Spring Boot将LogServiceAutoConfiguration初始化为Bean时,读取到注解@EnableConfigurationProperties(LogConfigProperties.class),就也会将LogConfigProperties初始化为Bean,完成配置的读取了!

讨论这个部分,其实是着重强调一下Starter的开发和我们普通Spring Boot项目有所不同:

  • 日常的Spring Boot项目开发,所有在主类相同层级及其子层级下标注了Bean相关注解(@Service@Component等等)都位于扫描范围内,都会被初始化为Bean
  • 而Starter的开发中,只有声明在自动配置候选类文件中的类,才会被初始化为Bean

(5) Java EE和Jakarta EE不一致导致自动配置失效问题

有的同学在制作同时兼容2.x3.x的Starter时可能会踩一个坑:这个Starter在Spring Boot 2.x项目中被引入后可以正常使用,但是在Spring Boot 3.x项目中被引入却无法被自动配置

这通常是由于我们的Starter中的自动配置类使用了Java EE或者Jakarta EE中的注解而导致,比如说@PostConstruct注解、@Resource注解等等。

因为要想制作同时兼容2.x3.x的Starter的话,就要基于Spring Boot 2.7.x版本,Spring Boot 2.x版本都使用的是Java EE规范,而Spring Boot 3.x则改用的是Jakarta EE规范,如果在Starter中使用了例如@PostConstruct这样的注解,当该Starter被引入Spring Boot 3.x项目中时,就会导致Starter中该注解命名空间和项目中该注解的命名空间不一致(Starter中@PostConstruct对应Java EE规范,位于包路径javax.annotation下,而项目中认为@PostConstruct对应Jakarta EE规范,位于包路径jakarta.annotation下),进而导致Starter中对应自动配置类中注解无法被项目加载,结果该自动配置类无法被配置。

所以正确的做法是,在Starter的自动配置类中应当尽量避免使用Java EE或者Jakarta EE规范中的注解,例如一个自动配置类需要在自动配置完成后(被实例化为Bean后)进行一些自定义的初始化操作,就不能使用@PostConstruct注解了,而是应当实现InitializingBean接口:

package com.gitee.swsk33.logspringbootstarter.autoconfigure;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;

/**
 * 在制作能够同时兼容Spring Boot 2.x和3.x的Starter时
 * 如果需要在自动配置类的Bean初始化时定义一些操作,则尽量通过实现InitializingBean接口去完成
 * 尽量避免使用@PostConstruct注解,因为Spring Boot 2.x和3.x分别使用的是Java EE和Jakarta EE规范
 * 如果使用会导致Bean在Spring Boot 3.x中无法被自动配置
 */
@Slf4j
@Configuration
public class TestInitializeAutoConfiguration implements InitializingBean {

	@Override
	public void afterPropertiesSet() {
		log.info("自动配置类的自定义初始化逻辑...");
	}

}

4,条件注解

上述我们成功地制作了一个Starter,不过我们也发现这个Starter中的配置,是一定会被加载的,我们能不能设定条件,比如说用户不需要的时候就不加载它以节省内存呢?当然可以!Spring Boot还提供了许多条件加载Bean的注解。

(1) 类加载条件

Spring Boot提供了@ConditionalOnClass@ConditionalOnMissingClass这两个注解,我们直接看例子:

// 省略package和import

/**
 * 用于自动配置日志库中的服务类的自动配置类
 */
@Configuration
@EnableConfigurationProperties(LogConfigProperties.class)
@ConditionalOnClass(LogService.class)
public class LogServiceAutoConfiguration {

	// 省略自动配置类的内容

}

我们在上述的自动配置类上面标注了@ConditionalOnClass(LogService.class)注解,表示只有当加载到LogService这个类的时候,这个自动配置类LogServiceAutoConfiguration才会被加载并初始化为Bean。

否则这个类不会被加载,其中的@Bean方法也会不生效,这个配置也就不生效了!

大家可以把上述Starter中的日志外部库依赖删掉,然后加上@ConditionalOnClass(LogService.class)注解,最后在Spring Boot工程中引用这个Starter,观察一下这个类会不会被加载。

那很简单,@ConditionalOnMissingClass就是和它相反,例如你标注@ConditionalOnMissingClass(LogService.class)就说明如果没有加载到LogService这个类,这个字段配置类才会被加载并初始化为Bean。

这两个注解只能用在类上面,而不能用在标注了@Bean的方法上!

(2) Bean条件

Spring Boot还可以根据是否存在或者不存在某个Bean作为条件,来初始化你的Bean,还是看下列例子:

// 省略package和import

/**
 * 用于自动配置日志库中的服务类的自动配置类
 */
@Configuration
@EnableConfigurationProperties(LogConfigProperties.class)
public class LogServiceAutoConfiguration {

	@Autowired
	private LogConfigProperties logConfigProperties;

	/**
	 * 在这里创建服务类LogService的实例,设定配置并注册为Bean
	 */
	@Bean
	@ConditionalOnBean
	public LogService logService() {
		// 以读取的配置值创建配置对象
		LogConfig config = new LogConfig();
		config.setShowTime(logConfigProperties.isShowTime());
		// 实例化日志服务类并设定配置
		LogService service = new LogService();
		service.setConfig(config);
		// 输出一个提示语
		System.out.println("------- LogService自动配置完成!-------");
		return service;
	}

}

可见上述logService方法上,标注了@ConditionalOnBean,表示在自动配置时,IoC容器中存在LogService类型的Bean的时候,就会执行这个方法以生成Bean。

同样地,如果改成:

@Bean
@ConditionalOnMissingBean
public LogService logService() {
	// 省略方法内容	
}

表示在自动配置时,IoC容器中不存在LogService类型的Bean的时候,才会执行这个方法以生成Bean。

可见,这两个注解直接标注(不传参)@Bean的方法上时,是判断这个方法的返回类型的Bean是否存在/不存在

当然,还可以这样:

@Bean
@ConditionalOnMissingBean(LogConfig.class)
public LogService logService() {
	// 省略方法内容
}

上述指定了注解的value字段值,表示当IoC容器中不存在LogConfig类型的Bean的时候才会执行这个方法生成Bean。

还可以这样:

@Bean
@ConditionalOnMissingBean(name = "logService")
public LogService logService() {
	// 省略方法内容
}

上述指定了注解的name字段值,表示当IoC容器中不存在名(id)logService的Bean的时候才会执行这个方法生成Bean。

那么@ConditionalOnBean注解同理。

事实上,@ConditionalOnMissingBean这个注解是很常用的,使用这个注解,可以允许用户是自定义这个Bean还是使用外部库开发者提供的默认的Bean

我们来看看Redis的Starter中,RedisTemplate类型的Bean:

image.png

这是Spring Boot的Redis的Starter中,用于自动配置RedisTemplate类型Bean的方法,这里加上了@ConditionalOnMissingBean注解,指定当未找到名为redisTemplate的Bean的时候,就会执行这个方法将RedisTemplate类型Bean注册到IoC容器中。

这样,如果用户需要自行配置RedisTemplate,例如配置Redis的序列化方式时,用户会自己创建一个RedisTemplate类型Bean,配置好序列化方式后就注册到IoC容器,这时有了用户自己创建的RedisTemplate类型Bean,上述官方Starter中的这个方法就不会被执行,就可以让用户使用自己自定义的Bean。

可见这种思路,可以使得用户去选择是使用自己自定义的Bean,还是使用官方给出的默认的Bean

(3) 配置文件条件

官方还提供了@ConditionalOnProperty注解,表示当读取到配置文件application.properties中有特定的配置值的时候,才会实例化某个Bean,例如:

@Bean
@ConditionalOnProperty(prefix = "com.gitee.swsk33", name = "enable-log", havingValue = "true")
public LogService logService() {
	// 省略方法内容
}

这表示只有配置文件中,存在配置项com.gitee.swsk33.enable-log并且其值为true时,这个方法才会被执行以生成Bean。

这个注解中,通过prefixname属性,指定具体的配置项名称,还有havingValuematchIfMissing两个属性,就是用于自定义匹配的规则了,其两者的详细作用如下:

  • havingValue属性表示当指定配置为什么值时,才表示匹配,需要分两种情况讨论:
    • 如果未提供该属性(默认情况下),那么指定的配置存在且值不为false就表示匹配
    • 如果提供了该属性并指定了值,那么指定的配置值必须存在且与havingValue属性指定的值相等时才被视为匹配
  • matchIfMissing属性控制了缺失配置属性时条件的匹配行为,如果:
    • 该属性为false,那么指定的配置不存在则视为不匹配(该属性默认为false
    • 反之该属性为true,那么指定配置不存在时,条件将被视为匹配,将创建被注解标记的Bean

加上上述注解,用户就可以通过配置文件来启用或者禁用日志功能:

# 启用日志功能
com.gitee.swsk33.enable-log=true

反之只需把配置值改成false,上述@Bean方法就不会被执行,这个配置不生效。

可见条件注解可以根据设定的条件控制哪些自动配置类被初始化为Bean,那么在这个过程中有的自动配置类没有被初始化为Bean,就称之为配置不生效

5,自动配置类的加载顺序

在Spring Boot中,还提供了@AutoConfigureAfter@AutoConfigureBefore这两个注解用于控制Starter中的自动配置类的加载顺序

假设现在我有两个自动配置类FirstSecond(都被声明在自动配置候选类配置文件中的),并且Second中的某个方法需要调用First类中的某个成员变量,所以这就要求First类必须在Second类之前完成加载

我们可以在Second上标注注解@AutoConfigureAfter如下:

// 省略package和import

@Configuration
@AutoConfigureAfter(First.class)
public class Second {

	// 省略内容

}

这就表示Second类将在First类完成自动配置之后才会进行自动配置。

反之,还可以使用@AutoConfigureBefore如下:

// 省略package和import

@Configuration
@AutoConfigureBefore(Second.class)
public class First {

	// 省略内容

}

这就表示First类将在Second之前进行自动配置。

上述两个注解还可以传入多个类组成的数组作为参数,需要注意的是上述两个注解只能够用于控制自动配置类之间的初始化顺序,也就是只对声明在自动配置候选类配置文件中的类生效。如果将其标注在其它类上面,或者是传入不是自动配置类的类作为参数,那么这两个注解将不会生效。

除此之外,如果你的Starter依赖了其它的 Starter,那么上述注解还能够传入其它的Starter中的类,例如:

@Configuration
@AutoConfigureAfter({DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public class MyAutoConfiguration {
    // 自动配置相关的代码
}

这意味着MyAutoConfiguration类将在DataSourceAutoConfigurationHibernateJpaAutoConfiguration类之后进行自动配置,传入的这两个类属于其它的Starter中的自动配置类,虽然不在自己的Starter中但是也是生效的。

6,总结

可见Spring Boot的自动配置,大大地方便了我们的开发,这也是为什么我们平时引入依赖例如MongoDB的Starter后,就可以直接自动装配MongoTemplate并使用了,非常方便。这些,都是自动配置机制,以及各个Starter帮我们简化了开发。

在最后来总结一下制作一个Starter的注意事项:

  • Starter中通常并不包含一个库的核心功能或者业务代码,只包含自动配置类自动配置候选类配置文件,当然有时候也可能会包含配置属性读取类。这说明Starter应当是和外部库核心是分开的,例如上述日志外部库的核心功能的代码并没有包含在Starter中,而是作为一个单独的项目,Starter只是引用它而已,当然这也要视实际情况而定,并非一定要遵守
  • Starter的artifactId通常也是有所讲究的,我们通常将自己制作的Starter命名为xxx-spring-boot-starter,例如log-spring-boot-starter,这个规范需要遵守,我们也可以发现Spring官方的Starter的命名格式为spring-boot-starter-xxx,例如spring-boot-starter-web,和我们自己的Starter命名“相反”,可见上述规范也是为了将第三方Starter和官方的区分开来
  • 制作Starter时最好选择较低的Spring Boot版本,可以自行修改pom.xml中的parent部分的版本号,当然要区分2.x3.x的Starter,也可以制作同时兼容2.x3.x版本的Starter,制作适用于2.x版本的Starter可以选择2.7.11版本,制作适用于3.x版本的Starter可以选择3.0.0版本,所有的Spring Boot版本可以在Maven中央仓库查看,官方仍提供支持的版本可以参考官网
  • 我们创建的Spring Boot项目都是默认以spring-boot-starter-parent为父项目的,这个父项目只是用于统一所有的Spring相关依赖版本,并不会引入任何额外依赖,因此如果你的Starter需要继承自己的父模块,也是可以的,只不过引入spring-boot-starter等Spring相关依赖时,需要声明版本号了
  • 如果一个Spring Boot项目所使用的Spring Boot的版本,比它所引入的Starter所使用的Spring Boot版本低,那也是可以使用的

本文以制作一个简单的外部库为例,比较了Spring框架直接引用外部库并配置,以及制作为Starter后使用Spring Boot引用这两种情景的区别,认识Spring Boot的自动配置机制帮我们简化了哪些步骤,以及Starter是由什么组成的,怎么制作。

这些对于初学者来说可能有些难以理解,希望大家能够仔细阅读完成本文的每一个部分,一步步地认识到自动配置机制解决了什么问题,以及其大致过程。

本文的参考文献:

  • Spring Boot自动配置机制概述:传送门
  • Spring Boot 2.x Starter制作指引:传送门
  • Spring Boot 3.x Starter制作指引:传送门

本文的代码仓库地址:传送门