Spring Boots @ConfigurationProperties
is a powerful annotation. Read the full story in the documentation.
These days it seems widely known that you can put it on type-level of one of your property classes and bind external configuration to that class.
What is less know is that you can use it on @Bean
annotated methods as well. These methods can return more or less arbitrary classes, which should ideally behave Java-beans like.
I have been propagating this solution in my Spring Boot Buch for JDBC databases already. Here’s a configuration class for using multiple datasources: MultipleDataSourcesConfig.java
import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; @Profile("multiple-data-sources") @Configuration public class MultipleDataSourcesConfig { @Primary @Bean @ConfigurationProperties("app.datasource-pg") public DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } @Primary @Bean public DataSource dataSource( final DataSourceProperties properties) { return properties .initializeDataSourceBuilder().build(); } @Bean @ConfigurationProperties("app.datasource-h2") public DataSourceProperties dataSourceH2Properties() { return new DataSourceProperties(); } @Bean public DataSource dataSourceH2( @Qualifier("dataSourceH2Properties") final DataSourceProperties properties ) { // Alternativly, you can use // dataSourceH2Properties() // instead of a qualifier return properties .initializeDataSourceBuilder().build(); } } |
The above configuration provides two beans of type DataSourceProperties
and both can be configured with a structure familiar for people working with JDBC. The main benefits I see: Reuse of existing property-classes, familiar structure, automatic data conversion based on Spring Boot mechanics.
app.datasource-pg.url = whatever app.datasource-pg.username = spring_postgres app.datasource-pg.password = spring_postgres app.datasource-pg.initialization-mode = ALWAYS app.datasource-h2.url = jdbc:h2:mem:test_mem app.datasource-h2.username = test_mem app.datasource-h2.password = test_mem |
The bean method names will define their names, but that could also be done explicit on the @Bean
annotation.
As you have multiple beans of the same type, you must use @Primary
to mark one as the default so that you don’t have to qualify it everywhere. For all non-primary ones, you must inject them with a qualifier.
The above example then creates data sources accordingly.
While I introduced new namespaces for the properties above, you can also an existing one, like I am doing here with Spring Data Neo4j 5 + OGM: Domain 1 uses the default properties via Domain1Config.java while Domain 2 uses different ones via Domain2Config.java. The project in question is an example of using different connections for different Spring Data (Neo4j) repositories, read the full story here.
A very similar project but for Spring Data Neo4j 6 is here as well: dn6-multidb-multi-connections.
In the later example these properties
spring.neo4j.authentication.username=u_movies spring.neo4j.authentication.password=p_movies spring.neo4j.uri=bolt://localhost:7687 spring.data.neo4j.database=movies fitness.spring.neo4j.authentication.username=u_fitness fitness.spring.neo4j.authentication.password=p_fitness fitness.spring.neo4j.uri=bolt://localhost:7687 fitness.spring.data.neo4j.database=fitness |
are mapped to two different instances of org.springframework.boot.autoconfigure.neo4j.Neo4jProperties
via this configuration:
import org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataProperties; import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @Configuration(proxyBeanMethods = false) public class Neo4jPropertiesConfig { @Bean @Primary @ConfigurationProperties("spring.neo4j") public Neo4jProperties moviesProperties() { return new Neo4jProperties(); } @Bean @Primary @ConfigurationProperties("spring.data.neo4j") public Neo4jDataProperties moviesDataProperties() { return new Neo4jDataProperties(); } @Bean @ConfigurationProperties("fitness.spring.neo4j") public Neo4jProperties fitnessProperties() { return new Neo4jProperties(); } @Bean @ConfigurationProperties("fitness.spring.data.neo4j") public Neo4jDataProperties fitnessDataProperties() { return new Neo4jDataProperties(); } } |
and can be processed further down the road. Here: Creating the necessary beans for SDN 6. Note that the injected properties are not qualified. The default or primary ones will be used:
@Configuration(proxyBeanMethods = false) @EnableNeo4jRepositories( basePackageClasses = MoviesConfig.class, neo4jMappingContextRef = "moviesContext", neo4jTemplateRef = "moviesTemplate", transactionManagerRef = "moviesManager" ) public class MoviesConfig { @Primary @Bean public Driver moviesDriver(Neo4jProperties neo4jProperties) { var authentication = neo4jProperties.getAuthentication(); return GraphDatabase.driver(neo4jProperties.getUri(), AuthTokens.basic( authentication.getUsername(), authentication .getPassword())); } @Primary @Bean public Neo4jClient moviesClient(Driver driver, DatabaseSelectionProvider moviesSelection) { return Neo4jClient.create(driver, moviesSelection); } @Primary @Bean public Neo4jOperations moviesTemplate( Neo4jClient moviesClient, Neo4jMappingContext moviesContext ) { return new Neo4jTemplate(moviesClient, moviesContext); } @Primary @Bean public DatabaseSelectionAwareNeo4jHealthIndicator movieHealthIndicator(Driver driver, DatabaseSelectionProvider moviesSelection) { return new DatabaseSelectionAwareNeo4jHealthIndicator(driver, moviesSelection); } @Primary @Bean public PlatformTransactionManager moviesManager(Driver driver, DatabaseSelectionProvider moviesSelection ) { return new Neo4jTransactionManager(driver, moviesSelection); } @Primary @Bean public DatabaseSelectionProvider moviesSelection( Neo4jDataProperties dataProperties) { return () -> DatabaseSelection.byName(dataProperties.getDatabase()); } @Primary @Bean public Neo4jMappingContext moviesContext(ResourceLoader resourceLoader, Neo4jConversions neo4jConversions) throws ClassNotFoundException { Neo4jMappingContext context = new Neo4jMappingContext(neo4jConversions); context.setInitialEntitySet(Neo4jEntityScanner.get(resourceLoader).scan(this.getClass().getPackageName())); return context; } } |
I do actually work for Neo4j and part of my job is the integration of our database connector with Spring Boot and we contributed the auto configuration of the driver.
There’s a lot of effort having sane defaults and an automatic configuration that doesn’t do anything surprising. We sometimes feel irritated when we find custom config that replicates what the build in does, but with different types and names.
This is not necessary, even not in a complex scenario of multiple connections for different repositories like shown above but especially not when dealing with properties: When you want to have the same set of properties multiple times in different namespaces, do yourself a favor and use the combination of @Bean
methods returning existing property classes mapped to the namespace in question via @ConfigurationProperties
.
If you have those configurational property classes at hand, use them for the actual bean creation: Autoconfiguration will step away if it sees your instances.
No comments yet
One Trackback/Pingback
[…] >> Multiple Instances of the Same Configuration-Properties-Class in Spring Boot [info.michael-simons.eu] […]
Post a Comment