Separate type customization from additional DataSource configuration

Closes gh-43054
This commit is contained in:
Andy Wilkinson 2024-11-06 18:18:32 +00:00
parent 94f2fabe24
commit 0be9fd91ab
9 changed files with 54 additions and 54 deletions

View File

@ -52,10 +52,15 @@ The following example shows how to define a JDBC data source by setting properti
pool-size: 30 pool-size: 30
---- ----
However, there is a catch. However, there is a catch due to the method's `DataSource` return type.
Because the actual type of the connection pool is not exposed, no keys are generated in the metadata for your custom `DataSource` and no completion is available in your IDE (because the `DataSource` interface exposes no properties). This hides the actual type of the connection pool so no configuration property metadata is generated for your custom `DataSource` and no auto-completion is available in your IDE.
Also, if you happen to have Hikari on the classpath, this basic setup does not work, because Hikari has no `url` property (but does have a `jdbcUrl` property). To address this problem, use the builder's `type(Class)` method to specify the type of `DataSource` to be built and update the method's return type.
In that case, you must rewrite your configuration as follows: For example, the following shows how to create a `HikariDataSource` with `DataSourceBuilder`:
include::code:simple/MyDataSourceConfiguration[]
Unfortunately, this basic setup does not work because Hikari has no `url` property.
Instead, it has a `jdbc-url` property which means that you must rewrite your configuration as follows:
[source,yaml,indent=0,subs="verbatim",configblocks] [source,yaml,indent=0,subs="verbatim",configblocks]
---- ----
@ -67,22 +72,15 @@ In that case, you must rewrite your configuration as follows:
pool-size: 30 pool-size: 30
---- ----
You can fix that by forcing the connection pool to use and return a dedicated implementation rather than `DataSource`. To address this problem, make use of `DataSourceProperties` which will handle the `url` to `jdbc-url` translation for you.
You cannot change the implementation at runtime, but the list of options will be explicit. You can initialize a `DataSourceBuilder` from the state of any `DataSourceProperties` object using its `initializeDataSourceBuilder()` method.
You could inject the `DataSourceProperties` that Spring Boot creates automatically, however, that would split your configuration across `+spring.datasource.*+` and `+app.datasource.*+`.
The following example shows how to create a `HikariDataSource` with `DataSourceBuilder`: To avoid this, define a custom `DataSourceProperties` with a custom configuration properties prefix, as shown in the following example:
include::code:simple/MyDataSourceConfiguration[]
You can even go further by leveraging what `DataSourceProperties` does for you -- that is, by providing a default embedded database with a sensible username and password if no URL is provided.
You can easily initialize a `DataSourceBuilder` from the state of any `DataSourceProperties` object, so you could also inject the DataSource that Spring Boot creates automatically.
However, that would split your configuration into two namespaces: `url`, `username`, `password`, `type`, and `driver` on `spring.datasource` and the rest on your custom namespace (`app.datasource`).
To avoid that, you can redefine a custom `DataSourceProperties` on your custom namespace, as shown in the following example:
include::code:configurable/MyDataSourceConfiguration[] include::code:configurable/MyDataSourceConfiguration[]
This setup puts you _in sync_ with what Spring Boot does for you by default, except that a dedicated connection pool is chosen (in code) and its settings are exposed in the `app.datasource.configuration` sub namespace. This setup is equivalent to what Spring Boot does for you by default, except that the pool's type is specified in code and its settings are exposed as `app.datasource.configuration.*` properties.
Because `DataSourceProperties` is taking care of the `url`/`jdbcUrl` translation for you, you can configure it as follows: `DataSourceProperties` takes care of the `url` to `jdbc-url` translation, so you can configure it as follows:
[source,yaml,indent=0,subs="verbatim",configblocks] [source,yaml,indent=0,subs="verbatim",configblocks]
---- ----
@ -95,13 +93,16 @@ Because `DataSourceProperties` is taking care of the `url`/`jdbcUrl` translation
maximum-pool-size: 30 maximum-pool-size: 30
---- ----
Note that, as the custom configuration specifies in code that Hikari should be used, `app.datasource.type` will have no effect.
As described in "`<<data#data.sql.datasource.connection-pool>>`", `DataSourceBuilder` supports several different connection pools.
To use a pool other than Hikari, add it to the classpath, use the `type(Class)` method to specify the pool class to use, and update the `@Bean` method's return type to match.
This will also provide you with configuration property metadata for the specific connection pool that you've chosen.
TIP: Spring Boot will expose Hikari-specific settings to `spring.datasource.hikari`. TIP: Spring Boot will expose Hikari-specific settings to `spring.datasource.hikari`.
This example uses a more generic `configuration` sub namespace as the example does not support multiple datasource implementations. This example uses a more generic `configuration` sub namespace as the example does not support multiple datasource implementations.
NOTE: Because your custom configuration chooses to go with Hikari, `app.datasource.type` has no effect. See "`<<data#data.sql.datasource>>`" and the {spring-boot-autoconfigure-module-code}/jdbc/DataSourceAutoConfiguration.java[`DataSourceAutoConfiguration`] class for more details.
In practice, the builder is initialized with whatever value you might set there and then overridden by the call to `.type()`.
See "`<<data#data.sql.datasource>>`" in the "`Spring Boot features`" section and the {spring-boot-autoconfigure-module-code}/jdbc/DataSourceAutoConfiguration.java[`DataSourceAutoConfiguration`] class for more details.
@ -142,9 +143,12 @@ You can apply the same concept to the secondary `DataSource` as well, as shown i
include::code:MyCompleteDataSourcesConfiguration[] include::code:MyCompleteDataSourcesConfiguration[]
The preceding example configures two data sources on custom namespaces with the same logic as Spring Boot would use in auto-configuration. The preceding example configures two data sources on custom configuration property namespaces with the same logic as Spring Boot would use in auto-configuration.
Note that each `configuration` sub namespace provides advanced settings based on the chosen implementation. Note that each `configuration` sub namespace provides advanced settings based on the chosen implementation.
As with <<#howto.data-access.configure-custom-datasource,configuring a single custom `DataSource`>>, the type of one or both of the `DataSource` beans can be customized using the `type(Class)` method on `DataSourceBuilder`.
See "`<<data#data.sql.datasource.connection-pool>>`" for details of the supported types.
[[howto.data-access.spring-data-repositories]] [[howto.data-access.spring-data-repositories]]

