彻底搞懂 Spring Boot 自动配置原理:从源码拆解到手写 Starter,零废话全干货

简介: 本文深入解析SpringBoot自动配置原理,基于SpringBoot 3.4.2版本详细拆解了自动配置的执行流程。主要内容包括:1)自动配置的本质是基于条件注解的动态JavaConfig配置类;2)核心执行流程通过AutoConfigurationImportSelector实现;3)SpringBoot 3.x采用新的自动配置注册方式;4)重点讲解了@Conditional系列条件注解的使用场景与常见坑点;5)通过开发自定义加密Starter实战演示完整实现过程。

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个核心前置知识点,否则无法理解源码的执行逻辑。

  1. JavaConfig配置类:@Configuration注解标注的类,用于替代传统XML配置,通过@Bean注解标注的方法向容器注册Bean。Spring Boot 3.x推荐使用@AutoConfiguration注解,该注解继承自@Configuration,默认设置proxyBeanMethods = false,关闭CGLIB代理,大幅提升配置类的解析性能。
  2. @Import注解:Spring框架的核心注解,用于向容器中导入指定类,支持导入普通Java类、配置类、ImportSelector实现类、ImportBeanDefinitionRegistrar实现类。自动配置的核心入口就是@Import(AutoConfigurationImportSelector.class)。
  3. @Conditional条件注解:Spring 4.0引入的条件控制注解,只有满足指定的匹配条件,才会向容器注册对应的Bean。Spring Boot在此基础上扩展了大量业务常用的条件注解,是自动配置的“动态开关”。
  4. 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中已完全失效。
  5. 配置绑定:@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,我们逐个拆解核心逻辑。

  1. @SpringBootConfiguration:本质就是一个标准的@Configuration注解,作用是标注启动类是一个Spring配置类,容器启动时会优先解析这个类。源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
   boolean proxyBeanMethods() default true;
}

  1. @ComponentScan:Spring框架的包扫描注解,默认扫描启动类所在包及其子包下所有标注了@Component、@Service、@Controller、@Repository等注解的类,将其注册到IOC容器中。注解中的excludeFilters用于排除自动配置类,避免被重复扫描导致解析顺序混乱。
  2. @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%理解底层逻辑:

  1. 步骤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中已完全失效,务必注意。

  1. 步骤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 核心区别与坑

这两个注解是日常开发中最容易踩坑的,我们做深度拆解:

  1. 核心区别
  • @ConditionalOnBean:容器中已存在指定Bean时,才注册当前Bean,是“存在才生效”。
  • @ConditionalOnMissingBean:容器中不存在指定Bean时,才注册当前Bean,是“不存在才生效”,主要用于注册默认Bean,允许用户自定义覆盖。
  1. 最常见的坑:执行顺序问题前面我们讲过,AutoConfigurationImportSelector实现了DeferredImportSelector,执行时机在所有用户自定义配置类解析完成之后,所以:
  • 正确用法:@ConditionalOnMissingBean只能用在自动配置类中,不能用在被@ComponentScan扫描到的用户自定义配置类中。
  • 错误用法:如果在用户自定义配置类中使用@ConditionalOnMissingBean,由于用户配置类解析顺序在自动配置类之前,此时容器中还没有注册自动配置的Bean,@ConditionalOnMissingBean会判断为“不存在”,注册用户的Bean,虽然看起来结果正常,但多个配置类存在依赖时,会出现顺序混乱,导致Bean注册失败。
  1. 第二个坑:类型匹配问题@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);
}

  1. 第三个坑:同配置类内匹配失效@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 项目结构

我们分为两个模块开发:

  1. jam-spring-boot-starter:自定义Starter核心模块,实现自动配置逻辑
  2. 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 项目运行与测试

  1. 先执行mvn clean install将jam-spring-boot-starter安装到本地Maven仓库。
  2. 在MySQL 8.0中执行上面的SQL脚本,创建数据库和表。
  3. 修改application.yml中的数据库用户名和密码为本地配置。
  4. 启动JamStarterTestApplication启动类,项目启动成功后,访问http://localhost:8080/swagger-ui.html,打开Swagger界面即可测试所有接口,验证加密解密功能正常。
  5. 在application.yml中设置jam.encrypt.enabled=false,重启项目,可验证加密功能关闭,接口直接返回原内容,@ConditionalOnProperty注解生效。
  6. 在项目中自定义一个EncryptService的Bean,重启项目,可验证Spring Boot使用自定义Bean,而非自动配置的默认Bean,@ConditionalOnMissingBean注解生效。

