diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 9cd8994da6c..f24f620518f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -236,6 +236,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.postgresql:postgresql") testImplementation("org.skyscreamer:jsonassert") testImplementation("org.springframework:spring-test") testImplementation("org.springframework:spring-core-test") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index 04a715ce639..c0bc7bc1710 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -47,6 +47,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayDataSourceCondition; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; @@ -86,6 +87,7 @@ import org.springframework.util.StringUtils; * @author Semyon Danilov * @author Chris Bono * @author Moritz Halbritter + * @author Andy Wilkinson * @since 1.1.0 */ @AutoConfiguration(after = { DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, @@ -125,7 +127,7 @@ public class FlywayAutoConfiguration { ObjectProvider fluentConfigurationCustomizers, ObjectProvider javaMigrations, ObjectProvider callbacks) { return flyway(properties, resourceLoader, dataSource, flywayDataSource, fluentConfigurationCustomizers, - javaMigrations, callbacks, new ResourceProviderCustomizer()); + javaMigrations, callbacks, new ResourceProviderCustomizer(), null); } @Bean @@ -133,9 +135,15 @@ public class FlywayAutoConfiguration { @FlywayDataSource ObjectProvider flywayDataSource, ObjectProvider fluentConfigurationCustomizers, ObjectProvider javaMigrations, ObjectProvider callbacks, - ResourceProviderCustomizer resourceProviderCustomizer) { + ResourceProviderCustomizer resourceProviderCustomizer, + ObjectProvider connectionDetailsProvider) { FluentConfiguration configuration = new FluentConfiguration(resourceLoader.getClassLoader()); - configureDataSource(configuration, properties, flywayDataSource.getIfAvailable(), dataSource.getIfUnique()); + JdbcConnectionDetails connectionDetails = (connectionDetailsProvider != null) + ? connectionDetailsProvider.getIfAvailable() : null; + connectionDetails = (connectionDetails != null) ? connectionDetails + : new FlywayPropertiesJdbcConnectionDetails(properties); + configureDataSource(configuration, flywayDataSource.getIfAvailable(), dataSource.getIfUnique(), + connectionDetails); configureProperties(configuration, properties); configureCallbacks(configuration, callbacks.orderedStream().toList()); configureJavaMigrations(configuration, javaMigrations.orderedStream().toList()); @@ -144,38 +152,41 @@ public class FlywayAutoConfiguration { return configuration.load(); } - private void configureDataSource(FluentConfiguration configuration, FlywayProperties properties, - DataSource flywayDataSource, DataSource dataSource) { - DataSource migrationDataSource = getMigrationDataSource(properties, flywayDataSource, dataSource); + private void configureDataSource(FluentConfiguration configuration, DataSource flywayDataSource, + DataSource dataSource, JdbcConnectionDetails connectionDetails) { + DataSource migrationDataSource = getMigrationDataSource(flywayDataSource, dataSource, connectionDetails); configuration.dataSource(migrationDataSource); } - private DataSource getMigrationDataSource(FlywayProperties properties, DataSource flywayDataSource, - DataSource dataSource) { + private DataSource getMigrationDataSource(DataSource flywayDataSource, DataSource dataSource, + JdbcConnectionDetails connectionDetails) { if (flywayDataSource != null) { return flywayDataSource; } - if (properties.getUrl() != null) { + String url = connectionDetails.getJdbcUrl(); + if (url != null) { DataSourceBuilder builder = DataSourceBuilder.create().type(SimpleDriverDataSource.class); - builder.url(properties.getUrl()); - applyCommonBuilderProperties(properties, builder); + builder.url(url); + applyConnectionDetails(connectionDetails, builder); return builder.build(); } - if (properties.getUser() != null && dataSource != null) { + String user = connectionDetails.getUsername(); + if (user != null && dataSource != null) { DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource) .type(SimpleDriverDataSource.class); - applyCommonBuilderProperties(properties, builder); + applyConnectionDetails(connectionDetails, builder); return builder.build(); } Assert.state(dataSource != null, "Flyway migration DataSource missing"); return dataSource; } - private void applyCommonBuilderProperties(FlywayProperties properties, DataSourceBuilder builder) { - builder.username(properties.getUser()); - builder.password(properties.getPassword()); - if (StringUtils.hasText(properties.getDriverClassName())) { - builder.driverClassName(properties.getDriverClassName()); + private void applyConnectionDetails(JdbcConnectionDetails connectionDetails, DataSourceBuilder builder) { + builder.username(connectionDetails.getUsername()); + builder.password(connectionDetails.getPassword()); + String driverClassName = connectionDetails.getDriverClassName(); + if (StringUtils.hasText(driverClassName)) { + builder.driverClassName(driverClassName); } } @@ -373,6 +384,11 @@ public class FlywayAutoConfiguration { } + @ConditionalOnBean(JdbcConnectionDetails.class) + private static final class JdbcConnectionDetailsCondition { + + } + @ConditionalOnProperty(prefix = "spring.flyway", name = "url") private static final class FlywayUrlCondition { @@ -389,4 +405,37 @@ public class FlywayAutoConfiguration { } + /** + * Adapts {@link FlywayProperties} to {@link JdbcConnectionDetails}. + */ + private static final class FlywayPropertiesJdbcConnectionDetails implements JdbcConnectionDetails { + + private final FlywayProperties properties; + + private FlywayPropertiesJdbcConnectionDetails(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.getUser(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + @Override + public String getJdbcUrl() { + return this.properties.getUrl(); + } + + @Override + public String getDriverClassName() { + return this.properties.getDriverClassName(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java index 6dc95e5820d..95523db179a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,13 @@ import com.zaxxer.hikari.HikariDataSource; import oracle.jdbc.OracleConnection; import oracle.ucp.jdbc.PoolDataSourceImpl; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -40,6 +43,8 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Stephane Nicoll * @author Fabio Grassi + * @author Moritz Halbritter + * @author Andy Wilkinson */ abstract class DataSourceConfiguration { @@ -48,6 +53,17 @@ abstract class DataSourceConfiguration { return (T) properties.initializeDataSourceBuilder().type(type).build(); } + @SuppressWarnings("unchecked") + protected static T createDataSource(JdbcConnectionDetails connectionDetails, Class type, + ClassLoader classLoader) { + return (T) DataSourceBuilder.create(classLoader) + .url(connectionDetails.getJdbcUrl()) + .username(connectionDetails.getUsername()) + .password(connectionDetails.getPassword()) + .type(type) + .build(); + } + /** * Tomcat Pool DataSource configuration. */ @@ -58,13 +74,26 @@ abstract class DataSourceConfiguration { matchIfMissing = true) static class Tomcat { + @Bean + @ConditionalOnBean(JdbcConnectionDetails.class) + static TomcatJdbcConnectionDetailsBeanPostProcessor tomcatJdbcConnectionDetailsBeanPostProcessor( + ObjectProvider connectionDetailsProvider) { + return new TomcatJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); + } + @Bean @ConfigurationProperties(prefix = "spring.datasource.tomcat") - org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties) { - org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(properties, - org.apache.tomcat.jdbc.pool.DataSource.class); - DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl()); - String validationQuery = databaseDriver.getValidationQuery(); + org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + Class dataSourceType = org.apache.tomcat.jdbc.pool.DataSource.class; + org.apache.tomcat.jdbc.pool.DataSource dataSource = (connectionDetails != null) + ? createDataSource(connectionDetails, dataSourceType, properties.getClassLoader()) + : createDataSource(properties, dataSourceType); + String validationQuery; + String url = (connectionDetails != null) ? connectionDetails.getJdbcUrl() : properties.determineUrl(); + DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(url); + validationQuery = databaseDriver.getValidationQuery(); if (validationQuery != null) { dataSource.setTestOnBorrow(true); dataSource.setValidationQuery(validationQuery); @@ -84,10 +113,21 @@ abstract class DataSourceConfiguration { matchIfMissing = true) static class Hikari { + @Bean + @ConditionalOnBean(JdbcConnectionDetails.class) + static HikariJdbcConnectionDetailsBeanPostProcessor jdbcConnectionDetailsHikariBeanPostProcessor( + ObjectProvider connectionDetailsProvider) { + return new HikariJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); + } + @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") - HikariDataSource dataSource(DataSourceProperties properties) { - HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); + HikariDataSource dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + HikariDataSource dataSource = (connectionDetails != null) + ? createDataSource(connectionDetails, HikariDataSource.class, properties.getClassLoader()) + : createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } @@ -106,10 +146,22 @@ abstract class DataSourceConfiguration { matchIfMissing = true) static class Dbcp2 { + @Bean + @ConditionalOnBean(JdbcConnectionDetails.class) + static Dbcp2JdbcConnectionDetailsBeanPostProcessor dbcp2JdbcConnectionDetailsBeanPostProcessor( + ObjectProvider connectionDetailsProvider) { + return new Dbcp2JdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); + } + @Bean @ConfigurationProperties(prefix = "spring.datasource.dbcp2") - org.apache.commons.dbcp2.BasicDataSource dataSource(DataSourceProperties properties) { - return createDataSource(properties, org.apache.commons.dbcp2.BasicDataSource.class); + org.apache.commons.dbcp2.BasicDataSource dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + Class dataSourceType = org.apache.commons.dbcp2.BasicDataSource.class; + return (connectionDetails != null) + ? createDataSource(connectionDetails, dataSourceType, properties.getClassLoader()) + : createDataSource(properties, dataSourceType); } } @@ -124,10 +176,21 @@ abstract class DataSourceConfiguration { matchIfMissing = true) static class OracleUcp { + @Bean + @ConditionalOnBean(JdbcConnectionDetails.class) + static OracleUcpJdbcConnectionDetailsBeanPostProcessor oracleUcpJdbcConnectionDetailsBeanPostProcessor( + ObjectProvider connectionDetailsProvider) { + return new OracleUcpJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); + } + @Bean @ConfigurationProperties(prefix = "spring.datasource.oracleucp") - PoolDataSourceImpl dataSource(DataSourceProperties properties) throws SQLException { - PoolDataSourceImpl dataSource = createDataSource(properties, PoolDataSourceImpl.class); + PoolDataSourceImpl dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) throws SQLException { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + PoolDataSourceImpl dataSource = (connectionDetails != null) + ? createDataSource(connectionDetails, PoolDataSourceImpl.class, properties.getClassLoader()) + : createDataSource(properties, PoolDataSourceImpl.class); dataSource.setValidateConnectionOnBorrow(true); if (StringUtils.hasText(properties.getName())) { dataSource.setConnectionPoolName(properties.getName()); @@ -146,7 +209,17 @@ abstract class DataSourceConfiguration { static class Generic { @Bean - DataSource dataSource(DataSourceProperties properties) { + DataSource dataSource(DataSourceProperties properties, + ObjectProvider connectionDetailsProvider) { + JdbcConnectionDetails connectionDetails = connectionDetailsProvider.getIfAvailable(); + if (connectionDetails != null) { + return DataSourceBuilder.create(properties.getClassLoader()) + .url(connectionDetails.getJdbcUrl()) + .username(connectionDetails.getUsername()) + .password(connectionDetails.getPassword()) + .type(properties.getType()) + .build(); + } return properties.initializeDataSourceBuilder().build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 00000000000..8aa2648e18e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.beans.factory.ObjectProvider; + +/** + * Post-processes beans of type {@link BasicDataSource} and name 'dataSource' to apply the + * values from {@link JdbcConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class Dbcp2JdbcConnectionDetailsBeanPostProcessor extends JdbcConnectionDetailsBeanPostProcessor { + + Dbcp2JdbcConnectionDetailsBeanPostProcessor(ObjectProvider connectionDetailsProvider) { + super(BasicDataSource.class, connectionDetailsProvider); + } + + @Override + protected Object processDataSource(BasicDataSource dataSource, JdbcConnectionDetails connectionDetails) { + dataSource.setUrl(connectionDetails.getJdbcUrl()); + dataSource.setUsername(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setDriverClassName(connectionDetails.getDriverClassName()); + return dataSource; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 00000000000..ca4b1b18f6a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import com.zaxxer.hikari.HikariDataSource; + +import org.springframework.beans.factory.ObjectProvider; + +/** + * Post-processes beans of type {@link HikariDataSource} and name 'dataSource' to apply + * the values from {@link JdbcConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class HikariJdbcConnectionDetailsBeanPostProcessor extends JdbcConnectionDetailsBeanPostProcessor { + + HikariJdbcConnectionDetailsBeanPostProcessor(ObjectProvider connectionDetailsProvider) { + super(HikariDataSource.class, connectionDetailsProvider); + } + + @Override + protected Object processDataSource(HikariDataSource dataSource, JdbcConnectionDetails connectionDetails) { + dataSource.setJdbcUrl(connectionDetails.getJdbcUrl()); + dataSource.setUsername(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setDriverClassName(connectionDetails.getDriverClassName()); + return dataSource; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetails.java new file mode 100644 index 00000000000..f6ca3c4afec --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetails.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.jdbc.DatabaseDriver; + +/** + * Details required to establish a connection to an SQL service using JDBC. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface JdbcConnectionDetails extends ConnectionDetails { + + /** + * Hostname for the database. + * @return the username for the database + */ + String getUsername(); + + /** + * Password for the database. + * @return the password for the database + */ + String getPassword(); + + /** + * JDBC url for the database. + * @return the JDBC url for the database + */ + String getJdbcUrl(); + + /** + * The name of the JDBC driver class. Defaults to the class name of the driver + * specified in the JDBC URL. + * @return the JDBC driver class name + * @see #getJdbcUrl() + * @see DatabaseDriver#fromJdbcUrl(String) + * @see DatabaseDriver#getDriverClassName() + */ + default String getDriverClassName() { + return DatabaseDriver.fromJdbcUrl(getJdbcUrl()).getDriverClassName(); + } + + /** + * Returns the name of the XA DataSource class. Defaults to the class name from the + * driver specified in the JDBC URL. + * @return the XA DataSource class name + * @see #getJdbcUrl() + * @see DatabaseDriver#fromJdbcUrl(String) + * @see DatabaseDriver#getXaDataSourceClassName() + */ + default String getXaDataSourceClassName() { + return DatabaseDriver.fromJdbcUrl(getJdbcUrl()).getXaDataSourceClassName(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 00000000000..e4cb0c8acff --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +/** + * Abstract base class for DataSource bean post processors which apply values from + * {@link JdbcConnectionDetails}. Acts on beans named 'dataSource' of type {@code T}. + * + * @param type of the datasource + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +abstract class JdbcConnectionDetailsBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { + + private final Class dataSourceClass; + + private final ObjectProvider connectionDetailsProvider; + + JdbcConnectionDetailsBeanPostProcessor(Class dataSourceClass, + ObjectProvider connectionDetailsProvider) { + this.dataSourceClass = dataSourceClass; + this.connectionDetailsProvider = connectionDetailsProvider; + } + + @Override + @SuppressWarnings("unchecked") + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (this.dataSourceClass.isAssignableFrom(bean.getClass()) && "dataSource".equals(beanName)) { + JdbcConnectionDetails connectionDetails = this.connectionDetailsProvider.getObject(); + return processDataSource((T) bean, connectionDetails); + } + return bean; + } + + protected abstract Object processDataSource(T dataSource, JdbcConnectionDetails connectionDetails); + + @Override + public int getOrder() { + // Runs after ConfigurationPropertiesBindingPostProcessor + return Ordered.HIGHEST_PRECEDENCE + 2; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 00000000000..4356a06f135 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import java.sql.SQLException; + +import oracle.ucp.jdbc.PoolDataSourceImpl; + +import org.springframework.beans.factory.ObjectProvider; + +/** + * Post-processes beans of type {@link PoolDataSourceImpl} and name 'dataSource' to apply + * the values from {@link JdbcConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class OracleUcpJdbcConnectionDetailsBeanPostProcessor + extends JdbcConnectionDetailsBeanPostProcessor { + + OracleUcpJdbcConnectionDetailsBeanPostProcessor(ObjectProvider connectionDetailsProvider) { + super(PoolDataSourceImpl.class, connectionDetailsProvider); + } + + @Override + protected Object processDataSource(PoolDataSourceImpl dataSource, JdbcConnectionDetails connectionDetails) { + try { + dataSource.setURL(connectionDetails.getJdbcUrl()); + dataSource.setUser(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setConnectionFactoryClassName(connectionDetails.getDriverClassName()); + return dataSource; + } + catch (SQLException ex) { + throw new RuntimeException("Failed to set URL / user / password of datasource", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessor.java new file mode 100644 index 00000000000..4efbcdf1fee --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import org.apache.tomcat.jdbc.pool.DataSource; + +import org.springframework.beans.factory.ObjectProvider; + +/** + * Post-processes beans of type {@link DataSource} and name 'dataSource' to apply the + * values from {@link JdbcConnectionDetails}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class TomcatJdbcConnectionDetailsBeanPostProcessor extends JdbcConnectionDetailsBeanPostProcessor { + + TomcatJdbcConnectionDetailsBeanPostProcessor(ObjectProvider connectionDetailsProvider) { + super(DataSource.class, connectionDetailsProvider); + } + + @Override + protected Object processDataSource(DataSource dataSource, JdbcConnectionDetails connectionDetails) { + dataSource.setUrl(connectionDetails.getJdbcUrl()); + dataSource.setUsername(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setDriverClassName(connectionDetails.getDriverClassName()); + return dataSource; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java index 37699912ac6..46a9f60f92d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,6 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyN import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; -import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.XADataSourceWrapper; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; @@ -54,6 +53,8 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Josh Long * @author Madhura Bhave + * @author Moritz Halbritter + * @author Andy Wilkinson * @since 1.2.0 */ @AutoConfiguration(before = DataSourceAutoConfiguration.class) @@ -67,8 +68,10 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { @Bean public DataSource dataSource(XADataSourceWrapper wrapper, DataSourceProperties properties, - ObjectProvider xaDataSource) throws Exception { - return wrapper.wrapDataSource(xaDataSource.getIfAvailable(() -> createXaDataSource(properties))); + ObjectProvider connectionDetails, ObjectProvider xaDataSource) + throws Exception { + return wrapper.wrapDataSource(xaDataSource.getIfAvailable(() -> createXaDataSource(properties, + connectionDetails.getIfAvailable(() -> new PropertiesJdbcConnectionDetails(properties))))); } @Override @@ -76,14 +79,11 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { this.classLoader = classLoader; } - private XADataSource createXaDataSource(DataSourceProperties properties) { - String className = properties.getXa().getDataSourceClassName(); - if (!StringUtils.hasLength(className)) { - className = DatabaseDriver.fromJdbcUrl(properties.determineUrl()).getXaDataSourceClassName(); - } + private XADataSource createXaDataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) { + String className = connectionDetails.getXaDataSourceClassName(); Assert.state(StringUtils.hasLength(className), "No XA DataSource class name specified"); XADataSource dataSource = createXaDataSourceInstance(className); - bindXaProperties(dataSource, properties); + bindXaProperties(dataSource, properties, connectionDetails); return dataSource; } @@ -99,18 +99,19 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { } } - private void bindXaProperties(XADataSource target, DataSourceProperties dataSourceProperties) { - Binder binder = new Binder(getBinderSource(dataSourceProperties)); + private void bindXaProperties(XADataSource target, DataSourceProperties dataSourceProperties, + JdbcConnectionDetails connectionDetails) { + Binder binder = new Binder(getBinderSource(dataSourceProperties, connectionDetails)); binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(target)); } - private ConfigurationPropertySource getBinderSource(DataSourceProperties dataSourceProperties) { - Map properties = new HashMap<>(); - properties.putAll(dataSourceProperties.getXa().getProperties()); - properties.computeIfAbsent("user", (key) -> dataSourceProperties.determineUsername()); - properties.computeIfAbsent("password", (key) -> dataSourceProperties.determinePassword()); + private ConfigurationPropertySource getBinderSource(DataSourceProperties dataSourceProperties, + JdbcConnectionDetails connectionDetails) { + Map properties = new HashMap<>(dataSourceProperties.getXa().getProperties()); + properties.computeIfAbsent("user", (key) -> connectionDetails.getUsername()); + properties.computeIfAbsent("password", (key) -> connectionDetails.getPassword()); try { - properties.computeIfAbsent("url", (key) -> dataSourceProperties.determineUrl()); + properties.computeIfAbsent("url", (key) -> connectionDetails.getJdbcUrl()); } catch (DataSourceBeanCreationException ex) { // Continue as not all XA DataSource's require a URL @@ -121,4 +122,45 @@ public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { return source.withAliases(aliases); } + /** + * Adapts {@link DataSourceProperties} to {@link JdbcConnectionDetails}. + */ + private static class PropertiesJdbcConnectionDetails implements JdbcConnectionDetails { + + private final DataSourceProperties properties; + + PropertiesJdbcConnectionDetails(DataSourceProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.determineUsername(); + } + + @Override + public String getPassword() { + return this.properties.determinePassword(); + } + + @Override + public String getJdbcUrl() { + return this.properties.determineUrl(); + } + + @Override + public String getDriverClassName() { + return (this.properties.getDriverClassName() != null) ? this.properties.getDriverClassName() + : JdbcConnectionDetails.super.getDriverClassName(); + } + + @Override + public String getXaDataSourceClassName() { + return (this.properties.getXa().getDataSourceClassName() != null) + ? this.properties.getXa().getDataSourceClassName() + : JdbcConnectionDetails.super.getXaDataSourceClassName(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java index c6b65c50e3e..1ae41a98f6d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseDataSourceCondition; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; @@ -61,6 +62,7 @@ import org.springframework.util.StringUtils; * @author András Deák * @author Ferenc Gratzer * @author Evgeniy Cheban + * @author Moritz Halbritter * @since 1.1.0 */ @AutoConfiguration(after = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) @@ -83,38 +85,34 @@ public class LiquibaseAutoConfiguration { @EnableConfigurationProperties(LiquibaseProperties.class) public static class LiquibaseConfiguration { - private final LiquibaseProperties properties; - - public LiquibaseConfiguration(LiquibaseProperties properties) { - this.properties = properties; - } - @Bean public SpringLiquibase liquibase(ObjectProvider dataSource, - @LiquibaseDataSource ObjectProvider liquibaseDataSource) { + @LiquibaseDataSource ObjectProvider liquibaseDataSource, LiquibaseProperties properties, + ObjectProvider connectionDetails) { SpringLiquibase liquibase = createSpringLiquibase(liquibaseDataSource.getIfAvailable(), - dataSource.getIfUnique()); - liquibase.setChangeLog(this.properties.getChangeLog()); - liquibase.setClearCheckSums(this.properties.isClearChecksums()); - liquibase.setContexts(this.properties.getContexts()); - liquibase.setDefaultSchema(this.properties.getDefaultSchema()); - liquibase.setLiquibaseSchema(this.properties.getLiquibaseSchema()); - liquibase.setLiquibaseTablespace(this.properties.getLiquibaseTablespace()); - liquibase.setDatabaseChangeLogTable(this.properties.getDatabaseChangeLogTable()); - liquibase.setDatabaseChangeLogLockTable(this.properties.getDatabaseChangeLogLockTable()); - liquibase.setDropFirst(this.properties.isDropFirst()); - liquibase.setShouldRun(this.properties.isEnabled()); - liquibase.setLabelFilter(this.properties.getLabelFilter()); - liquibase.setChangeLogParameters(this.properties.getParameters()); - liquibase.setRollbackFile(this.properties.getRollbackFile()); - liquibase.setTestRollbackOnUpdate(this.properties.isTestRollbackOnUpdate()); - liquibase.setTag(this.properties.getTag()); + dataSource.getIfUnique(), + connectionDetails.getIfAvailable(() -> new LiquibasePropertiesJdbcConnectionDetails(properties))); + liquibase.setChangeLog(properties.getChangeLog()); + liquibase.setClearCheckSums(properties.isClearChecksums()); + liquibase.setContexts(properties.getContexts()); + liquibase.setDefaultSchema(properties.getDefaultSchema()); + liquibase.setLiquibaseSchema(properties.getLiquibaseSchema()); + liquibase.setLiquibaseTablespace(properties.getLiquibaseTablespace()); + liquibase.setDatabaseChangeLogTable(properties.getDatabaseChangeLogTable()); + liquibase.setDatabaseChangeLogLockTable(properties.getDatabaseChangeLogLockTable()); + liquibase.setDropFirst(properties.isDropFirst()); + liquibase.setShouldRun(properties.isEnabled()); + liquibase.setLabelFilter(properties.getLabelFilter()); + liquibase.setChangeLogParameters(properties.getParameters()); + liquibase.setRollbackFile(properties.getRollbackFile()); + liquibase.setTestRollbackOnUpdate(properties.isTestRollbackOnUpdate()); + liquibase.setTag(properties.getTag()); return liquibase; } - private SpringLiquibase createSpringLiquibase(DataSource liquibaseDataSource, DataSource dataSource) { - LiquibaseProperties properties = this.properties; - DataSource migrationDataSource = getMigrationDataSource(liquibaseDataSource, dataSource, properties); + private SpringLiquibase createSpringLiquibase(DataSource liquibaseDataSource, DataSource dataSource, + JdbcConnectionDetails connectionDetails) { + DataSource migrationDataSource = getMigrationDataSource(liquibaseDataSource, dataSource, connectionDetails); SpringLiquibase liquibase = (migrationDataSource == liquibaseDataSource || migrationDataSource == dataSource) ? new SpringLiquibase() : new DataSourceClosingSpringLiquibase(); @@ -123,31 +121,34 @@ public class LiquibaseAutoConfiguration { } private DataSource getMigrationDataSource(DataSource liquibaseDataSource, DataSource dataSource, - LiquibaseProperties properties) { + JdbcConnectionDetails connectionDetails) { if (liquibaseDataSource != null) { return liquibaseDataSource; } - if (properties.getUrl() != null) { + String url = connectionDetails.getJdbcUrl(); + if (url != null) { DataSourceBuilder builder = DataSourceBuilder.create().type(SimpleDriverDataSource.class); - builder.url(properties.getUrl()); - applyCommonBuilderProperties(properties, builder); + builder.url(url); + applyConnectionDetails(connectionDetails, builder); return builder.build(); } - if (properties.getUser() != null && dataSource != null) { + String user = connectionDetails.getUsername(); + if (user != null && dataSource != null) { DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource) .type(SimpleDriverDataSource.class); - applyCommonBuilderProperties(properties, builder); + applyConnectionDetails(connectionDetails, builder); return builder.build(); } Assert.state(dataSource != null, "Liquibase migration DataSource missing"); return dataSource; } - private void applyCommonBuilderProperties(LiquibaseProperties properties, DataSourceBuilder builder) { - builder.username(properties.getUser()); - builder.password(properties.getPassword()); - if (StringUtils.hasText(properties.getDriverClassName())) { - builder.driverClassName(properties.getDriverClassName()); + private void applyConnectionDetails(JdbcConnectionDetails connectionDetails, DataSourceBuilder builder) { + builder.username(connectionDetails.getUsername()); + builder.password(connectionDetails.getPassword()); + String driverClassName = connectionDetails.getDriverClassName(); + if (StringUtils.hasText(driverClassName)) { + builder.driverClassName(driverClassName); } } @@ -164,6 +165,11 @@ public class LiquibaseAutoConfiguration { } + @ConditionalOnBean(JdbcConnectionDetails.class) + private static final class JdbcConnectionDetailsCondition { + + } + @ConditionalOnProperty(prefix = "spring.liquibase", name = "url") private static final class LiquibaseUrlCondition { @@ -180,4 +186,37 @@ public class LiquibaseAutoConfiguration { } + /** + * Adapts {@link LiquibaseProperties} to {@link JdbcConnectionDetails}. + */ + private static final class LiquibasePropertiesJdbcConnectionDetails implements JdbcConnectionDetails { + + private final LiquibaseProperties properties; + + private LiquibasePropertiesJdbcConnectionDetails(LiquibaseProperties properties) { + this.properties = properties; + } + + @Override + public String getUsername() { + return this.properties.getUser(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + @Override + public String getJdbcUrl() { + return this.properties.getUrl(); + } + + @Override + public String getDriverClassName() { + return this.properties.getDriverClassName(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index 6ac6211f37a..d904bc9be6a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -49,6 +49,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.SchemaManagement; @@ -120,6 +121,16 @@ class FlywayAutoConfigurationTests { }); } + @Test + void createsDataSourceWithNoDataSourceBeanAndJdbcConnectionDetails() { + this.contextRunner + .withUserConfiguration(JdbcConnectionDetailsConfiguration.class, MockFlywayMigrationStrategy.class) + .run((context) -> { + assertThat(context).hasSingleBean(Flyway.class); + assertThat(context.getBean(Flyway.class).getConfiguration().getDataSource()).isNotNull(); + }); + } + @Test void backsOffWithFlywayUrlAndNoSpringJdbc() { this.contextRunner.withPropertyValues("spring.flyway.url:jdbc:hsqldb:mem:" + UUID.randomUUID()) @@ -137,6 +148,28 @@ class FlywayAutoConfigurationTests { }); } + @Test + void createDataSourceWithJdbcConnectionDetails() { + this.contextRunner + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, JdbcConnectionDetailsConfiguration.class, + MockFlywayMigrationStrategy.class) + .withPropertyValues("spring.flyway.url=jdbc:hsqldb:mem:flywaytest", "spring.flyway.user=some-user", + "spring.flyway.password=some-password", + "spring.flyway.driver-class-name=org.hsqldb.jdbc.JDBCDriver") + .run((context) -> { + assertThat(context).hasSingleBean(Flyway.class); + Flyway flyway = context.getBean(Flyway.class); + DataSource dataSource = flyway.getConfiguration().getDataSource(); + assertThat(dataSource).isInstanceOf(SimpleDriverDataSource.class); + SimpleDriverDataSource simpleDriverDataSource = (SimpleDriverDataSource) dataSource; + assertThat(simpleDriverDataSource.getUrl()) + .isEqualTo("jdbc:postgresql://database.example.com:12345/database-1"); + assertThat(simpleDriverDataSource.getUsername()).isEqualTo("user-1"); + assertThat(simpleDriverDataSource.getPassword()).isEqualTo("secret-1"); + assertThat(simpleDriverDataSource.getDriver()).isInstanceOf(org.postgresql.Driver.class); + }); + } + @Test void createDataSourceWithUser() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -200,6 +233,19 @@ class FlywayAutoConfigurationTests { }); } + @Test + void flywayDataSourceIsUsedWhenJdbcConnectionDetailsIsAvailable() { + this.contextRunner + .withUserConfiguration(FlywayDataSourceConfiguration.class, EmbeddedDataSourceConfiguration.class, + JdbcConnectionDetailsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(JdbcConnectionDetails.class); + assertThat(context).hasSingleBean(Flyway.class); + assertThat(context.getBean(Flyway.class).getConfiguration().getDataSource()) + .isEqualTo(context.getBean("flywayDataSource")); + }); + } + @Test void flywayDataSourceWithoutDataSourceAutoConfiguration() { this.contextRunner.withUserConfiguration(FlywayDataSourceConfiguration.class).run((context) -> { @@ -1036,4 +1082,31 @@ class FlywayAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class JdbcConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new JdbcConnectionDetails() { + + @Override + public String getJdbcUrl() { + return "jdbc:postgresql://database.example.com:12345/database-1"; + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "secret-1"; + } + + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java index 181eab518df..7f4542a3430 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java @@ -36,6 +36,7 @@ import com.zaxxer.hikari.HikariDataSource; import io.r2dbc.spi.ConnectionFactory; import oracle.ucp.jdbc.PoolDataSourceImpl; import org.apache.commons.dbcp2.BasicDataSource; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; @@ -60,6 +61,9 @@ import static org.mockito.Mockito.mock; * * @author Dave Syer * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class DataSourceAutoConfigurationTests { @@ -244,6 +248,41 @@ class DataSourceAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(DataSourceScriptDatabaseInitializer.class)); } + @Test + void dbcp2UsesJdbcConnectionDetailsIfAvailable() { + ApplicationContextRunner runner = new ApplicationContextRunner() + .withPropertyValues("spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource", + "spring.datasource.dbcp2.url=jdbc:broken", "spring.datasource.dbcp2.username=alice", + "spring.datasource.dbcp2.password=secret") + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)); + runner.withUserConfiguration(JdbcConnectionDetailsConfiguration.class).run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).asInstanceOf(InstanceOfAssertFactories.type(BasicDataSource.class)) + .satisfies((dbcp2) -> { + assertThat(dbcp2.getUsername()).isEqualTo("user-1"); + assertThat(dbcp2.getPassword()).isEqualTo("password-1"); + assertThat(dbcp2.getDriverClassName()).isEqualTo("org.postgresql.Driver"); + assertThat(dbcp2.getUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + }); + } + + @Test + void genericUsesJdbcConnectionDetailsIfAvailable() { + ApplicationContextRunner runner = new ApplicationContextRunner() + .withPropertyValues("spring.datasource.type=" + TestDataSource.class.getName()) + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)); + runner.withUserConfiguration(JdbcConnectionDetailsConfiguration.class).run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(TestDataSource.class); + TestDataSource source = (TestDataSource) dataSource; + assertThat(source.getUsername()).isEqualTo("user-1"); + assertThat(source.getPassword()).isEqualTo("password-1"); + assertThat(source.getDriver().getClass().getName()).isEqualTo("org.postgresql.Driver"); + assertThat(source.getUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + } + private static Function hideConnectionPools() { return (runner) -> runner.withClassLoader(new FilteredClassLoader("org.apache.tomcat", "com.zaxxer.hikari", "org.apache.commons.dbcp2", "oracle.ucp.jdbc", "com.mchange")); @@ -259,6 +298,16 @@ class DataSourceAutoConfigurationTests { }); } + @Configuration(proxyBeanMethods = false) + static class JdbcConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails sqlJdbcConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + @Configuration(proxyBeanMethods = false) static class TestDataSourceConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java new file mode 100644 index 00000000000..996e6bd4165 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jdbc.DatabaseDriver; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Dbcp2JdbcConnectionDetailsBeanPostProcessor}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class Dbcp2JdbcConnectionDetailsBeanPostProcessorTests { + + @Test + void setUsernamePasswordAndUrl() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setUrl("will-be-overwritten"); + dataSource.setUsername("will-be-overwritten"); + dataSource.setPassword("will-be-overwritten"); + dataSource.setDriverClassName("will-be-overwritten"); + new Dbcp2JdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, + new TestJdbcConnectionDetails()); + assertThat(dataSource.getUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("password-1"); + assertThat(dataSource.getDriverClassName()).isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java index e308663eb31..35a33edffe3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java @@ -19,10 +19,13 @@ package org.springframework.boot.autoconfigure.jdbc; import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; @@ -31,9 +34,14 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Dave Syer * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class HikariDataSourceConfigurationTests { + private static final String PREFIX = "spring.datasource.hikari."; + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()); @@ -49,8 +57,7 @@ class HikariDataSourceConfigurationTests { @Test void testDataSourcePropertiesOverridden() { this.contextRunner - .withPropertyValues("spring.datasource.hikari.jdbc-url=jdbc:foo//bar/spam", - "spring.datasource.hikari.max-lifetime=1234") + .withPropertyValues(PREFIX + "jdbc-url=jdbc:foo//bar/spam", "spring.datasource.hikari.max-lifetime=1234") .run((context) -> { HikariDataSource ds = context.getBean(HikariDataSource.class); assertThat(ds.getJdbcUrl()).isEqualTo("jdbc:foo//bar/spam"); @@ -61,8 +68,7 @@ class HikariDataSourceConfigurationTests { @Test void testDataSourceGenericPropertiesOverridden() { this.contextRunner - .withPropertyValues( - "spring.datasource.hikari.data-source-properties.dataSourceClassName=org.h2.JDBCDataSource") + .withPropertyValues(PREFIX + "data-source-properties.dataSourceClassName=org.h2.JDBCDataSource") .run((context) -> { HikariDataSource ds = context.getBean(HikariDataSource.class); assertThat(ds.getDataSourceProperties().getProperty("dataSourceClassName")) @@ -90,12 +96,38 @@ class HikariDataSourceConfigurationTests { @Test void poolNameTakesPrecedenceOverName() { - this.contextRunner - .withPropertyValues("spring.datasource.name=myDS", "spring.datasource.hikari.pool-name=myHikariDS") + this.contextRunner.withPropertyValues("spring.datasource.name=myDS", PREFIX + "pool-name=myHikariDS") .run((context) -> { HikariDataSource ds = context.getBean(HikariDataSource.class); assertThat(ds.getPoolName()).isEqualTo("myHikariDS"); }); } + @Test + void usesConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class) + .withPropertyValues(PREFIX + "url=jdbc:broken", PREFIX + "username=alice", PREFIX + "password=secret") + .run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).asInstanceOf(InstanceOfAssertFactories.type(HikariDataSource.class)) + .satisfies((hikari) -> { + assertThat(hikari.getUsername()).isEqualTo("user-1"); + assertThat(hikari.getPassword()).isEqualTo("password-1"); + assertThat(hikari.getDriverClassName()).isEqualTo("org.postgresql.Driver"); + assertThat(hikari.getJdbcUrl()) + .isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails sqlConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessorTests.java new file mode 100644 index 00000000000..8031bea661d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariJdbcConnectionDetailsBeanPostProcessorTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jdbc.DatabaseDriver; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HikariJdbcConnectionDetailsBeanPostProcessor}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class HikariJdbcConnectionDetailsBeanPostProcessorTests { + + @Test + void setUsernamePasswordAndUrl() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("will-be-overwritten"); + dataSource.setUsername("will-be-overwritten"); + dataSource.setPassword("will-be-overwritten"); + dataSource.setDriverClassName(DatabaseDriver.H2.getDriverClassName()); + new HikariJdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, + new TestJdbcConnectionDetails()); + assertThat(dataSource.getJdbcUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("password-1"); + assertThat(dataSource.getDriverClassName()).isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java index 856fe0b408c..c8905b90265 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java @@ -22,10 +22,13 @@ import javax.sql.DataSource; import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceImpl; +import oracle.ucp.util.OpaqueString; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; @@ -34,9 +37,14 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Fabio Grassi * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class OracleUcpDataSourceConfigurationTests { + private static final String PREFIX = "spring.datasource.oracleucp."; + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) .withPropertyValues("spring.datasource.type=" + PoolDataSource.class.getName()); @@ -54,9 +62,7 @@ class OracleUcpDataSourceConfigurationTests { @Test void testDataSourcePropertiesOverridden() { - this.contextRunner - .withPropertyValues("spring.datasource.oracleucp.url=jdbc:foo//bar/spam", - "spring.datasource.oracleucp.max-idle-time=1234") + this.contextRunner.withPropertyValues(PREFIX + "url=jdbc:foo//bar/spam", PREFIX + "max-idle-time=1234") .run((context) -> { PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); assertThat(ds.getURL()).isEqualTo("jdbc:foo//bar/spam"); @@ -66,11 +72,10 @@ class OracleUcpDataSourceConfigurationTests { @Test void testDataSourceConnectionPropertiesOverridden() { - this.contextRunner.withPropertyValues("spring.datasource.oracleucp.connection-properties.autoCommit=false") - .run((context) -> { - PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); - assertThat(ds.getConnectionProperty("autoCommit")).isEqualTo("false"); - }); + this.contextRunner.withPropertyValues(PREFIX + "connection-properties.autoCommit=false").run((context) -> { + PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); + assertThat(ds.getConnectionProperty("autoCommit")).isEqualTo("false"); + }); } @Test @@ -100,12 +105,38 @@ class OracleUcpDataSourceConfigurationTests { @Test void poolNameTakesPrecedenceOverName() { this.contextRunner - .withPropertyValues("spring.datasource.name=myDS", - "spring.datasource.oracleucp.connection-pool-name=myOracleUcpDS") + .withPropertyValues("spring.datasource.name=myDS", PREFIX + "connection-pool-name=myOracleUcpDS") .run((context) -> { PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); assertThat(ds.getConnectionPoolName()).isEqualTo("myOracleUcpDS"); }); } + @Test + void usesJdbcConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class) + .withPropertyValues(PREFIX + "url=jdbc:broken", PREFIX + "username=alice", PREFIX + "password=secret") + .run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(PoolDataSourceImpl.class); + PoolDataSourceImpl oracleUcp = (PoolDataSourceImpl) dataSource; + assertThat(oracleUcp.getUser()).isEqualTo("user-1"); + assertThat(oracleUcp).extracting("password") + .extracting((o) -> ((OpaqueString) o).get()) + .isEqualTo("password-1"); + assertThat(oracleUcp.getConnectionFactoryClassName()).isEqualTo("org.postgresql.Driver"); + assertThat(oracleUcp.getURL()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java new file mode 100644 index 00000000000..8848bffcd23 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpJdbcConnectionDetailsBeanPostProcessorTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import java.sql.SQLException; + +import oracle.ucp.jdbc.PoolDataSourceImpl; +import oracle.ucp.util.OpaqueString; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jdbc.DatabaseDriver; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OracleUcpJdbcConnectionDetailsBeanPostProcessor}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class OracleUcpJdbcConnectionDetailsBeanPostProcessorTests { + + @Test + void setUsernamePasswordAndUrl() throws SQLException { + PoolDataSourceImpl dataSource = new PoolDataSourceImpl(); + dataSource.setURL("will-be-overwritten"); + dataSource.setUser("will-be-overwritten"); + dataSource.setPassword("will-be-overwritten"); + dataSource.setConnectionFactoryClassName("will-be-overwritten"); + new OracleUcpJdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, + new TestJdbcConnectionDetails()); + assertThat(dataSource.getURL()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUser()).isEqualTo("user-1"); + assertThat(dataSource).extracting("password") + .extracting((password) -> ((OpaqueString) password).get()) + .isEqualTo("password-1"); + assertThat(dataSource.getConnectionFactoryClassName()) + .isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestJdbcConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestJdbcConnectionDetails.java new file mode 100644 index 00000000000..3f277b01e01 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestJdbcConnectionDetails.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +/** + * {@link JdbcConnectionDetails} used in tests. + * + * @author Moritz Halbritter + */ +class TestJdbcConnectionDetails implements JdbcConnectionDetails { + + @Override + public String getJdbcUrl() { + return "jdbc:postgresql://postgres.example.com:12345/database-1"; + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatDataSourceConfigurationTests.java index ff193868912..5bfc55a32e1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatDataSourceConfigurationTests.java @@ -24,9 +24,11 @@ import org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -41,6 +43,9 @@ import static org.junit.jupiter.api.Assertions.fail; * * @author Dave Syer * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class TomcatDataSourceConfigurationTests { @@ -48,6 +53,10 @@ class TomcatDataSourceConfigurationTests { private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.datasource.type=" + org.apache.tomcat.jdbc.pool.DataSource.class.getName()); + @BeforeEach void init() { TestPropertyValues.of(PREFIX + "initialize:false").applyTo(this.context); @@ -106,6 +115,32 @@ class TomcatDataSourceConfigurationTests { assertThat(ds.getValidationInterval()).isEqualTo(3000L); } + @Test + void usesJdbcConnectionDetailsIfAvailable() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class) + .withPropertyValues(PREFIX + "url=jdbc:broken", PREFIX + "username=alice", PREFIX + "password=secret") + .run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(org.apache.tomcat.jdbc.pool.DataSource.class); + org.apache.tomcat.jdbc.pool.DataSource tomcat = (org.apache.tomcat.jdbc.pool.DataSource) dataSource; + assertThat(tomcat.getPoolProperties().getUsername()).isEqualTo("user-1"); + assertThat(tomcat.getPoolProperties().getPassword()).isEqualTo("password-1"); + assertThat(tomcat.getPoolProperties().getDriverClassName()).isEqualTo("org.postgresql.Driver"); + assertThat(tomcat.getPoolProperties().getUrl()) + .isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties @EnableMBeanExport diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessorTests.java new file mode 100644 index 00000000000..5906c6dfde6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TomcatJdbcConnectionDetailsBeanPostProcessorTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jdbc.DatabaseDriver; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TomcatJdbcConnectionDetailsBeanPostProcessor}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class TomcatJdbcConnectionDetailsBeanPostProcessorTests { + + @Test + void setUsernamePasswordAndUrl() { + DataSource dataSource = new DataSource(); + dataSource.setUrl("will-be-overwritten"); + dataSource.setUsername("will-be-overwritten"); + dataSource.setPassword("will-be-overwritten"); + dataSource.setDriverClassName("will-be-overwritten"); + new TomcatJdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, + new TestJdbcConnectionDetails()); + assertThat(dataSource.getUrl()).isEqualTo("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPoolProperties().getPassword()).isEqualTo("password-1"); + assertThat(dataSource.getPoolProperties().getDriverClassName()) + .isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java index 54ff0354f39..96cb982fa52 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java @@ -22,6 +22,7 @@ import javax.sql.XADataSource; import com.ibm.db2.jcc.DB2XADataSource; import org.hsqldb.jdbc.pool.JDBCXADataSource; import org.junit.jupiter.api.Test; +import org.postgresql.xa.PGXADataSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.jdbc.XADataSourceWrapper; @@ -40,6 +41,8 @@ import static org.mockito.Mockito.mock; * Tests for {@link XADataSourceAutoConfiguration}. * * @author Phillip Webb + * @author Moritz Halbritter + * @author Andy Wilkinson */ class XADataSourceAutoConfigurationTests { @@ -90,6 +93,20 @@ class XADataSourceAutoConfigurationTests { assertThat(dataSource.getLoginTimeout()).isEqualTo(123); } + @Test + void shouldUseConnectionDetailsIfAvailable() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(XADataSourceAutoConfiguration.class)) + .withUserConfiguration(FromProperties.class, JdbcConnectionDetailsConfiguration.class) + .run((context) -> { + MockXADataSourceWrapper wrapper = context.getBean(MockXADataSourceWrapper.class); + PGXADataSource dataSource = (PGXADataSource) wrapper.getXaDataSource(); + assertThat(dataSource).isNotNull(); + assertThat(dataSource.getUrl()).startsWith("jdbc:postgresql://postgres.example.com:12345/database-1"); + assertThat(dataSource.getUser()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("password-1"); + }); + } + private ApplicationContext createContext(Class configuration, String... env) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); TestPropertyValues.of(env).applyTo(context); @@ -98,6 +115,16 @@ class XADataSourceAutoConfigurationTests { return context; } + @Configuration(proxyBeanMethods = false) + static class JdbcConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new TestJdbcConnectionDetails(); + } + + } + @Configuration(proxyBeanMethods = false) static class WrapExisting { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index f61a1399832..39d64536921 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -28,6 +28,7 @@ import java.util.function.Consumer; import javax.sql.DataSource; +import com.zaxxer.hikari.HikariDataSource; import liquibase.integration.spring.SpringLiquibase; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,6 +40,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseAutoConfigurationRuntimeHints; @@ -75,6 +77,8 @@ import static org.assertj.core.api.Assertions.contentOf; * @author Andrii Hrytsiuk * @author Ferenc Gratzer * @author Evgeniy Cheban + * @author Moritz Halbritter + * @author Phillip Webb */ @ExtendWith(OutputCaptureExtension.class) class LiquibaseAutoConfigurationTests { @@ -97,6 +101,18 @@ class LiquibaseAutoConfigurationTests { })); } + @Test + void createsDataSourceWithNoDataSourceBeanAndJdbcConnectionDetails() { + this.contextRunner.withSystemProperties("shouldRun=false") + .withUserConfiguration(JdbcConnectionDetailsConfiguration.class) + .run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo("jdbc:postgresql://database.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("secret-1"); + })); + } + @Test void backsOffWithLiquibaseUrlAndNoSpringJdbc() { this.contextRunner.withPropertyValues("spring.liquibase.url:jdbc:hsqldb:mem:" + UUID.randomUUID()) @@ -116,6 +132,30 @@ class LiquibaseAutoConfigurationTests { })); } + @Test + void jdbcConnectionDetailsAreUsedIfAvailable() { + this.contextRunner.withSystemProperties("shouldRun=false") + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, JdbcConnectionDetailsConfiguration.class) + .run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo("jdbc:postgresql://database.example.com:12345/database-1"); + assertThat(dataSource.getUsername()).isEqualTo("user-1"); + assertThat(dataSource.getPassword()).isEqualTo("secret-1"); + })); + } + + @Test + void liquibaseDataSourceIsUsedOverJdbcConnectionDetails() { + this.contextRunner + .withUserConfiguration(LiquibaseDataSourceConfiguration.class, JdbcConnectionDetailsConfiguration.class) + .run(assertLiquibase((liquibase) -> { + HikariDataSource dataSource = (HikariDataSource) liquibase.getDataSource(); + assertThat(dataSource.getJdbcUrl()).startsWith("jdbc:hsqldb:mem:liquibasetest"); + assertThat(dataSource.getUsername()).isEqualTo("sa"); + assertThat(dataSource.getPassword()).isNull(); + })); + } + @Test void changelogXml() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -509,6 +549,33 @@ class LiquibaseAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class JdbcConnectionDetailsConfiguration { + + @Bean + JdbcConnectionDetails jdbcConnectionDetails() { + return new JdbcConnectionDetails() { + + @Override + public String getJdbcUrl() { + return "jdbc:postgresql://database.example.com:12345/database-1"; + } + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "secret-1"; + } + + }; + } + + } + static class CustomH2Driver extends org.h2.Driver { }