View File

@ -33,7 +33,7 @@ include::{docs-java}/howto/testing/slicetests/MyConfiguration.java[]
For a `@WebMvcTest` for an application with the above `@Configuration` class, you might expect to have the `SecurityFilterChain` bean in the application context so that you can test if your controller endpoints are secured properly. For a `@WebMvcTest` for an application with the above `@Configuration` class, you might expect to have the `SecurityFilterChain` bean in the application context so that you can test if your controller endpoints are secured properly.
However, `MyConfiguration` is not picked up by @WebMvcTest's component scanning filter because it doesn't match any of the types specified by the filter. However, `MyConfiguration` is not picked up by @WebMvcTest's component scanning filter because it doesn't match any of the types specified by the filter.
You can include the configuration explicitly by annotating the test class with `@Import(MyConfiguration.class)`. You can include the configuration explicitly by annotating the test class with `@Import(MyConfiguration.class)`.
This will load all the beans in `MyConfiguration` including the `BasicDataSource` bean which isn't required when testing the web tier. This will load all the beans in `MyConfiguration` including the `HikariDataSource` bean which isn't required when testing the web tier.
Splitting the configuration class into two will enable importing just the security configuration. Splitting the configuration class into two will enable importing just the security configuration.
[source,java,indent=0,subs="verbatim"] [source,java,indent=0,subs="verbatim"]

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2021 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources; package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
@ -51,9 +50,9 @@ public class MyCompleteDataSourcesConfiguration {
@Bean @Bean
@ConfigurationProperties("app.datasource.second.configuration") @ConfigurationProperties("app.datasource.second.configuration")
public BasicDataSource secondDataSource( public HikariDataSource secondDataSource(
@Qualifier("secondDataSourceProperties") DataSourceProperties secondDataSourceProperties) { @Qualifier("secondDataSourceProperties") DataSourceProperties secondDataSourceProperties) {
return secondDataSourceProperties.initializeDataSourceBuilder().type(BasicDataSource.class).build(); return secondDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2021 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources; package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@ -45,8 +44,8 @@ public class MyDataSourcesConfiguration {
@Bean @Bean
@ConfigurationProperties("app.datasource.second") @ConfigurationProperties("app.datasource.second")
public BasicDataSource secondDataSource() { public HikariDataSource secondDataSource() {
return DataSourceBuilder.create().type(BasicDataSource.class).build(); return DataSourceBuilder.create().type(HikariDataSource.class).build();
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2022 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +16,7 @@
package org.springframework.boot.docs.howto.testing.slicetests; package org.springframework.boot.docs.howto.testing.slicetests;
import org.apache.commons.dbcp2.BasicDataSource; import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DataSourceBuilder;
@ -36,8 +36,8 @@ public class MyConfiguration {
@Bean @Bean
@ConfigurationProperties("app.datasource.second") @ConfigurationProperties("app.datasource.second")
public BasicDataSource secondDataSource() { public HikariDataSource secondDataSource() {
return DataSourceBuilder.create().type(BasicDataSource.class).build(); return DataSourceBuilder.create().type(HikariDataSource.class).build();
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2022 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +16,7 @@
package org.springframework.boot.docs.howto.testing.slicetests; package org.springframework.boot.docs.howto.testing.slicetests;
import org.apache.commons.dbcp2.BasicDataSource; import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DataSourceBuilder;
@ -28,8 +28,8 @@ public class MyDatasourceConfiguration {
@Bean @Bean
@ConfigurationProperties("app.datasource.second") @ConfigurationProperties("app.datasource.second")
public BasicDataSource secondDataSource() { public HikariDataSource secondDataSource() {
return DataSourceBuilder.create().type(BasicDataSource.class).build(); return DataSourceBuilder.create().type(HikariDataSource.class).build();
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2022 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import org.apache.commons.dbcp2.BasicDataSource
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
@ -49,8 +48,8 @@ class MyCompleteDataSourcesConfiguration {
@Bean @Bean
@ConfigurationProperties("app.datasource.second.configuration") @ConfigurationProperties("app.datasource.second.configuration")
fun secondDataSource(secondDataSourceProperties: DataSourceProperties): BasicDataSource { fun secondDataSource(secondDataSourceProperties: DataSourceProperties): HikariDataSource {
return secondDataSourceProperties.initializeDataSourceBuilder().type(BasicDataSource::class.java).build() return secondDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource::class.java).build()
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2022 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@
package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import org.apache.commons.dbcp2.BasicDataSource
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.jdbc.DataSourceBuilder import org.springframework.boot.jdbc.DataSourceBuilder
@ -44,8 +43,8 @@ class MyDataSourcesConfiguration {
@Bean @Bean
@ConfigurationProperties("app.datasource.second") @ConfigurationProperties("app.datasource.second")
fun secondDataSource(): BasicDataSource { fun secondDataSource(): HikariDataSource {
return DataSourceBuilder.create().type(BasicDataSource::class.java).build() return DataSourceBuilder.create().type(HikariDataSource::class.java).build()
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2021 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,7 +20,7 @@ import java.sql.SQLException;
import javax.sql.DataSource; import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource; import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -38,8 +38,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@SpringBootTest(properties = { "app.datasource.second.url=jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1", @SpringBootTest(properties = { "app.datasource.second.jdbc-url=jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1",
"app.datasource.second.max-total=42" }) "app.datasource.second.maximum-pool-size=42" })
@Import(MyDataSourcesConfiguration.class) @Import(MyDataSourcesConfiguration.class)
class MyDataSourcesConfigurationTests { class MyDataSourcesConfigurationTests {
@ -52,9 +52,9 @@ class MyDataSourcesConfigurationTests {
DataSource dataSource = this.context.getBean(DataSource.class); DataSource dataSource = this.context.getBean(DataSource.class);
assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource); assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource);
assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:"); assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:");
BasicDataSource secondDataSource = this.context.getBean("secondDataSource", BasicDataSource.class); HikariDataSource secondDataSource = this.context.getBean("secondDataSource", HikariDataSource.class);
assertThat(secondDataSource.getUrl()).isEqualTo("jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1"); assertThat(secondDataSource.getJdbcUrl()).isEqualTo("jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1");
assertThat(secondDataSource.getMaxTotal()).isEqualTo(42); assertThat(secondDataSource.getMaximumPoolSize()).isEqualTo(42);
} }
} }