@Configuration 详解
本章定位:深入理解 Spring 的 Java Config 配置模式。@Configuration 是 Spring 从 XML 配置向类型安全、IDE 友好的编程式配置演进的核心注解,是现代 Spring 应用的配置基石。
定义与作用
@Configuration 是 Spring 提供的配置类注解,用于标记一个类为 Spring 的配置类。在该类中定义的 @Bean 方法将被容器特殊处理,确保方法返回的 Bean 遵循单例语义。
解决的痛点:早期 Spring 使用 XML 配置,存在类型不安全、重构困难、冗长繁琐等问题:
<!-- 痛点:XML 配置冗长,无编译期检查 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/student_db"/>
<property name="username" value="root"/>
<property name="password" value="secret"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
@Configuration 让配置类型安全、可重构、可编程,彻底告别 XML 的字符串引用:
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db");
config.setUsername("root");
config.setPassword("secret");
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
与 @Component 的关键差异
@Configuration 虽然标注了 @Component,但容器对其有特殊处理——方法拦截(Method Interception):
适用位置与常用属性
适用位置
@Configuration 只能标注在类级别,通常作为应用的主配置类或模块化配置类。
@Configuration
public class AppConfig {
// 配置内容
}
常用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| value | String | "" | 指定配置类的 Bean 名称 |
| proxyBeanMethods | boolean | true | 是否通过 CGLIB 代理拦截 @Bean 方法调用,确保单例语义 |
proxyBeanMethods 详解
从 Spring 5.2 开始,@Configuration 增加了 proxyBeanMethods 属性:
// 默认:启用方法代理(Full 模式)
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() { ... }
@Bean
public JdbcTemplate jdbcTemplate() {
// 这里调用 dataSource() 会被代理拦截,返回容器中的单例
return new JdbcTemplate(dataSource());
}
}
// Lite 模式:不启用方法代理,性能略优
@Configuration(proxyBeanMethods = false)
public class LiteConfig {
@Bean
public DataSource dataSource() { ... }
@Bean
public JdbcTemplate jdbcTemplate() {
// 这里调用 dataSource() 会执行方法体,创建新实例!
return new JdbcTemplate(dataSource());
}
}
Lite 模式适用于配置类内部不存在 @Bean 方法互相调用的场景,可减少 CGLIB 代理开销。
核心原理:CGLIB 方法拦截
@Configuration 类在容器启动时会被 CGLIB 生成子类,@Bean 方法被重写为"先查容器缓存,再决定是否执行方法体":
源码层面的实现:
// CGLIB 生成的代理类(伪代码)
public class AppConfig$$EnhancerBySpringCGLIB extends AppConfig {
@Override
public DataSource dataSource() {
// 先检查容器中是否已有该 Bean
if (this.beanFactory.containsBean("dataSource")) {
return this.beanFactory.getBean("dataSource", DataSource.class);
}
// 没有则调用父类方法创建
DataSource ds = super.dataSource();
// 注册到容器
this.beanFactory.registerSingleton("dataSource", ds);
return ds;
}
}
完整示例:飞翔科技公司的多环境配置
场景简述
飞翔科技的学生管理系统需要支持开发环境(dev)和生产环境(prod)。架构师白歌要求使用 @Configuration 将数据源、模板引擎等基础设施的配置集中管理,并支持按环境切换。
操作前:XML 配置的维护噩梦
<!-- 操作前:application-context.xml —— 冗长且类型不安全 -->
<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 id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
<property name="maximumPoolSize" value="${db.pool.size}"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
痛点:
- 属性名拼写错误(如 jdbcUrl 写成 jdbcURL)在编译期无法发现
- IDE 重构类名时,XML 中的全限定类名不会自动更新
- 条件配置(如 dev 用 H2,prod 用 MySQL)需要多套 XML 文件
- 无法使用 Java 的编程能力(如 if/else、循环)动态构建配置
操作后:使用 @Configuration 的 Java Config
// 操作后:AppConfig.java —— 主配置类
@Configuration
@ComponentScan("com.feixiang.student")
@PropertySource("classpath:application.properties")
@EnableTransactionManagement
public class AppConfig {
@Bean
public DataSource dataSource(
@Value("${db.url}") String url,
@Value("${db.username}") String username,
@Value("${db.password}") String password,
@Value("${db.pool.size:10}") int poolSize) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(poolSize);
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
// 操作后:DevDataSourceConfig.java —— 开发环境专属配置
@Configuration
@Profile("dev")
public class DevDataSourceConfig {
@Bean
public DataSource dataSource() {
// 开发环境使用嵌入式 H2 数据库,无需外部 MySQL
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:data.sql")
.build();
}
}
// 操作后:ProdDataSourceConfig.java —— 生产环境专属配置
@Configuration
@Profile("prod")
public class ProdDataSourceConfig {
@Bean
public DataSource dataSource(
@Value("${db.url}") String url,
@Value("${db.username}") String username,
@Value("${db.password}") String password) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(50);
config.setConnectionTimeout(30000);
return new HikariDataSource(config);
}
}
// 操作后:启动类
public class Main {
public static void main(String[] args) {
// 开发环境启动
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(AppConfig.class);
ctx.refresh();
DataSource ds = ctx.getBean(DataSource.class);
System.out.println("当前数据源:" + ds.getClass().getSimpleName());
// 输出:当前数据源:EmbeddedDatabaseFactory$EmbeddedDataSourceProxy
}
}
运行结果及分析
开发环境(dev)启动:
[main] INFO o.s.c.a.AnnotationConfigApplicationContext -
Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6d06d69c
[main] INFO o.s.j.d.e.EmbeddedDatabaseFactory -
Starting embedded database 'testdb'
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -
Creating shared instance of singleton bean 'dataSource'
[main] INFO c.f.s.config.Main - 当前数据源:EmbeddedDatabaseFactory$EmbeddedDataSourceProxy
生产环境(prod)启动:
[main] INFO o.s.c.a.AnnotationConfigApplicationContext -
Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6d06d69c
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -
Creating shared instance of singleton bean 'dataSource'
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed
[main] INFO c.f.s.config.Main - 当前数据源:HikariDataSource
改进点总结:
| 维度 | 操作前(XML) | 操作后(@Configuration) |
|---|---|---|
| 类型安全 | 字符串配置,编译期不检查 | Java 代码,编译期强类型检查 |
| IDE 重构 | 类名变更后 XML 需手动更新 | IDE 自动重构所有引用 |
| 条件逻辑 | 需多套 XML 文件 | @Profile + if/else 编程式控制 |
| 可读性 | XML 冗长,嵌套层级深 | Java 代码,结构清晰 |
| 调试 | 难以断点 | 可在 @Bean 方法中打断点 |
@Configuration 的模块化组织
大型项目中,配置类应按职责拆分:
// 数据源配置
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() { ... }
}
// 事务配置
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource ds) { ... }
}
// Web 配置
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean loggingFilter() { ... }
}
// 主配置类:聚合所有模块
@Configuration
@Import({DataSourceConfig.class, TransactionConfig.class, WebConfig.class})
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
易错场景与面试考点
反例一:@Configuration 类中直接调用 @Bean 方法导致多实例
小崔在 Lite 模式下调用 @Bean 方法,发现每个依赖都创建了新的 DataSource:
// ❌ 错误:proxyBeanMethods = false 时,@Bean 方法互相调用会创建多实例
@Configuration(proxyBeanMethods = false)
public class LiteConfig {
@Bean
public DataSource dataSource() {
System.out.println("创建 DataSource");
return new HikariDataSource();
}
@Bean
public JdbcTemplate jdbcTemplate() {
// 这里调用 dataSource() 会执行方法体,创建新的 DataSource!
return new JdbcTemplate(dataSource());
}
@Bean
public PlatformTransactionManager transactionManager() {
// 又创建了一个新的 DataSource!
return new DataSourceTransactionManager(dataSource());
}
}
运行输出:
创建 DataSource ← jdbcTemplate 调用
创建 DataSource ← transactionManager 调用
问题:产生了 3 个 DataSource 实例,连接池被重复初始化,资源浪费且事务管理器与 JdbcTemplate 使用不同的连接池。
纠正:使用默认的 Full 模式(proxyBeanMethods = true),或通过方法参数注入:
// ✅ 方案一:使用 Full 模式(默认)
@Configuration
public class FullConfig {
@Bean
public DataSource dataSource() { ... }
@Bean
public JdbcTemplate jdbcTemplate() {
// 被代理拦截,返回容器中的单例 DataSource
return new JdbcTemplate(dataSource());
}
}
// ✅ 方案二:通过方法参数注入(推荐,更明确)
@Configuration(proxyBeanMethods = false)
public class LiteConfig {
@Bean
public DataSource dataSource() { ... }
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
// 由容器注入唯一的 DataSource
return new JdbcTemplate(dataSource);
}
}
反例二:在 @Configuration 类中使用 @Autowired 字段注入
// ❌ 错误:配置类中使用字段注入,破坏不可变性
@Configuration
public class AppConfig {
@Autowired // 字段注入,不推荐
private Environment env;
@Bean
public DataSource dataSource() {
String url = env.getProperty("db.url");
// ...
}
}
纠正:配置类也应使用构造器注入:
// ✅ 正确:配置类使用构造器注入
@Configuration
public class AppConfig {
private final Environment env;
public AppConfig(Environment env) {
this.env = env;
}
@Bean
public DataSource dataSource() {
String url = env.getProperty("db.url");
// ...
}
}
反例三:@Configuration 类被误用为 @Component
小崔在 @Configuration 类中写了业务逻辑:
// ❌ 错误:@Configuration 类中混入业务逻辑
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() { ... }
// 业务逻辑不应该出现在配置类中!
public void enrollStudent(Student student) {
// ...
}
}
纠正:配置类只负责 Bean 定义和基础设施配置,业务逻辑放在 @Service 中:
// ✅ 正确:配置类只负责配置
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() { ... }
}
// ✅ 正确:业务逻辑放在 @Service
@Service
public class StudentService {
public void enrollStudent(Student student) { ... }
}
面试高频题
Q1:@Configuration 和 @Component 有什么区别?
@Configuration 标注了 @Component,因此也会被组件扫描注册为 Bean。但 @Configuration 有特殊处理:容器通过 CGLIB 为其生成代理子类,拦截 @Bean 方法调用,确保方法内部互相调用时返回的是容器中的单例 Bean,而非多次执行方法体创建新实例。这是 @Component 不具备的能力。
Q2:proxyBeanMethods = false 有什么作用?
关闭 @Bean 方法代理(Lite 模式)。适用于配置类内部不存在 @Bean 方法互相调用的场景,可减少 CGLIB 代理开销,提升启动性能。但如果配置类中存在 @Bean 方法互相调用(如 jdbcTemplate() 中调用 dataSource()),必须使用 proxyBeanMethods = true(Full 模式),否则会产生多实例。
Q3:@Bean 方法为什么能保证单例?
在 Full 模式下,@Configuration 类被 CGLIB 代理。@Bean 方法被重写为:先检查容器中是否已有该名称的 Bean,如果有则直接返回,不再执行方法体。因此即使多次调用,也只会创建一次实例。
Q4:@Configuration 类可以互相导入吗?
可以,通过 @Import 注解实现。被导入的 @Configuration 类中的 @Bean 方法同样会被处理。这是 Spring 模块化配置的核心机制,用于将大型配置拆分为多个独立的配置类。