一、Configuration注解第一阶段XML 配置时代在 Spring 早期所有 Bean 都定义在 XML 里beans bean iduserService classcom.example.UserService property nameuserDao refuserDao/ /bean bean iduserDao classcom.example.UserDao property namedataSource refdataSource/ /bean bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property nameurl valuejdbc:mysql://localhost:3306/db/ /bean /beans弊端字符串写类名编译期不检查类改名了 XML 里不会报错XML 文件越来越长配置和代码分离阅读困难无法利用 IDE 的跳转、重构所以 Spring 开始支持注解配置。第二阶段尝试用普通类 Bean问题暴露Spring 引入了Bean注解允许你在 Java 类里定义 Bean。一开始大家这样写Component public class AppConfig { Bean public UserService userService() { return new UserService(userDao()); // 调用下面的方法 } Bean public UserDao userDao() { return new UserDao(dataSource()); // 调用下面的方法 } Bean public DataSource dataSource() { return new DruidDataSource(); } }看起来没问题但运行起来就出大事了。问题单例被破坏了Spring 的默认行为是每个 Bean 在容器中只存在一份单例。但在上面的代码里userService()方法内部调用了userDao()而userDao()内部又调用了dataSource()。当 Spring 容器启动时调用userService()→ 内部执行userDao()→new 了一个 UserDao容器发现还有个Bean public UserDao userDao()→ 又调用一次 →又 new 了一个 UserDao结果UserDao 被创建了两次而且userService里持有的userDao实例和 Spring 容器里管理的userDao实例不是同一个对象。这违反了 Spring 最核心的单例容器设计。第三阶段Configuration 的诞生为了解决这个问题Spring 3.0 引入了Configuration。Configuration public class AppConfig { Bean public UserService userService() { return new UserService(userDao()); } Bean public UserDao userDao() { return new UserDao(dataSource()); } Bean public DataSource dataSource() { return new DruidDataSource(); } }代码看起来和Component版本一模一样但行为完全不同。核心原理CGLIB 代理加了Configuration后Spring 会在运行时用 CGLIB 生成这个类的子类代理。代理后的逻辑大致是这样的伪代码// CGLIB 生成的代理类伪代码 public class AppConfig$$SpringCGLIB extends AppConfig { Override public UserService userService() { // 先去容器里找如果已经有了就直接返回不再执行原方法 if (容器里已有 userService Bean) { return 容器.get(userService); } return super.userService(); } Override public UserDao userDao() { if (容器里已有 userDao Bean) { return 容器.get(userDao); } return super.userDao(); } }关键点Bean方法之间的互相调用会被代理拦截拦截后从 Spring 容器中获取已创建的实例而不是重新new这样userDao()无论被调用多少次返回的都是同一个单例第四阶段Configuration vs Component 的本质区别ConfigurationComponent是否生成代理✅ 是CGLIB 代理❌ 否Bean 方法互相调用走容器返回单例直接方法调用每次 new 新对象模式Full 模式完整模式Lite 模式轻量模式适用场景配置类里有 Bean 方法互相依赖简单的 Bean 注册方法间无依赖验证代码你可以写个测试自己验证Configuration public class FullConfig { Bean public UserDao userDao() { System.out.println(userDao() 被调用了); return new UserDao(); } Bean public UserService userService() { return new UserService(userDao()); // 这里调用 userDao() } }启动容器后控制台只会打印一次userDao() 被调用了。但如果你把Configuration换成Component会打印两次。现在idea会自动检查报错第五阶段Spring 5.2 的优化 —— proxyBeanMethods从 Spring 5.2 开始Configuration增加了一个属性Configuration(proxyBeanMethods false) // 关闭代理 public class AppConfig { // ... }为什么要关闭代理CGLIB 代理虽然解决了单例问题但有性能开销生成代理类需要额外的类加载每次调用Bean方法都要经过代理拦截如果你的配置类里各个Bean方法之间没有互相调用那代理就是多余的。所以 Spring 5.2 让你可以选择属性值模式行为proxyBeanMethods true默认Full 模式生成 CGLIB 代理Bean 方法互相调用走容器proxyBeanMethods falseLite 模式不生成代理性能更好但方法间调用会重复创建判断标准如果你的Bean方法 A 内部调用了Bean方法 B → 必须用true默认如果各Bean方法独立互不调用 → 可以设false提升启动速度总结Configuration 的设计逻辑XML 配置痛苦 ↓ 引入 Bean 注解尝试用 Java 类配置 ↓ 发现普通类里 Bean 方法互相调用会破坏单例 ↓ 用 CGLIB 代理拦截方法调用从容器取实例 ↓ 代理有性能开销所以提供 proxyBeanMethods 开关一句话记住Configuration的作用它告诉 Spring这个类不是普通的组件而是一个配置类。类里的Bean方法可能会被互相调用请用代理确保它们返回的是容器里的单例而不是每次都 new 新对象。二、Import讲解第一阶段配置类膨胀的问题当你理解了Configuration之后项目里很快就会出现多个配置类// 数据库配置 Configuration public class DataSourceConfig { /* ... */ } // Redis 配置 Configuration public class RedisConfig { /* ... */ } // Web 配置 Configuration public class WebConfig { /* ... */ } // 安全认证配置 Configuration public class SecurityConfig { /* ... */ }问题Spring 怎么知道要加载这些配置类方案一组件扫描ComponentScanSpringBootApplication ComponentScan(basePackages com.example.config) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }弊端ComponentScan是扫整个包它会把com.example.config包下所有带Component、Service、Repository、Configuration的类都加载进来你无法精确控制只加载哪几个配置类如果包结构不合理可能会误扫到测试类、临时类导致容器启动失败更重要的是配置类之间可能有加载顺序要求组件扫描不保证顺序方案二在启动类上写多个 Configuration不行。一个类上不能重复贴Configuration。方案三XML 时代的importSpring 早期 XML 配置里有import resourcexxx.xml/但咱们现在用 Java 配置需要 Java 版的import。第二阶段Import 的诞生 —— 精确导入配置类Spring 3.0 引入了Import目的就是让你显式地、精确地告诉 Spring 要加载哪些配置类。Configuration Import({DataSourceConfig.class, RedisConfig.class, WebConfig.class}) public class AppConfig { // 这个类本身也可以有 Bean 方法 }和 ComponentScan 的核心区别ComponentScanImport方式扫描包路径批量抓取显式指定类点名要谁精确度低可能扫到不需要的类高只加载你指定的类可控性依赖包结构不依赖包结构类在哪都行顺序不保证按数组顺序加载Import 的第一个设计意图当你想把多个配置类组装成一个整体但又不想用大包大揽的组件扫描时用Import精确导入。第三阶段Import 导入普通类不是 ConfigurationImport还有一个用法导入一个普通类没有Component、Configuration等注解。// 一个普通类没有任何 Spring 注解 public class PaymentService { public void pay() { System.out.println(支付成功); } } // 配置类 Configuration Import(PaymentService.class) public class AppConfig { }启动后PaymentService会自动注册到 Spring 容器里Bean 的名字是全限定类名com.example.PaymentService。为什么要这样设计假设你引入了一个第三方 jar 包里面有个类// 第三方 jar 里的类你不能改它的源码它也没有 Component public class ThirdPartyTool { public void doSomething() { } }你没法给它加Component也没法把它放到你的组件扫描路径下。没有 Import 时的痛苦你要么写个Bean方法手动创建它要么用 XML 配置有了 Import直接Import(ThirdPartyTool.class)它就进容器了不需要修改第三方源码不需要写额外的Bean方法第四阶段ImportSelector —— 程序化决定导入什么前面讲的Import(Xxx.class)都是静态的——你在写代码时就确定了要导入哪些类。但真实项目中经常有这种需求我想根据当前环境、或者根据某个条件动态决定要导入哪些配置类。没有 ImportSelector 时的痛苦假设你做了一个框架想让用户按需开启功能用户引入了mybatis-spring-boot-starter就自动加载 MyBatis 配置用户没引入就不加载如果没有动态导入机制你得让用户手动写Configuration Import({MyBatisConfig.class, MapperScanConfig.class, TransactionConfig.class}) public class UserConfig { }弊端用户怎么知道要导入哪些类如果框架升级新增了一个配置类用户得手动改代码无法根据用户是否引入了某个依赖来做判断ImportSelector 的诞生Spring 允许你Import一个实现了ImportSelector接口的类public class MyImportSelector implements ImportSelector { Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 这里可以写任何逻辑返回要导入的类的全限定名数组 // 比如检查 classpath 里有没有某个类 boolean hasMyBatis ClassUtils.isPresent(org.apache.ibatis.session.SqlSessionFactory, getClass().getClassLoader()); if (hasMyBatis) { return new String[]{ com.example.MyBatisConfig, com.example.MapperScanConfig }; } // 没有 MyBatis 依赖返回空数组什么都不导入 return new String[0]; } }使用方式Configuration Import(MyImportSelector.class) public class AppConfig { }核心逻辑Spring 启动时发现Import(MyImportSelector.class)调用MyImportSelector.selectImports()方法根据方法返回的字符串数组动态决定要加载哪些配置类这就是Spring Boot 自动配置的底层核心机制第五阶段Spring Boot 自动配置的真实案例Spring Boot 的EnableAutoConfiguration本质上就是靠ImportImportSelector实现的。简化版源码逻辑Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Inherited AutoConfigurationPackage Import(AutoConfigurationImportSelector.class) // 关键在这里 public interface EnableAutoConfiguration { }AutoConfigurationImportSelector的伪代码public class AutoConfigurationImportSelector implements ImportSelector { Override public String[] selectImports(AnnotationMetadata metadata) { // 1. 去 META-INF/spring.factories 文件里读取所有自动配置类的全限定名 ListString configurations SpringFactoriesLoader.loadFactoryNames( EnableAutoConfiguration.class, getClass().getClassLoader() ); // 2. 用 Conditional 注解过滤掉不符合条件的 // 比如 ConditionalOnClass 检查 classpath 里有没有某个类 // 比如 ConditionalOnProperty 检查配置文件中有没有某个属性 // 3. 返回最终要导入的类名数组 return configurations.toArray(new String[0]); } }逻辑链条你的启动类贴 SpringBootApplication ↓ 里面包含了 EnableAutoConfiguration ↓ EnableAutoConfiguration 上贴了 Import(AutoConfigurationImportSelector.class) ↓ Spring 调用 AutoConfigurationImportSelector.selectImports() ↓ 它读取 META-INF/spring.factories拿到所有候选配置类 ↓ 根据 Conditional 条件过滤 ↓ 把符合条件的配置类导入 Spring 容器这就是为什么你引入一个starter依赖功能就自动生效了。第六阶段ImportBeanDefinitionRegistrar —— 最底层的注册方式Import还支持第三种方式导入一个实现了ImportBeanDefinitionRegistrar接口的类。为什么需要它ImportSelector返回的是类名字符串数组Spring 后续会按标准流程实例化这些类。但有时候你需要更精细地控制 Bean 的注册过程动态修改 Bean 的定义比如修改作用域、添加代理根据某些条件注册多个 Bean注册一些不是普通类的 Bean比如 MyBatis 的 Mapper 接口代理MyBatis-Spring 的经典案例MyBatis 的MapperScan底层就是ImportBeanDefinitionRegistrarRetention(RetentionPolicy.RUNTIME) Target(ElementType.TYPE) Import(MapperScannerRegistrar.class) // 关键 public interface MapperScan { String[] basePackages(); }MapperScannerRegistrar的简化逻辑public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar { Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 1. 获取 MapperScan 注解上的 basePackages 属性 MapString, Object attrs importingClassMetadata .getAnnotationAttributes(MapperScan.class.getName()); String[] packages (String[]) attrs.get(basePackages); // 2. 扫描这些包下的所有接口 ClassPathMapperScanner scanner new ClassPathMapperScanner(registry); scanner.scan(packages); // 3. 对每个 Mapper 接口注册一个 FactoryBean // 这个 FactoryBean 负责在运行时生成 Mapper 接口的代理对象 } }关键点它不是返回类名字符串而是直接操作 BeanDefinitionRegistry它可以注册MapperFactoryBean这种特殊的 Bean用来生成接口代理这是MapperScan能把接口变成 Bean的底层原理总结Import 的三种用法和设计逻辑用法导入目标适用场景代表案例直接导入类Configuration或普通类精确组装配置类或导入第三方无注解类手动导入配置类ImportSelector返回类名字符串数组根据条件动态决定导入哪些配置Spring Boot 自动配置ImportBeanDefinitionRegistrar直接操作 BeanDefinitionRegistry需要精细控制Bean 注册过程MyBatis 的MapperScan演进逻辑配置类多了需要组装 ↓ ComponentScan 太粗放Import 提供精确导入 ↓ 静态导入不够灵活ImportSelector 支持动态决定 ↓ 动态决定还不够精细ImportBeanDefinitionRegistrar 支持直接操作注册表三、Conditional 注解第一阶段没有条件控制时的痛苦当你用Configuration和Import把项目拆成多个配置类后很快会遇到一个问题有些配置类不是在所有场景下都需要。比如你的项目里有一个 Redis 配置类Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate() { return new RedisTemplate(); } }假设某个部署环境不需要 Redis比如只是跑一个纯后台任务不缓存数据但这个类依然会被 Spring 扫描到并加载。后果是什么如果 classpath 上没有 Redis 的 jar 包RedisTemplate这个类不存在Spring 在解析这个配置类时就会抛出ClassNotFoundException项目直接启动失败即使 classpath 上有 jar 包Redis 服务没启动创建RedisTemplateBean 时连接失败还是启动失败即使能启动也白白创建了一个用不到的 Bean浪费资源核心矛盾配置类一旦写在代码里、一旦被扫描到就一定会被加载你无法让它看情况决定是否加载。第二阶段Profile 的尝试与局限Spring 3.1 引入了Profile试图解决不同环境加载不同配置的问题Configuration Profile(dev) public class DevDataSourceConfig { Bean public DataSource dataSource() { // 开发环境用 H2 内存数据库 return new EmbeddedDatabaseBuilder().build(); } } Configuration Profile(prod) public class ProdDataSourceConfig { Bean public DataSource dataSource() { // 生产环境用 MySQL return DataSourceBuilder.create().url(jdbc:mysql://...).build(); } }你需要在启动时指定环境propertiesspring.profiles.activedev这解决了什么问题按开发/测试/生产环境区分配置确实有用但它解决不了什么问题粒度太粗只能按环境区分不能按是否有某个类、是否有某个配置项区分无法表达这种需求只有当 classpath 里有 MySQL 驱动时才加载这个配置类需要手动设置spring.profiles.active不够自动所以 Spring 需要一种更通用的、可编程的条件判断机制。第三阶段Conditional 的诞生Spring 4.0 引入了Conditional。它的设计思想很简单给配置类或Bean方法贴一个条件注解Spring 在加载它之前先判断条件是否满足。不满足直接跳过就当这个类/方法不存在。基本用法Conditional要求你提供一个实现了Condition接口的类// 1. 定义条件判断逻辑 public class MySQLDriverCondition implements Condition { Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { try { // 检查 classpath 里有没有 MySQL 驱动类 Class.forName(com.mysql.cj.jdbc.Driver); return true; // 有条件满足 } catch (ClassNotFoundException e) { return false; // 没有条件不满足 } } } // 2. 用在配置类上 Configuration Conditional(MySQLDriverCondition.class) public class MySQLConfig { Bean public DataSource dataSource() { // 只有 classpath 上有 MySQL 驱动时这个配置类才会生效 return DataSourceBuilder.create().build(); } }关键行为如果MySQLDriverCondition.matches()返回true→MySQLConfig正常加载如果返回false→ Spring直接忽略这个类就像代码里没有它一样这解决了前面的核心矛盾配置类可以看情况决定是否加载了。第四阶段底层原理 —— Spring 什么时候判断条件理解时机很重要。Spring 不是在 Bean创建的时候判断而是在 Bean定义注册的阶段判断。流程简化版Spring 启动 ↓ 扫描/解析配置类 ↓ 遇到 Configuration 类 ↓ 检查类上有没有 Conditional ↓ ├─ 条件不满足 → 直接丢弃这个类不注册 BeanDefinition ↓ └─ 条件满足 → 继续解析类里的 Bean 方法 ↓ 每个 Bean 方法也可以有 Conditional ↓ 条件不满足 → 这个方法不注册 条件满足 → 注册 BeanDefinition为什么要在这个阶段判断因为如果一个配置类被跳过了类里的Bean方法根本不会被解析。这样即使方法里引用了某个不存在的类比如RedisTemplate也不会报错——因为 Spring 根本没去看这些方法。第五阶段Spring Boot 的封装 —— 常用条件注解如果每次都要自己写Condition实现类还是很麻烦。Spring Boot 基于Conditional做了一层封装提供了一系列开箱即用的注解。这些注解的本质它们自己身上贴了Conditional(某个Condition实现类.class)。1. ConditionalOnClass / ConditionalOnMissingClassConfiguration ConditionalOnClass(name com.mysql.cj.jdbc.Driver) public class MySQLAutoConfiguration { // 只有 classpath 上有 MySQL 驱动时才生效 }Bean ConditionalOnMissingBean(DataSource.class) public DataSource dataSource() { // 只有当容器里还没有 DataSource 这个 Bean 时才创建 // 这允许用户自定义的 DataSource 优先 }2. ConditionalOnPropertyConfiguration ConditionalOnProperty( prefix spring.redis, // 前缀 name host, // 属性名 havingValue localhost // 属性值等于 localhost 时才生效 ) public class LocalRedisConfig { }3. ConditionalOnBean / ConditionalOnMissingBeanBean ConditionalOnBean(name redisTemplate) public CacheManager cacheManager(RedisTemplate redisTemplate) { // 只有当容器里已经有 redisTemplate 这个 Bean 时才创建 CacheManager return new RedisCacheManager(redisTemplate); }看看它们的源码长什么样以ConditionalOnClass为例Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Documented Conditional(OnClassCondition.class) // ← 核心在这里 public interface ConditionalOnClass { Class?[] value() default {}; String[] name() default {}; }OnClassCondition是Spring Boot 内置的Condition实现它负责真正去检查classpath上有没有这些类。注意一个细节Spring Boot 检查类是否存在时不会用Class.forName()直接加载类因为加载类可能有副作用。它用的是ASM 字节码解析和ClassLoader 的资源查找只看类在不在不真的加载它。第六阶段条件注解的组合与逻辑关系多个条件注解可以叠加在同一个类或方法上Configuration ConditionalOnClass(DataSource.class) // 条件1有 DataSource 类 ConditionalOnProperty(prefix spring.datasource, name url) // 条件2配了 url public class DataSourceAutoConfiguration { }默认是AND 关系所有条件都满足配置类才生效。一个真实的 Spring Boot 自动配置类看看 Spring Boot 源码里DataSourceAutoConfiguration的真实样子简化版Configuration ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) EnableConfigurationProperties(DataSourceProperties.class) Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class }) public class DataSourceAutoConfiguration { Configuration ConditionalOnMissingBean(DataSource.class) // 用户没自定义 DataSource 时才生效 static class EmbeddedDatabaseConfiguration { Bean public DataSource dataSource() { // 创建一个嵌入式数据库 } } }逻辑链条先检查 classpath 上有没有DataSource和EmbeddedDatabaseType→ 没有就整个类跳过类生效后再看内部配置类EmbeddedDatabaseConfiguration它上面又有一个条件容器里没有用户自定义的DataSourceBean两个条件都满足才创建嵌入式数据库这就是为什么 Spring Boot 能智能地不和你冲突你自己定义了DataSourceConditionalOnMissingBean就生效Spring Boot 的自动配置自动退让。第七阶段自定义条件注解进阶Spring Boot 的条件注解都是元注解模式——注解上贴Conditional。你也可以按这个模式封装自己的注解// 1. 定义注解 Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Documented Conditional(OnCustomFeatureCondition.class) public interface ConditionalOnCustomFeature { String value(); // 特性名称 } // 2. 实现 Condition public class OnCustomFeatureCondition extends SpringBootCondition { Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { // 读取注解上的 value 属性 MapString, Object attrs metadata.getAnnotationAttributes( ConditionalOnCustomFeature.class.getName() ); String feature (String) attrs.get(value); // 检查配置 String prop context.getEnvironment().getProperty(features. feature); if (true.equals(prop)) { return ConditionOutcome.match(); } return ConditionOutcome.noMatch(features. feature is not enabled); } } // 3. 使用 Configuration ConditionalOnCustomFeature(redis) public class RedisAdvancedConfig { }总结Conditional 的设计逻辑配置类无条件加载 → 导致启动失败/资源浪费 ↓ Profile 只能按环境区分粒度太粗 ↓ Conditional 诞生允许自定义任意条件判断 ↓ Spring Boot 封装出常用条件注解OnClass/OnProperty/OnBean... ↓ 自动配置体系建立starter 引入 → 条件满足 → 配置生效 ↓ 用户自定义 Bean → ConditionalOnMissingBean 避让 → 不冲突一句话记住Conditional的作用它让 Spring 在加载配置之前先问一个问题。答案为真配置生效答案为假就当这段代码不存在。这是 Spring Boot自动配置能够按需加载、不冲突的根基。