Как в Spring ограничить создание бина, чтобы при старте приложения инициализировался только один из нескольких DataSource
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ: Селективная инициализация DataSource в Spring
Вопрос касается того, как в Spring выбрать, какой из нескольких DataSource бинов должен быть инициализирован при старте приложения. Это решается несколькими способами.
Проблема
Представим, что у вас есть несколько конфигураций DataSource, но вы хотите использовать только одну в зависимости от окружения (dev, staging, prod) или другого условия.
// ❌ Проблема: оба DataSource будут инициализированы
@Configuration
public class DataSourceConfig {
@Bean
public DataSource primaryDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://primary-db:3306/mydb")
.username("user")
.password("password")
.build();
}
@Bean
public DataSource secondaryDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://secondary-db:3306/mydb")
.username("user")
.password("password")
.build();
}
}
Оба DataSource будут инициализированы и попытаются подключиться к БД при старте приложения.
Решение 1: @ConditionalOnProperty
@Configuration
public class DataSourceConfig {
// Инициализируется только если app.datasource.type = primary
@Bean
@ConditionalOnProperty(
name = "app.datasource.type",
havingValue = "primary",
matchIfMissing = true // Если свойство не указано, использовать primary
)
public DataSource primaryDataSource() {
return DataSourceBuilder.create()
.url("${spring.datasource.primary.url}")
.username("${spring.datasource.primary.username}")
.password("${spring.datasource.primary.password}")
.build();
}
// Инициализируется только если app.datasource.type = secondary
@Bean
@ConditionalOnProperty(
name = "app.datasource.type",
havingValue = "secondary"
)
public DataSource secondaryDataSource() {
return DataSourceBuilder.create()
.url("${spring.datasource.secondary.url}")
.username("${spring.datasource.secondary.username}")
.password("${spring.datasource.secondary.password}")
.build();
}
}
// application.properties
// app.datasource.type=primary
// spring.datasource.primary.url=jdbc:mysql://db1:3306/app
// spring.datasource.primary.username=user1
// spring.datasource.primary.password=pass1
// application-staging.properties
// app.datasource.type=secondary
// spring.datasource.secondary.url=jdbc:mysql://db2:3306/app
// spring.datasource.secondary.username=user2
// spring.datasource.secondary.password=pass2
Решение 2: @ConditionalOnExpression
@Configuration
public class DataSourceConfig {
// Используется при сложной логике
@Bean
@ConditionalOnExpression("'${spring.profiles.active}' == 'prod'")
public DataSource productionDataSource() {
return createDataSource(
"jdbc:mysql://prod-db:3306/myapp",
"produser",
"${DB_PASSWORD}" // Из переменной окружения
);
}
@Bean
@ConditionalOnExpression("'${spring.profiles.active}' != 'prod'")
public DataSource developmentDataSource() {
return createDataSource(
"jdbc:h2:mem:db",
"sa",
""
);
}
private DataSource createDataSource(String url, String user, String password) {
return DataSourceBuilder.create()
.url(url)
.username(user)
.password(password)
.build();
}
}
// application.properties
// spring.profiles.active=dev
// application-prod.properties
// spring.profiles.active=prod
Решение 3: Использование Profiles
Это самый «Spring way» подход.
// Конфигурация для профиля "dev"
@Configuration
@Profile("dev")
public class DevDataSourceConfig {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:h2:mem:devdb")
.username("sa")
.password("")
.build();
}
}
// Конфигурация для профиля "prod"
@Configuration
@Profile("prod")
public class ProdDataSourceConfig {
@Bean
public DataSource dataSource(
@Value("${db.prod.url}") String url,
@Value("${db.prod.username}") String username,
@Value("${db.prod.password}") String password
) {
return DataSourceBuilder.create()
.url(url)
.username(username)
.password(password)
.build();
}
}
// Конфигурация для профиля "staging"
@Configuration
@Profile("staging")
public class StagingDataSourceConfig {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://staging-db:3306/myapp")
.username("staging_user")
.password("${STAGING_DB_PASSWORD}")
.build();
}
}
// application.properties
// spring.profiles.active=prod
// Или через env переменные
// SPRING_PROFILES_ACTIVE=staging
Решение 4: @Conditional с Custom Condition
Для более сложной логики:
// Кастомное условие
public class IsPrimaryDatabaseCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Читаем конфигурацию и проверяем условие
String datasourceType = context.getEnvironment()
.getProperty("app.datasource.type");
return "primary".equals(datasourceType);
}
}
public class IsSecondaryDatabaseCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String datasourceType = context.getEnvironment()
.getProperty("app.datasource.type");
return "secondary".equals(datasourceType);
}
}
// Использование кастомного условия
@Configuration
public class DataSourceConfig {
@Bean
@Conditional(IsPrimaryDatabaseCondition.class)
public DataSource primaryDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://primary:3306/db")
.build();
}
@Bean
@Conditional(IsSecondaryDatabaseCondition.class)
public DataSource secondaryDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://secondary:3306/db")
.build();
}
}
Решение 5: Factory Bean с условием
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource(
@Value("${app.datasource.type:primary}") String type,
@Value("${spring.datasource.url}") String url,
@Value("${spring.datasource.username}") String username,
@Value("${spring.datasource.password}") String password
) {
DataSourceBuilder<?> builder = DataSourceBuilder.create();
switch (type) {
case "primary":
return builder
.url(url)
.username(username)
.password(password)
.build();
case "secondary":
return builder
.url("jdbc:mysql://secondary:3306/db")
.username("user2")
.password("pass2")
.build();
default:
throw new IllegalArgumentException("Unknown datasource type: " + type);
}
}
}
// Это простой способ, но инициализирует все параметры БД при создании бина
Решение 6: Ленивая инициализация
@Configuration
public class DataSourceConfig {
@Bean
@Lazy // Бин инициализируется только когда его запросят
@ConditionalOnProperty(name = "app.datasource.type", havingValue = "primary")
public DataSource primaryDataSource() {
System.out.println("Initializing Primary DataSource");
return DataSourceBuilder.create()
.url("jdbc:mysql://primary:3306/db")
.build();
}
@Bean
@Lazy
@ConditionalOnProperty(name = "app.datasource.type", havingValue = "secondary")
public DataSource secondaryDataSource() {
System.out.println("Initializing Secondary DataSource");
return DataSourceBuilder.create()
.url("jdbc:mysql://secondary:3306/db")
.build();
}
}
Практический пример: Multi-tenant приложение
@Configuration
public class MultiTenantDataSourceConfig {
@Bean
public DataSource dataSource(
@Value("${app.tenant:default}") String tenant,
@Value("${spring.datasource.url}") String url,
@Value("${spring.datasource.username}") String username,
@Value("${spring.datasource.password}") String password
) {
// Выбираем URL в зависимости от tenant
String tenantUrl = url.replace("{tenant}", tenant);
return DataSourceBuilder.create()
.url(tenantUrl)
.username(username)
.password(password)
.hikariConfig(hikariConfig())
.build();
}
private HikariConfig hikariConfig() {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
config.setConnectionTimeout(10000);
return config;
}
}
// application.properties
// spring.datasource.url=jdbc:mysql://db-{tenant}.example.com:3306/app
// spring.datasource.username=app_user
// spring.datasource.password=${DB_PASSWORD}
// app.tenant=customer1
Сравнение подходов
┌──────────────────────────┬──────────┬──────────┬─────────────────┐
│ Подход │ Гибкость │ Сложность│ Рекомендуется │
├──────────────────────────┼──────────┼──────────┼─────────────────┤
│ @ConditionalOnProperty │ ⭐⭐⭐ │ ⭐ │ Для простых │
│ @Profile │ ⭐⭐ │ ⭐ │ Для окружений │
│ @ConditionalOnExpression │ ⭐⭐⭐⭐ │ ⭐⭐ │ Сложная логика │
│ @Conditional │ ⭐⭐⭐⭐⭐│ ⭐⭐⭐ │ Очень сложная │
│ Factory Bean │ ⭐⭐ │ ⭐⭐ │ Не рекомендуется│
│ @Lazy │ ⭐ │ ⭐ │ Дополнение │
└──────────────────────────┴──────────┴──────────┴─────────────────┘
Лучшая практика
// ✅ Рекомендуемый подход для большинства случаев
@Configuration
public class DataSourceConfig {
@Bean
@ConditionalOnProperty(
name = "app.db.enabled",
havingValue = "true",
matchIfMissing = true
)
public DataSource dataSource(
@Value("${spring.datasource.url}") String url,
@Value("${spring.datasource.username}") String username,
@Value("${spring.datasource.password}") String password
) {
return DataSourceBuilder.create()
.url(url)
.username(username)
.password(password)
.build();
}
@Bean
@ConditionalOnMissingBean(DataSource.class)
public DataSource defaultDataSource() {
// Fallback: в памяти БД для тестирования
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
}
// application.properties
// spring.datasource.url=jdbc:mysql://localhost:3306/app
// spring.datasource.username=root
// spring.datasource.password=password
// app.db.enabled=true
Заключение
Для селективной инициализации DataSource в Spring используйте:
- @ConditionalOnProperty — для простого выбора по свойствам
- @Profile — для разных окружений (dev, staging, prod)
- @ConditionalOnExpression — для более сложной логики
- @Conditional — для кастомной логики
- @Lazy — для отложенной инициализации
Много DataSource бинов инициализируются, только если их условие выполняется (указано в @ConditionalOnProperty или других аннотациях). Spring не создаст их, если условие false.