六、自动配置常见问题排查与最佳实践

6.1 自动配置不生效的排查步骤

  1. 开启自动配置报告:在application.yml中设置debug=true,启动项目,控制台会打印自动配置报告,列出所有自动配置类的生效状态和不生效的原因,这是最直接的排查方式。
  2. 检查自动配置类的注册文件:Spring Boot 3.x必须在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中正确编写自动配置类的全限定名,检查包名是否正确,有无拼写错误。
  3. 检查@Conditional条件是否满足:根据自动配置报告的原因,检查条件注解的条件是否满足,比如类路径下是否有对应类、配置文件中的属性是否正确、容器中是否有对应Bean。
  4. 检查是否被@ComponentScan重复扫描:如果自动配置类被启动类的@ComponentScan扫描到,会变成用户自定义配置类,解析顺序提前,导致@ConditionalOnMissingBean失效,自动配置类的包名不要和启动类包名一致,避免被扫描。
  5. 检查自动配置类的顺序:如果自动配置类依赖其他自动配置类,必须用@AutoConfigureAfter注解指定依赖的配置类,确保解析顺序正确。
  6. 检查配置绑定是否正确:@ConfigurationProperties注解的prefix是否正确,字段的getter/setter方法是否齐全,有无拼写错误。

6.2 自动配置最佳实践

  1. 自动配置类使用@AutoConfiguration注解,不要使用@Configuration注解,@AutoConfiguration默认设置proxyBeanMethods = false,关闭CGLIB代理,提升性能,是Spring Boot 3.x推荐的写法。
  2. 自动配置类不要被@ComponentScan扫描到,放在单独的包中,只通过imports文件注册,避免解析顺序混乱。
  3. 优先使用@ConditionalOnClass注解做前置判断,确保类路径下存在对应类,避免ClassNotFoundException。
  4. @ConditionalOnMissingBean只用于注册默认Bean,允许用户自定义Bean覆盖,不要用于强制Bean的唯一性。
  5. 自动配置类的顺序使用@AutoConfigureBefore/@AutoConfigureAfter注解控制,不要使用@Order注解,@Order无法控制配置类的解析顺序。
  6. 配置绑定使用@ConfigurationProperties注解,配合spring-boot-configuration-processor生成配置元数据,让IDE有代码提示,提升开发体验。
  7. 自动配置类要保持职责单一,一个自动配置类只负责一个组件的配置,不要把多个组件的配置放在同一个类中。
  8. 提供自动配置的排除方式,通过@EnableAutoConfiguration的exclude属性和spring.autoconfigure.exclude配置,允许用户关闭不需要的自动配置。

七、总结

Spring Boot自动配置的核心本质,是基于Spring的JavaConfig配置类、@Import注解、@Conditional条件注解,配合SPI服务发现机制,实现了配置的自动化加载和注册。它的核心逻辑并不复杂,只要搞懂了它的执行流程、核心注解的作用、以及常见的坑点,就能轻松驾驭Spring Boot的自动配置,不仅能解决日常开发中的问题,还能根据业务需求开发自定义Starter,大幅提升开发效率。

