Spring Boot 凭借“约定大于配置”的核心理念,彻底解决了传统Spring框架繁琐的XML配置问题,让Java企业级开发效率实现质的飞跃。而自动配置(AutoConfiguration)作为Spring Boot的灵魂核心,是所有开发者入门必学、进阶必懂的知识点。本文将从底层源码出发,用通俗的语言拆解自动配置的完整执行流程,配合可直接运行的实战示例,帮你彻底吃透自动配置原理,同时解决日常开发中的常见坑点。
一、自动配置到底是什么?和手动配置的核心区别
传统Spring开发中,比如整合MyBatis,需要手动配置DataSource、SqlSessionFactory、MapperScannerConfigurer等核心Bean,要么编写冗长的XML文件,要么通过@Configuration+@Bean手动注册,每个第三方依赖都要重复编写配置代码,不仅效率低下,还极易出现配置错误。
而Spring Boot自动配置,是框架根据项目引入的依赖包(Starter),自动判断需要注册的Bean组件,自动完成配置参数的绑定与Bean的初始化,开发者直接注入即可使用,无需手动编写任何配置代码。
其核心本质是:一套基于条件注解的、可动态加载的JavaConfig配置类,由Spring Boot在项目启动时自动扫描、条件匹配后,批量注册到Spring IOC容器中。
二、自动配置的核心前置知识
想要彻底搞懂自动配置原理,必须先掌握以下5个核心前置知识点,否则无法理解源码的执行逻辑。
- JavaConfig配置类:@Configuration注解标注的类,用于替代传统XML配置,通过@Bean注解标注的方法向容器注册Bean。Spring Boot 3.x推荐使用@AutoConfiguration注解,该注解继承自@Configuration,默认设置proxyBeanMethods = false,关闭CGLIB代理,大幅提升配置类的解析性能。
- @Import注解:Spring框架的核心注解,用于向容器中导入指定类,支持导入普通Java类、配置类、ImportSelector实现类、ImportBeanDefinitionRegistrar实现类。自动配置的核心入口就是@Import(AutoConfigurationImportSelector.class)。
- @Conditional条件注解:Spring 4.0引入的条件控制注解,只有满足指定的匹配条件,才会向容器注册对应的Bean。Spring Boot在此基础上扩展了大量业务常用的条件注解,是自动配置的“动态开关”。
- Spring Boot的SPI加载机制:SPI(Service Provider Interface)是一种服务发现机制,Spring Boot通过SpringFactoriesLoader工具类,加载类路径下META-INF目录中的配置文件,获取需要注册的自动配置类全限定名。 重点注意:Spring Boot 2.7版本引入了新的自动配置注册方式,需在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中编写自动配置类全限定名;Spring Boot 3.x版本开始,完全移除了spring.factories文件对自动配置类的支持,必须使用新的imports文件注册,旧教程中的spring.factories方式在3.x中已完全失效。
- 配置绑定:@ConfigurationProperties注解,用于将application.yml/application.properties配置文件中的属性,批量绑定到Java对象的字段上,配合@EnableConfigurationProperties注解使用,实现配置与业务代码的解耦。
三、自动配置核心源码全拆解
本文所有源码均基于Spring Boot 3.4.2最新稳定版,100%匹配官方源码逻辑,无任何错误解读。
Spring Boot项目的启动入口,是标注了@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 {
Class<?>[] exclude() default {};
String[] excludeName() default {};
String[] scanBasePackages() default {};
Class<?>[] scanBasePackageClasses() default {};
boolean proxyBeanMethods() default true;
}
可以看到,@SpringBootApplication是一个复合注解,核心功能由三个注解实现:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan,我们逐个拆解核心逻辑。
- @SpringBootConfiguration:本质就是一个标准的@Configuration注解,作用是标注启动类是一个Spring配置类,容器启动时会优先解析这个类。源码如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
boolean proxyBeanMethods() default true;
}
- @ComponentScan:Spring框架的包扫描注解,默认扫描启动类所在包及其子包下所有标注了@Component、@Service、@Controller、@Repository等注解的类,将其注册到IOC容器中。注解中的excludeFilters用于排除自动配置类,避免被重复扫描导致解析顺序混乱。
- @EnableAutoConfiguration:自动配置的核心注解,没有这个注解,自动配置完全失效。我们看它的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
这里有两个核心注解,直接决定了自动配置的执行:
- @AutoConfigurationPackage:用于注册自动配置的基础包路径,将启动类所在的包路径注册到容器中,供后续组件使用,比如MyBatisPlus的Mapper扫描,就是从这里获取默认的包路径。
- @Import(AutoConfigurationImportSelector.class):导入了AutoConfigurationImportSelector类,这个类是自动配置的核心入口,所有自动配置类的加载、过滤、排序,全由这个类完成。
3.1 AutoConfigurationImportSelector核心执行逻辑
AutoConfigurationImportSelector实现了DeferredImportSelector接口,而DeferredImportSelector是ImportSelector的子接口,它的核心特性是:所有用户自定义的@Configuration配置类全部解析完成后,才会执行selectImports方法。
这是@ConditionalOnMissingBean注解能生效的核心原因!用户自定义的Bean会先注册到容器中,自动配置类执行时,@ConditionalOnMissingBean就能判断到用户已注册对应Bean,从而不再注册默认Bean,实现用户自定义Bean对自动配置默认Bean的覆盖。
我们先看selectImports核心方法:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
核心逻辑全部封装在getAutoConfigurationEntry方法中,我们拆解这个方法的8个核心执行步骤:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 步骤1:获取@EnableAutoConfiguration注解的exclude和excludeName属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 步骤2:加载所有候选的自动配置类,从META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件读取
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 步骤3:去除重复的自动配置类
configurations = removeDuplicates(configurations);
// 步骤4:获取需要排除的自动配置类,从注解属性和配置文件中获取
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
// 步骤5:移除需要排除的自动配置类
configurations.removeAll(exclusions);
// 步骤6:应用配置过滤器,通过@Conditional条件注解过滤不符合条件的配置类
configurations = getConfigurationClassFilter().filter(configurations);
// 步骤7:触发自动配置导入事件
fireAutoConfigurationImportEvents(configurations, exclusions);
// 步骤8:封装并返回符合条件的自动配置条目
return new AutoConfigurationEntry(configurations, exclusions);
}
我们对核心步骤做深度拆解,确保100%理解底层逻辑:
- 步骤2:加载候选自动配置类getCandidateConfigurations方法负责加载所有候选的自动配置类,Spring Boot 3.x中已完全使用新的加载方式,源码如下:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
.getCandidates();
if (configurations.isEmpty()) {
throw new IllegalArgumentException(
"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;
}
这里明确了Spring Boot 3.x的自动配置类加载路径:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,不再从spring.factories文件中加载自动配置类,旧教程中的相关内容在3.x中已完全失效,务必注意。
- 步骤6:条件过滤自动配置类这里的配置过滤器会解析自动配置类上的@Conditional系列注解,逐一判断条件是否满足,只有全部条件满足的自动配置类,才会被保留下来交给Spring容器解析。
我们以Spring Boot官方的DataSourceAutoConfiguration自动配置类为例,看它的条件注解配置:
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class })
public class DataSourceAutoConfiguration {
// 省略内部配置类
}
这个自动配置类,只有当类路径下存在DataSource和EmbeddedDatabaseType类(即引入了jdbc相关依赖),且容器中没有R2DBC的ConnectionFactory Bean时,才会生效,否则会被过滤掉,不会被容器解析。
当AutoConfigurationImportSelector返回符合条件的自动配置类全限定名后,Spring容器会将这些类作为配置类,解析其中的@Bean方法,将对应的Bean实例注册到IOC容器中,整个自动配置流程完成。
3.2 自动配置完整执行流程图
四、核心条件注解全解析与易混淆点区分
Spring Boot扩展了大量@Conditional的子注解,用于控制自动配置类和Bean的注册条件,这里我们讲解最常用的注解,以及开发中90%的人都会踩的坑点。
4.1 常用条件注解详解
| 注解名称 | 核心作用 | 典型使用场景 |
| @ConditionalOnClass | 类路径下存在指定的类时,配置生效 | 依赖包存在时才注册对应Bean,避免ClassNotFoundException |
| @ConditionalOnMissingClass | 类路径下不存在指定的类时,配置生效 | 排除特定依赖场景下的配置 |
| @ConditionalOnBean | Spring容器中存在指定类型/名称的Bean时,配置生效 | 依赖其他Bean存在时才注册当前Bean |
| @ConditionalOnMissingBean | Spring容器中不存在指定类型/名称的Bean时,配置生效 | 注册默认Bean,允许用户自定义Bean覆盖 |
| @ConditionalOnProperty | 配置文件中存在指定属性且值匹配时,配置生效 | 通过配置文件开关控制配置是否生效 |
| @ConditionalOnWebApplication | 当前应用是Web应用时,配置生效 | Web环境下才注册的Bean,如Controller、拦截器 |
| @ConditionalOnNotWebApplication | 当前应用不是Web应用时,配置生效 | 非Web环境下才注册的Bean |
| @ConditionalOnResource | 类路径下存在指定资源文件时,配置生效 | 依赖配置文件存在时才生效的配置 |
| @ConditionalOnSingleCandidate | 容器中存在且仅存在一个指定类型的Bean时,配置生效 | 自动注入依赖时,确保唯一候选Bean |
4.2 易混淆点与坑点详解
4.2.1 @ConditionalOnBean vs @ConditionalOnMissingBean 核心区别与坑
这两个注解是日常开发中最容易踩坑的,我们做深度拆解:
- 核心区别
- @ConditionalOnBean:容器中已存在指定Bean时,才注册当前Bean,是“存在才生效”。
- @ConditionalOnMissingBean:容器中不存在指定Bean时,才注册当前Bean,是“不存在才生效”,主要用于注册默认Bean,允许用户自定义覆盖。
- 最常见的坑:执行顺序问题前面我们讲过,AutoConfigurationImportSelector实现了DeferredImportSelector,执行时机在所有用户自定义配置类解析完成之后,所以:
- 正确用法:@ConditionalOnMissingBean只能用在自动配置类中,不能用在被@ComponentScan扫描到的用户自定义配置类中。
- 错误用法:如果在用户自定义配置类中使用@ConditionalOnMissingBean,由于用户配置类解析顺序在自动配置类之前,此时容器中还没有注册自动配置的Bean,@ConditionalOnMissingBean会判断为“不存在”,注册用户的Bean,虽然看起来结果正常,但多个配置类存在依赖时,会出现顺序混乱,导致Bean注册失败。
- 第二个坑:类型匹配问题@ConditionalOnMissingBean默认按类型匹配,如果要按名称匹配,必须指定name属性,很多人写错导致注解失效。 正确示例:
// 按类型匹配,容器中没有EncryptService类型的Bean时,才注册
@Bean
@ConditionalOnMissingBean
public EncryptService encryptService(EncryptProperties properties) {
return new EncryptService(properties);
}
// 按名称匹配,容器中名称为encryptService的Bean不存在时,才注册
@Bean
@ConditionalOnMissingBean(name = "encryptService")
public EncryptService encryptService(EncryptProperties properties) {
return new EncryptService(properties);
}
- 第三个坑:同配置类内匹配失效@ConditionalOnBean只能匹配到已被容器完成注册的Bean,如果两个Bean在同一个配置类中,@ConditionalOnBean无法匹配到同一个配置类中前面定义的Bean,因为配置类的解析是先解析所有@Bean方法,再统一注册Bean,所以同一个配置类中,@ConditionalOnBean无法生效,必须拆分到不同的配置类中,通过@AutoConfigureAfter控制解析顺序。
4.2.2 @AutoConfigureOrder vs @Order 核心区别
很多人会用@Order注解控制自动配置类的顺序,这是完全错误的!
- @AutoConfigureOrder/@AutoConfigureBefore/@AutoConfigureAfter:专门用于控制自动配置类的解析顺序,只能用在自动配置类上,Spring Boot会在过滤完自动配置类后,根据这些注解进行排序,确保依赖的自动配置类先解析。
- @Order注解:只能控制Bean的注册顺序,无法控制配置类的解析顺序,用在配置类上完全无效,这是90%的开发者都会踩的坑!
正确用法:如果你的自动配置类依赖DataSourceAutoConfiguration,必须在DataSourceAutoConfiguration解析完成后再解析,写法如下:
@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MyBatisPlusAutoConfiguration {
// 省略配置内容
}
4.2.3 @ConditionalOnProperty的matchIfMissing属性
@ConditionalOnProperty注解有一个非常重要的属性matchIfMissing,默认值为false,含义是:如果配置文件中没有这个属性,条件不成立,配置不生效。
如果我们希望配置文件中没有这个属性时,配置默认生效,就需要设置matchIfMissing = true,这是很多人配置不生效的常见原因。 正确示例:
// 配置文件中存在jam.encrypt.enabled=true时生效,无该配置时默认不生效
@ConditionalOnProperty(prefix = "jam.encrypt", name = "enabled", havingValue = "true")
// 配置文件中存在jam.encrypt.enabled=true时生效,无该配置时默认生效
@ConditionalOnProperty(prefix = "jam.encrypt", name = "enabled", havingValue = "true", matchIfMissing = true)
五、实战:手写自定义Spring Boot Starter,吃透自动配置
理论讲完,我们通过手写一个可直接运行的自定义Starter,巩固自动配置的所有知识点。这个Starter实现了配置化的字符串加密解密功能,完全符合开发规范,可直接编译运行。
5.1 环境与版本说明
- JDK版本:17
- Spring Boot版本:3.4.2
- 项目管理:Maven
5.2 项目结构
我们分为两个模块开发:
- jam-spring-boot-starter:自定义Starter核心模块,实现自动配置逻辑
- jam-spring-boot-starter-test:测试模块,引入Starter,验证功能可用性
5.2.1 自定义Starter模块(jam-spring-boot-starter)
首先编写pom.xml文件,引入核心依赖:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.2</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>jam-spring-boot-starter</artifactId>
<version>1.0.0</version>
<name>jam-spring-boot-starter</name>
<description>自定义Spring Boot Starter 加密工具</description>
<properties>
<java.version>17</java.version>
<lombok.version>1.18.34</lombok.version>
<guava.version>33.2.0-jre</guava.version>
<fastjson2.version>2.0.52</fastjson2.version>
</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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
</dependencies>
</project>
编写配置属性绑定类,用于绑定application.yml中的配置:
package com.jam.demo.starter.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 加密工具配置属性类
* 用于绑定application.yml中jam.encrypt前缀的配置
* @author ken
*/
@ConfigurationProperties(prefix = "jam.encrypt")
public class EncryptProperties {
/**
* 加密密钥,默认值:defaultSecretKey123456
*/
private String secretKey = "defaultSecretKey123456";
/**
* 加密算法,默认值:AES
*/
private String algorithm = "AES";
/**
* 是否开启加密工具,默认开启
*/
private Boolean enabled = true;
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
编写核心服务类,实现加密解密功能:
package com.jam.demo.starter.service;
import com.jam.demo.starter.config.EncryptProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* 加密解密核心服务类
* @author ken
*/
@Slf4j
public class EncryptService {
private final EncryptProperties encryptProperties;
private static final int AES_KEY_LENGTH = 16;
/**
* 构造方法注入配置属性
* @param encryptProperties 加密配置属性
*/
public EncryptService(EncryptProperties encryptProperties) {
this.encryptProperties = encryptProperties;
}
/**
* 字符串加密方法
* @param content 待加密的明文内容
* @return 加密后的Base64编码字符串
* @throws Exception 加密异常
*/
public String encrypt(String content) throws Exception {
if (!StringUtils.hasText(content)) {
log.warn("待加密内容为空,直接返回原内容");
return content;
}
if (ObjectUtils.isEmpty(encryptProperties.getEnabled()) || !encryptProperties.getEnabled()) {
log.info("加密工具未开启,直接返回原内容");
return content;
}
SecretKeySpec keySpec = buildSecretKeySpec();
Cipher cipher = Cipher.getInstance(encryptProperties.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedBytes = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
String result = Base64.getEncoder().encodeToString(encryptedBytes);
log.debug("内容加密成功");
return result;
}
/**
* 字符串解密方法
* @param encryptedContent 待解密的Base64编码字符串
* @return 解密后的明文内容
* @throws Exception 解密异常
*/
public String decrypt(String encryptedContent) throws Exception {
if (!StringUtils.hasText(encryptedContent)) {
log.warn("待解密内容为空,直接返回原内容");
return encryptedContent;
}
if (ObjectUtils.isEmpty(encryptProperties.getEnabled()) || !encryptProperties.getEnabled()) {
log.info("加密工具未开启,直接返回原内容");
return encryptedContent;
}
SecretKeySpec keySpec = buildSecretKeySpec();
Cipher cipher = Cipher.getInstance(encryptProperties.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedContent));
String result = new String(decryptedBytes, StandardCharsets.UTF_8);
log.debug("内容解密成功");
return result;
}
/**
* 构建加密密钥规范
* @return SecretKeySpec 密钥规范
*/
private SecretKeySpec buildSecretKeySpec() {
String secretKey = encryptProperties.getSecretKey();
if (!StringUtils.hasText(secretKey)) {
throw new IllegalArgumentException("加密密钥不能为空");
}
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
byte[] validKeyBytes = new byte[AES_KEY_LENGTH];
System.arraycopy(keyBytes, 0, validKeyBytes, 0, Math.min(keyBytes.length, AES_KEY_LENGTH));
return new SecretKeySpec(validKeyBytes, encryptProperties.getAlgorithm());
}
}
编写核心自动配置类,这是Starter的核心:
package com.jam.demo.starter.autoconfigure;
import com.jam.demo.starter.config.EncryptProperties;
import com.jam.demo.starter.service.EncryptService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* 加密工具自动配置类
* @author ken
*/
@AutoConfiguration
@ConditionalOnClass(EncryptService.class)
@EnableConfigurationProperties(EncryptProperties.class)
@ConditionalOnProperty(prefix = "jam.encrypt", name = "enabled", havingValue = "true", matchIfMissing = true)
public class EncryptAutoConfiguration {
/**
* 注册加密服务Bean,当容器中没有EncryptService类型的Bean时生效
* @param encryptProperties 加密配置属性
* @return EncryptService 加密服务实例
*/
@Bean
@ConditionalOnMissingBean
public EncryptService encryptService(EncryptProperties encryptProperties) {
return new EncryptService(encryptProperties);
}
}
最后编写自动配置类的注册文件,Spring Boot 3.x必须使用该文件,否则自动配置不生效: 在resources目录下创建META-INF/spring目录,新建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,内容如下:
com.jam.demo.starter.autoconfigure.EncryptAutoConfiguration
到这里,自定义Starter开发完成,执行mvn clean install命令,将Starter安装到本地Maven仓库,即可在其他项目中引入使用。
5.2.2 测试模块(jam-spring-boot-starter-test)
我们创建Spring Boot Web项目,引入上面的Starter,配合Swagger3和MyBatisPlus,完成功能测试,所有代码可直接运行。
首先编写pom.xml文件:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.2</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>jam-spring-boot-starter-test</artifactId>
<version>1.0.0</version>
<name>jam-spring-boot-starter-test</name>
<description>自定义Starter测试项目</description>
<properties>
<java.version>17</java.version>
<lombok.version>1.18.34</lombok.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<mysql.version>8.4.0</mysql.version>
<springdoc.version>2.6.0</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>com.jam.demo</groupId>
<artifactId>jam-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
编写application.yml配置文件,配置加密工具、数据源、Swagger等:
server:
port: 8080
jam:
encrypt:
secret-key: jamDemo123456789
algorithm: AES
enabled: true
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jam_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.jam.demo.test.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
swagger-ui:
path: /swagger-ui.html
enabled: true
api-docs:
enabled: true
path: /v3/api-docs
编写MySQL 8.0可直接执行的表结构SQL:
CREATE DATABASE IF NOT EXISTS jam_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jam_demo;
DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
password VARCHAR(256) NOT NULL COMMENT '密码',
real_name VARCHAR(32) COMMENT '真实姓名',
phone VARCHAR(11) COMMENT '手机号',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户表';
INSERT INTO sys_user (username, password, real_name, phone) VALUES
('test01', '123456', '测试用户01', '13800138000'),
('test02', '123456', '测试用户02', '13800138001');
编写实体类,带MyBatisPlus注解:
package com.jam.demo.test.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统用户实体类
* @author ken
*/
@Data
@TableName("sys_user")
@Schema(description = "系统用户实体")
public class SysUser {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "用户名", example = "test01")
private String username;
@Schema(description = "密码", example = "123456")
private String password;
@Schema(description = "真实姓名", example = "测试用户01")
private String realName;
@Schema(description = "手机号", example = "13800138000")
private String phone;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
编写Mapper接口,继承MyBatisPlus的BaseMapper:
package com.jam.demo.test.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.test.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
/**
* 系统用户Mapper接口
* @author ken
*/
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}
编写Service接口和实现类:
package com.jam.demo.test.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.test.entity.SysUser;
/**
* 系统用户服务接口
* @author ken
*/
public interface SysUserService extends IService<SysUser> {
}
package com.jam.demo.test.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.test.entity.SysUser;
import com.jam.demo.test.mapper.SysUserMapper;
import com.jam.demo.test.service.SysUserService;
import org.springframework.stereotype.Service;
/**
* 系统用户服务实现类
* @author ken
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
}
编写Controller,注入自定义的EncryptService,带Swagger3注解:
package com.jam.demo.test.controller;
import com.jam.demo.starter.service.EncryptService;
import com.jam.demo.test.entity.SysUser;
import com.jam.demo.test.service.SysUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 加密测试Controller
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/encrypt")
@RequiredArgsConstructor
@Tag(name = "加密工具测试接口", description = "自定义Starter加密解密功能测试")
public class EncryptTestController {
private final EncryptService encryptService;
private final SysUserService sysUserService;
/**
* 字符串加密接口
* @param content 待加密的明文
* @return 加密后的密文
* @throws Exception 加密异常
*/
@GetMapping("/encrypt")
@Operation(summary = "字符串加密", description = "对输入的明文字符串进行加密")
public ResponseEntity<String> encrypt(
@Parameter(description = "待加密的明文", required = true, example = "123456")
@RequestParam String content) throws Exception {
String encryptedContent = encryptService.encrypt(content);
log.info("加密完成,明文:{},密文:{}", content, encryptedContent);
return ResponseEntity.ok(encryptedContent);
}
/**
* 字符串解密接口
* @param encryptedContent 待解密的密文
* @return 解密后的明文
* @throws Exception 解密异常
*/
@GetMapping("/decrypt")
@Operation(summary = "字符串解密", description = "对输入的密文字符串进行解密")
public ResponseEntity<String> decrypt(
@Parameter(description = "待解密的密文", required = true)
@RequestParam String encryptedContent) throws Exception {
String content = encryptService.decrypt(encryptedContent);
log.info("解密完成,密文:{},明文:{}", encryptedContent, content);
return ResponseEntity.ok(content);
}
/**
* 查询所有用户,密码加密返回
* @return 用户列表
*/
@GetMapping("/user/list")
@Operation(summary = "查询用户列表", description = "查询所有用户,密码字段加密返回")
public ResponseEntity<List<SysUser>> getUserList() throws Exception {
List<SysUser> userList = sysUserService.list();
for (SysUser user : userList) {
user.setPassword(encryptService.encrypt(user.getPassword()));
}
return ResponseEntity.ok(userList);
}
}
编写项目启动类:
package com.jam.demo.test;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.test.mapper")
public class JamStarterTestApplication {
public static void main(String[] args) {
SpringApplication.run(JamStarterTestApplication.class, args);
}
}
5.3 项目运行与测试
- 先执行mvn clean install将jam-spring-boot-starter安装到本地Maven仓库。
- 在MySQL 8.0中执行上面的SQL脚本,创建数据库和表。
- 修改application.yml中的数据库用户名和密码为本地配置。
- 启动JamStarterTestApplication启动类,项目启动成功后,访问http://localhost:8080/swagger-ui.html,打开Swagger界面即可测试所有接口,验证加密解密功能正常。
- 在application.yml中设置jam.encrypt.enabled=false,重启项目,可验证加密功能关闭,接口直接返回原内容,@ConditionalOnProperty注解生效。
- 在项目中自定义一个EncryptService的Bean,重启项目,可验证Spring Boot使用自定义Bean,而非自动配置的默认Bean,@ConditionalOnMissingBean注解生效。
六、自动配置常见问题排查与最佳实践
6.1 自动配置不生效的排查步骤
- 开启自动配置报告:在application.yml中设置debug=true,启动项目,控制台会打印自动配置报告,列出所有自动配置类的生效状态和不生效的原因,这是最直接的排查方式。
- 检查自动配置类的注册文件:Spring Boot 3.x必须在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中正确编写自动配置类的全限定名,检查包名是否正确,有无拼写错误。
- 检查@Conditional条件是否满足:根据自动配置报告的原因,检查条件注解的条件是否满足,比如类路径下是否有对应类、配置文件中的属性是否正确、容器中是否有对应Bean。
- 检查是否被@ComponentScan重复扫描:如果自动配置类被启动类的@ComponentScan扫描到,会变成用户自定义配置类,解析顺序提前,导致@ConditionalOnMissingBean失效,自动配置类的包名不要和启动类包名一致,避免被扫描。
- 检查自动配置类的顺序:如果自动配置类依赖其他自动配置类,必须用@AutoConfigureAfter注解指定依赖的配置类,确保解析顺序正确。
- 检查配置绑定是否正确:@ConfigurationProperties注解的prefix是否正确,字段的getter/setter方法是否齐全,有无拼写错误。
6.2 自动配置最佳实践
- 自动配置类使用@AutoConfiguration注解,不要使用@Configuration注解,@AutoConfiguration默认设置proxyBeanMethods = false,关闭CGLIB代理,提升性能,是Spring Boot 3.x推荐的写法。
- 自动配置类不要被@ComponentScan扫描到,放在单独的包中,只通过imports文件注册,避免解析顺序混乱。
- 优先使用@ConditionalOnClass注解做前置判断,确保类路径下存在对应类,避免ClassNotFoundException。
- @ConditionalOnMissingBean只用于注册默认Bean,允许用户自定义Bean覆盖,不要用于强制Bean的唯一性。
- 自动配置类的顺序使用@AutoConfigureBefore/@AutoConfigureAfter注解控制,不要使用@Order注解,@Order无法控制配置类的解析顺序。
- 配置绑定使用@ConfigurationProperties注解,配合spring-boot-configuration-processor生成配置元数据,让IDE有代码提示,提升开发体验。
- 自动配置类要保持职责单一,一个自动配置类只负责一个组件的配置,不要把多个组件的配置放在同一个类中。
- 提供自动配置的排除方式,通过@EnableAutoConfiguration的exclude属性和spring.autoconfigure.exclude配置,允许用户关闭不需要的自动配置。
七、总结
Spring Boot自动配置的核心本质,是基于Spring的JavaConfig配置类、@Import注解、@Conditional条件注解,配合SPI服务发现机制,实现了配置的自动化加载和注册。它的核心逻辑并不复杂,只要搞懂了它的执行流程、核心注解的作用、以及常见的坑点,就能轻松驾驭Spring Boot的自动配置,不仅能解决日常开发中的问题,还能根据业务需求开发自定义Starter,大幅提升开发效率。