1. 从XML到注解:Spring配置的演进之路
记得十年前我刚接触Spring框架时,项目中充斥着各种XML配置文件。那时候一个中等规模的项目,spring-config.xml文件动辄上千行,维护起来简直是场噩梦。直到Spring 3.0引入了基于Java的配置方式,特别是@Configuration注解的出现,才让我们这些Java开发者真正找回了"写代码"的感觉。
1.1 传统XML配置的痛点
在早期的Spring项目中,所有Bean的定义都必须在XML中声明。想象一下这样的场景:
xml复制<beans>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean id="userRepository" class="com.example.UserRepositoryImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="userService" class="com.example.UserServiceImpl">
<property name="userRepository" ref="userRepository"/>
</bean>
</beans>
这种配置方式存在几个明显问题:
- 类型不安全:拼写错误只能在运行时发现
- 重构困难:重命名类或属性时IDE无法提供支持
- 可读性差:复杂的依赖关系难以直观理解
1.2 @Configuration带来的变革
@Configuration注解的出现彻底改变了这一局面。同样的配置用Java代码可以这样写:
java复制@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/mydb");
ds.setUsername("root");
ds.setPassword("123456");
return ds;
}
@Bean
public UserRepository userRepository() {
return new UserRepositoryImpl(dataSource());
}
@Bean
public UserService userService() {
return new UserServiceImpl(userRepository());
}
}
这种方式的优势显而易见:
- 类型安全:编译器可以检查类型匹配
- 支持重构:IDE可以自动更新引用
- 更灵活:可以在配置中加入逻辑判断
- 更直观:代码即文档
提示:虽然现在推荐使用Java配置,但在某些场景下(如第三方库的配置)XML仍有其价值。Spring支持混合使用两种配置方式。
2. @Configuration的核心工作机制
2.1 配置类的本质
很多人以为@Configuration标记的类就是个普通的Bean定义容器,实际上它的工作机制要复杂得多。Spring会对@Configuration类进行CGLIB代理,这也是为什么配置类必须是非final的。
当你在配置类中调用@Bean方法时:
java复制@Configuration
public class AppConfig {
@Bean
public ServiceA serviceA() {
return new ServiceA(serviceB()); // 这里会被代理拦截
}
@Bean
public ServiceB serviceB() {
return new ServiceB();
}
}
实际上Spring会确保:
- serviceB()方法只会被调用一次
- 每次调用serviceA()时都会返回同一个ServiceA实例
- 依赖注入时使用的是容器管理的Bean
2.2 @Configuration vs @Component
虽然@Configuration本身也被@Component元注解标记,但两者有本质区别:
| 特性 | @Configuration | @Component |
|---|---|---|
| 代理行为 | 是 | 否 |
| 方法调用拦截 | 有 | 无 |
| 典型用途 | 定义Bean | 组件扫描 |
| 生命周期回调 | 支持 | 支持 |
一个常见的错误是把@Configuration当作普通@Component使用:
java复制// 错误用法!
@Component
public class BadConfig {
@Bean
public ServiceA serviceA() {
return new ServiceA(serviceB()); // 这里会直接创建新实例!
}
@Bean
public ServiceB serviceB() {
return new ServiceB();
}
}
这种情况下,每次调用serviceB()都会创建新实例,完全破坏了单例模式。
3. 高级配置技巧
3.1 条件化配置
在实际项目中,我们经常需要根据环境或条件决定是否注册某些Bean。Spring提供了强大的条件化配置机制:
java复制@Configuration
public class FeatureConfig {
@Bean
@ConditionalOnProperty(name = "feature.new", havingValue = "true")
public NewService newService() {
return new NewService();
}
@Bean
@ConditionalOnMissingBean(OldService.class)
public FallbackService fallbackService() {
return new FallbackService();
}
}
Spring Boot提供了丰富的条件注解:
- @ConditionalOnClass:类路径存在指定类时生效
- @ConditionalOnMissingBean:容器中不存在指定Bean时生效
- @ConditionalOnProperty:配置属性满足条件时生效
- @ConditionalOnWebApplication:是Web应用时生效
3.2 配置类组织策略
对于大型项目,合理的配置类组织非常重要。我推荐以下几种模式:
-
按功能模块划分:
code复制config/ ├── DatabaseConfig.java ├── SecurityConfig.java ├── CacheConfig.java └── MvcConfig.java -
按环境划分:
code复制config/ ├── DevConfig.java ├── TestConfig.java └── ProdConfig.java -
混合模式:
java复制@Configuration @Import({ DatabaseConfig.class, SecurityConfig.class }) @Profile("production") public class ProductionConfig { // 生产环境特有配置 }
3.3 配置外部化
最佳实践是将配置参数外部化,结合@PropertySource使用:
java复制@Configuration
@PropertySource("classpath:app.properties")
public class ExternalConfig {
@Value("${db.url}")
private String dbUrl;
@Bean
public DataSource dataSource(
@Value("${db.username}") String username,
@Value("${db.password}") String password) {
// 使用外部化配置创建数据源
}
}
对于更复杂的场景,可以使用@ConfigurationProperties:
java复制@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private Database database;
private Security security;
// getters and setters
public static class Database {
private String url;
private String username;
private String password;
// getters and setters
}
public static class Security {
private boolean enabled;
private String secretKey;
// getters and setters
}
}
4. 实战中的陷阱与解决方案
4.1 循环依赖问题
虽然Spring能解决大多数循环依赖问题,但在@Configuration类中要特别小心:
java复制@Configuration
public class CircularConfig {
@Bean
public ServiceA serviceA() {
return new ServiceA(serviceB()); // 直接方法调用会导致问题
}
@Bean
public ServiceB serviceB() {
return new ServiceB(serviceA());
}
}
解决方案:
- 使用setter注入代替构造器注入
- 通过参数注入:
java复制@Configuration
public class FixedCircularConfig {
@Bean
public ServiceA serviceA(ServiceB serviceB) {
return new ServiceA(serviceB);
}
@Bean
public ServiceB serviceB(ServiceA serviceA) {
return new ServiceB(serviceA);
}
}
4.2 Bean覆盖问题
默认情况下,Spring不允许同名Bean定义。但在某些场景下(如测试),我们可能需要覆盖Bean:
java复制@Configuration
public class PrimaryConfig {
@Bean
@Primary // 标记为首选Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public DataSource testDataSource() {
return new EmbeddedDatabaseBuilder().build();
}
}
在测试中可以使用@TestConfiguration来覆盖主配置:
java复制@TestConfiguration
public class TestConfig {
@Bean
@Primary // 覆盖主配置中的DataSource
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().build();
}
}
4.3 配置类加载顺序
当有多个配置类时,加载顺序可能影响Bean的初始化:
java复制@Configuration
@DependsOn("earlyInitConfig") // 明确指定依赖
public class DependOnConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
另一个技巧是使用@Order注解:
java复制@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE) // 最先加载
public class FirstConfig {
// 最先初始化的Bean
}
5. 性能优化技巧
5.1 懒加载配置
对于不立即需要的Bean,可以使用@Lazy:
java复制@Configuration
public class LazyConfig {
@Bean
@Lazy // 只有在首次被请求时才会初始化
public ExpensiveService expensiveService() {
return new ExpensiveService();
}
}
5.2 配置类轻量化
避免在配置类中放入业务逻辑,保持配置类简洁:
java复制// 不推荐
@Configuration
public class HeavyConfig {
@Bean
public ComplexService complexService() {
// 这里包含大量业务逻辑
Data data = loadData();
processData(data);
validate(data);
return new ComplexService(data);
}
}
// 推荐做法
@Configuration
public class LightConfig {
@Bean
public ComplexService complexService(DataLoader loader,
DataProcessor processor,
DataValidator validator) {
Data data = loader.load();
processor.process(data);
validator.validate(data);
return new ComplexService(data);
}
@Bean
public DataLoader dataLoader() { /* ... */ }
@Bean
public DataProcessor dataProcessor() { /* ... */ }
@Bean
public DataValidator dataValidator() { /* ... */ }
}
5.3 配置类缓存
Spring默认会缓存配置类的CGLIB代理,但大量配置类仍会影响启动性能。可以考虑:
- 合并相关配置类
- 使用@Import代替多个独立配置类
- 对于不常变化的配置,考虑使用XML配置
6. 与Spring Boot的深度集成
6.1 自动配置原理
Spring Boot的自动配置实际上就是基于@Configuration的:
java复制@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
理解这一点有助于我们:
- 自定义自动配置
- 覆盖默认配置
- 调试自动配置问题
6.2 自定义Starter
创建自定义Spring Boot Starter的关键也是@Configuration:
java复制@Configuration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MyStarterAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(DataSource dataSource) {
return new MyServiceImpl(dataSource);
}
}
在META-INF/spring.factories中声明:
code复制org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyStarterAutoConfiguration
6.3 配置类测试
Spring Boot提供了强大的测试支持:
java复制@SpringBootTest
@ContextConfiguration(classes = TestConfig.class)
public class ConfigTest {
@Autowired
private MyService myService;
@TestConfiguration
static class TestConfig {
@Bean
public MyService myService() {
return new MockMyService();
}
}
@Test
public void testService() {
assertThat(myService).isInstanceOf(MockMyService.class);
}
}
7. 最佳实践总结
经过多年Spring项目实践,我总结了以下@Configuration使用原则:
- 单一职责原则:每个配置类应该只负责一个特定领域的配置
- 显式优于隐式:明确声明依赖,避免隐式加载
- 环境隔离:使用@Profile严格区分不同环境配置
- 文档化:在配置类中添加必要的注释说明配置目的
- 测试覆盖:为关键配置编写测试用例
一个典型的良好实践示例:
java复制/**
* 数据库相关配置
* 配置主数据源和事务管理器
*/
@Configuration
@Profile("!test") // 不在测试环境中加载
@EnableTransactionManagement
@Slf4j
public class DatabaseConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.primary")
public DataSource primaryDataSource() {
log.info("Initializing primary datasource");
return DataSourceBuilder.create().build();
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
记住,好的配置应该像好的代码一样:清晰、简洁、易于维护。当你的配置变得复杂时,可能是时候考虑重构了。