目录
相关文章
|
1月前
|
缓存 Java 开发者
吃透 Spring Bean 生命周期:从源码底层到实战落地
本文深度解析Spring 6.2.3 Bean生命周期,涵盖BeanDefinition注册、实例化、属性填充、Aware回调、BeanPostProcessor前后置处理、初始化(@PostConstruct/InitializingBean/init-method)、AOP代理、单例缓存及销毁全流程,结合源码、实战示例与生产问题排查,助你彻底掌握IoC核心机制。
455 3
|
存储 Java
每日一道面试题之谈一谈HashMap和HashSet的区别
每日一道面试题之谈一谈HashMap和HashSet的区别
261 0
|
1月前
|
Java 调度 开发者
Java AQS:JUC 并发体系的底层同步框架基石
AQS(AbstractQueuedSynchronizer)是Java并发包(JUC)的底层核心,以volatile state + CLH双向队列统一实现同步控制。支持独占(如ReentrantLock)与共享(如Semaphore、CountDownLatch)两种模式,通过模板方法封装排队、阻塞/唤醒等通用逻辑,是理解与定制高性能同步组件的关键基石。(239字)
281 7
|
2月前
|
存储 关系型数据库 MySQL
从二叉树到B+树:深入解析MySQL索引的底层数据结构原理
本文深入剖析数据库索引底层数据结构演进:从易退化的二叉搜索树,到为磁盘优化的B树,最终聚焦现代数据库(如MySQL InnoDB)广泛采用的B+树——其高扇出、叶节点链表连接等特性,显著降低I/O次数并提升范围查询效率。
261 5
|
17天前
|
SQL 关系型数据库 Java
吃透 Seata 分布式事务:原理拆解 + 生产级落地 + 全场景避坑实战
本文深度解析阿里开源分布式事务框架Seata:剖析TC/TM/RM三大角色与全局事务流程,详解AT(零侵入)、TCC(强控制)、SAGA(长事务)、XA(强一致)四大模式原理、适用场景及核心对比,并通过电商下单实战演示AT模式落地,最后系统梳理生产环境高可用、SQL限制、幂等处理、XID传播等全链路避坑指南。
292 4
|
5月前
|
XML Java 开发者
springboot自动装配的基本原理
Spring Boot自动装配基于“约定大于配置”理念,通过@SpringBootApplication、@EnableAutoConfiguration与spring.factories机制,结合条件注解实现智能Bean加载。它根据依赖自动配置组件,大幅简化开发。其核心是AutoConfigurationImportSelector筛选符合条件的配置类,实现按需装配。开发者可专注业务,享受“开箱即用”的便捷体验。(238字)
|
7月前
|
人工智能 Java 开发者
【Spring】原理解析:Spring Boot 自动配置
Spring Boot通过“约定优于配置”的设计理念,自动检测项目依赖并根据这些依赖自动装配相应的Bean,从而解放开发者从繁琐的配置工作中解脱出来,专注于业务逻辑实现。
2437 0
|
1月前
|
存储 缓存 NoSQL
Redis 生产级实战
Redis作为互联网业务的核心内存数据库,其生产环境的稳定性、性能与可扩展性直接决定了业务的可用性上限。多数开发者仅掌握基础的缓存读写操作,一旦面对集群搭建、数据备份、性能瓶颈排查、在线数据迁移等生产级场景,极易出现踩坑、故障甚至数据丢失问题。Redis作为互联网业务的核心基础设施,其生产环境的稳定性与性能直接决定了业务的上限。本文从集群搭建、冷热备份、性能调优、数据迁移四大核心生产场景出发,讲透了底层实现逻辑,提供了全量可落地、零错误的实战方案。
182 4
|
4月前
|
监控 Java 测试技术
OOM排查之路:一次曲折的线上故障复盘
本文记录了一次Paimon数据湖与RocksDB集成服务线上频繁OOM的排查历程。通过分析线程暴增、堆外内存泄漏,最终定位到SDK中RocksDB的JNI内存未释放问题,并借助Flink重构写入链路彻底解决。分享了MAT、NMT、async-profiler等工具的实战经验与排查思路,为类似技术栈提供借鉴。
OOM排查之路:一次曲折的线上故障复盘
|
5月前
|
SQL 关系型数据库 MySQL
MySQL慢sql的排查与优化
本文详解MySQL慢查询排查与优化,涵盖EXPLAIN执行计划分析、索引失效场景及10大优化方案,如避免全表扫描、合理使用索引、分页与排序优化等,助力提升数据库性能。
MySQL慢sql的排查与优化