Multiple instances of the same configuration-properties-class in Spring Boot

Useful for JDBC datasource, Neo4j connections and related things.
September 29, 2021 by Michael

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

Post a Comment

Your email is never published. We need your name and email address only for verifying a legitimate comment. For more information, a copy of your saved data or a request to delete any data under this address, please send a short notice to michael@simons.ac from the address you used to comment on this entry.
By entering and submitting a comment, wether with or without name or email address, you'll agree that all data you have entered including your IP address will be checked and stored for a limited time by Automattic Inc., 60 29th Street #343, San Francisco, CA 94110-4929, USA. only for the purpose of avoiding spam. You can deny further storage of your data by sending an email to support@wordpress.com, with subject “Deletion of Data stored by Akismet”.
Required fields are marked *