一、引言
Spring Boot 提供了数百个注解,但大多数开发者只使用了其中的 10%。今天我要分享的这 3 个注解,虽然不是最热门的,但用好了能让你的代码更简洁、更优雅、更易维护。
核心观点:好的代码不是写得更多,而是写得更少——用最简洁的方式表达最复杂的逻辑。
二、@ConfigurationProperties:比 @Value 强 10 倍的配置绑定
2.1 问题场景
你是否写过这样的代码?
@RestController
public class UserController {
@Value("${app.user.default-page-size}")
private int defaultPageSize;
@Value("${app.user.max-page-size}")
private int maxPageSize;
@Value("${app.user.cache-enabled}")
private boolean cacheEnabled;
@Value("${app.user.cache-ttl-minutes}")
private int cacheTtlMinutes;
@Value("${app.user.api-timeout-seconds}")
private int apiTimeoutSeconds;
// ... 业务逻辑
}
这种方式有什么问题?
- 类型不安全:全部是字符串,需要手动转换
- 松散耦合:配置分散在各个类中,难以管理
- 缺少校验:没有默认值和范围校验
- 重复代码:每个字段都要写 @Value
2.2 解决方案:@ConfigurationProperties
@Data
@ConfigurationProperties(prefix = "app.user")
public class UserProperties {
private int defaultPageSize = 20;
@Min(1)
@Max(100)
private int maxPageSize = 100;
private boolean cacheEnabled = true;
@Positive
private int cacheTtlMinutes = 30;
@Positive
private int apiTimeoutSeconds = 5;
}
启用配置类:
@Configuration
@EnableConfigurationProperties(UserProperties.class)
public class AppConfig {
}
使用方式:
@RestController
public class UserController {
private final UserProperties properties;
public UserController(UserProperties properties) {
this.properties = properties;
}
@GetMapping("/users")
public ResponseEntity<?> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
int actualSize = Math.min(size, properties.getMaxPageSize());
// ...
}
}
2.3 核心优势对比
| 特性 | @Value | @ConfigurationProperties |
|---|---|---|
| 类型安全 | ❌ 字符串为主 | ✅ 完整类型支持 |
| 默认值 | 需手动处理 | ✅ 字段默认值 |
| 宽松绑定 | ❌ | ✅ kebab-case/camelCase |
| JSR-303 校验 | ❌ | ✅ @Validated |
| 配置分组 | ❌ | ✅ 按前缀分组 |
| IDE 提示 | 弱 | ✅ 完整提示 |
| 文档生成 | ❌ | ✅ spring-boot-configuration-processor |
2.4 宽松绑定示例
# application.yml
app:
user:
default-page-size: 20 # kebab-case
maxPageSize: 100 # camelCase
max_page_size: 100 # underscore_case
以上三种写法都能正确绑定到 maxPageSize 字段。
2.5 校验示例
@Data
@Validated
@ConfigurationProperties(prefix = "app.user")
public class UserProperties {
@NotNull
@Size(min = 3, max = 50)
private String defaultRole;
@Min(1)
@Max(100)
private int maxPageSize;
@Email
private String supportEmail;
@Pattern(regexp = "^[a-z]+$")
private String environment;
}
启动时如果配置不符合要求,会抛出 BindValidationException。
三、@ConditionalOnExpression:用 SpEL 写复杂条件判断
3.1 问题场景
你是否遇到过这种情况?
@Configuration
public class DataSourceConfig {
@Bean
@ConditionalOnProperty(name = "app.datasource.type", havingValue = "mysql")
public DataSource mysqlDataSource() {
// MySQL 配置
}
@Bean
@ConditionalOnProperty(name = "app.datasource.type", havingValue = "postgresql")
public DataSource postgresqlDataSource() {
// PostgreSQL 配置
}
// 如果需要同时满足多个条件?
// 如果需要否定条件?
// 如果需要表达式判断?
}
@ConditionalOnProperty 只能处理简单的等值判断,遇到复杂场景就力不从心了。
3.2 解决方案:@ConditionalOnExpression
@Configuration
public class DataSourceConfig {
@Bean
@ConditionalOnExpression("'${app.datasource.type}' == 'mysql' && ${app.datasource.enabled:true}")
public DataSource mysqlDataSource() {
return new MySQLDataSource();
}
@Bean
@ConditionalOnExpression("'${app.datasource.type}' == 'postgresql' && ${app.datasource.enabled:true}")
public DataSource postgresqlDataSource() {
return new PostgreSQLDataSource();
}
@Bean
@ConditionalOnExpression("${app.datasource.type} != 'mysql' && ${app.datasource.type} != 'postgresql'")
public DataSource defaultDataSource() {
return new H2DataSource();
}
@Bean
@ConditionalOnExpression("#{!environment.containsProperty('app.datasource.type')}")
public DataSource autoDetectDataSource() {
return new AutoDetectDataSource();
}
}
3.3 SpEL 表达式技巧
字符串比较:
@ConditionalOnExpression("'${app.env}' == 'production'")
数值比较:
@ConditionalOnExpression("${app.threads:0} > 10")
否定条件:
@ConditionalOnExpression("!${app.feature.enabled:false}")
多条件组合:
@ConditionalOnExpression("${app.env} == 'test' || ${app.env} == 'dev'")
检查属性是否存在:
@ConditionalOnExpression("#{environment.containsProperty('app.custom.config')}")
3.4 实际应用场景
场景一:根据环境加载不同配置
@Configuration
public class MonitoringConfig {
@Bean
@ConditionalOnExpression("${app.env} == 'production'")
public PrometheusMeterRegistry prometheusMeterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}
@Bean
@ConditionalOnExpression("${app.env} != 'production'")
public SimpleMeterRegistry simpleMeterRegistry() {
return new SimpleMeterRegistry();
}
}
场景二:功能开关组合
@Configuration
public class FeatureConfig {
@Bean
@ConditionalOnExpression("${app.feature.a:false} && ${app.feature.b:false}")
public CombinedFeature combinedFeature() {
return new CombinedFeature();
}
@Bean
@ConditionalOnExpression("${app.feature.a:false} || ${app.feature.b:false}")
public PartialFeature partialFeature() {
return new PartialFeature();
}
}
四、@DependsOn:控制 Bean 初始化顺序
4.1 问题场景
你是否遇到过这种情况?
@Component
public class ResourceManager {
public ResourceManager() {
// 初始化外部资源(如加载配置文件、连接外部服务)
initExternalResource();
}
}
@Component
public class BusinessService {
public BusinessService(ResourceManager resourceManager) {
// 依赖 ResourceManager
// 但如果 ResourceManager 没有被 Spring 管理呢?
}
}
或者更复杂的场景:
@Component
public class DatabaseInitializer {
public DatabaseInitializer() {
// 初始化数据库 schema
initSchema();
}
}
@Component
public class Repository {
// 依赖数据库已经初始化
// 但 Spring 可能先初始化 Repository
}
4.2 解决方案:@DependsOn
@Component
public class DatabaseInitializer {
public DatabaseInitializer() {
initSchema();
}
}
@Component
@DependsOn("databaseInitializer")
public class Repository {
public Repository() {
// 此时 DatabaseInitializer 已经初始化完成
}
}
4.3 多个依赖
@Component
@DependsOn({"databaseInitializer", "cacheInitializer", "mqInitializer"})
public class ApplicationStartup {
public ApplicationStartup() {
// 所有依赖都已初始化
}
}
4.4 与 @Order 的区别
| 注解 | 作用 | 适用场景 |
|---|---|---|
@Order | 定义 Bean 的优先级 | 多个 Bean 实现同一接口 |
@DependsOn | 强制 Bean 初始化顺序 | 显式依赖关系 |
@Component
@Order(1)
public class FirstBean {
}
@Component
@Order(2)
public class SecondBean {
// 不保证 FirstBean 先初始化!
}
@Order 只定义优先级,不保证初始化顺序。而 @DependsOn 是强制的。
4.5 实际应用场景
场景一:初始化外部资源
@Component
public class ExternalResourceLoader {
private static ExternalResource resource;
public ExternalResourceLoader() {
resource = loadResourceFromFileSystem();
}
public static ExternalResource getResource() {
return resource;
}
}
@Component
@DependsOn("externalResourceLoader")
public class ResourceConsumer {
public ResourceConsumer() {
ExternalResource resource = ExternalResourceLoader.getResource();
// 使用资源
}
}
场景二:多数据源初始化
@Component
public class PrimaryDataSourceInitializer {
public PrimaryDataSourceInitializer() {
initPrimaryDataSource();
}
}
@Component
public class SecondaryDataSourceInitializer {
public SecondaryDataSourceInitializer() {
initSecondaryDataSource();
}
}
@Component
@DependsOn({"primaryDataSourceInitializer", "secondaryDataSourceInitializer"})
public class DataSourceRouter {
public DataSourceRouter() {
// 两个数据源都已初始化
}
}
五、综合实战示例
5.1 配置类
@Data
@Validated
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private Environment env = Environment.DEVELOPMENT;
private Database database = new Database();
private Features features = new Features();
@Data
public static class Database {
@NotNull
private String url;
@NotNull
private String username;
@NotNull
private String password;
@Min(1)
@Max(100)
private int poolSize = 10;
}
@Data
public static class Features {
private boolean monitoring = false;
private boolean caching = true;
private boolean async = false;
}
public enum Environment {
DEVELOPMENT, TEST, PRODUCTION
}
}
5.2 条件配置
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {
@Bean
@ConditionalOnExpression("${app.features.monitoring:true} && ${app.env} == 'production'")
public MonitoringService monitoringService() {
return new PrometheusMonitoringService();
}
@Bean
@ConditionalOnExpression("${app.features.monitoring:true} && ${app.env} != 'production'")
public MonitoringService devMonitoringService() {
return new SimpleMonitoringService();
}
@Bean
@ConditionalOnExpression("${app.features.caching:true}")
public CacheManager cacheManager() {
return new RedisCacheManager();
}
@Bean
@DependsOn("cacheManager")
@ConditionalOnExpression("${app.features.caching:true}")
public CacheService cacheService(CacheManager cacheManager) {
return new CacheServiceImpl(cacheManager);
}
}
5.3 YAML 配置
app:
env: production
database:
url: jdbc:mysql://localhost:3306/example
username: admin
password: password
pool-size: 20
features:
monitoring: true
caching: true
async: false
六、效果对比
| 指标 | 传统方式 | 使用注解后 | 提升 |
|---|---|---|---|
| 配置代码行数 | 50+ | 20 | -60% |
| 条件判断复杂度 | 高 | 低 | 显著降低 |
| 初始化顺序控制 | 困难 | 简单 | 100% 可控 |
| 类型安全 | 无 | 完整 | 类型检查 |
| 代码可维护性 | 差 | 好 | 显著提升 |
七、总结
核心要点:
- @ConfigurationProperties:类型安全配置绑定,配合 JSR-303 校验,让配置管理更规范
- @ConditionalOnExpression:用 SpEL 表达式写复杂条件,比 @ConditionalOnProperty 灵活得多
- @DependsOn:显式控制 Bean 初始化顺序,解决依赖外部资源的尴尬场景
最佳实践:
- 配置类优先使用
@ConfigurationProperties而非@Value - 复杂条件判断使用
@ConditionalOnExpression - 明确依赖关系时使用
@DependsOn - 配合
@Validated做配置校验
互动话题:你在实际项目中用过这三个注解吗?有没有遇到过什么坑?欢迎在评论区分享你的经验!
