From 48819253eb5171b93ede2b630fdb8e64ca845e62 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 11 Jul 2018 11:23:44 +0200 Subject: [PATCH] Fix dependency order between JdbcTemplate and database migration tools This commit makes sure that Flyway/Liquibase migrates the schema if necessary before a `JdbcTemplate` is made available as an injection point. This commit also adds a test that validates simple datasource initialization (spring.datasource.*) happens before a `JdbcTemplate` bean can be used. Closes gh-13155 --- ...ractDependsOnBeanFactoryPostProcessor.java | 16 +++-- .../flyway/FlywayAutoConfiguration.java | 37 ++++++++++- .../JdbcOperationsDependsOnPostProcessor.java | 42 +++++++++++++ .../liquibase/LiquibaseAutoConfiguration.java | 18 ++++++ .../JdbcTemplateAutoConfigurationTests.java | 62 +++++++++++++++++++ 5 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcOperationsDependsOnPostProcessor.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AbstractDependsOnBeanFactoryPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AbstractDependsOnBeanFactoryPostProcessor.java index 8a86fedfb11..e9b72b49f02 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AbstractDependsOnBeanFactoryPostProcessor.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AbstractDependsOnBeanFactoryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -58,6 +58,11 @@ public abstract class AbstractDependsOnBeanFactoryPostProcessor this.dependsOn = dependsOn; } + protected AbstractDependsOnBeanFactoryPostProcessor(Class beanClass, + String... dependsOn) { + this(beanClass, null, dependsOn); + } + @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { for (String beanName : getBeanNames(beanFactory)) { @@ -74,9 +79,12 @@ public abstract class AbstractDependsOnBeanFactoryPostProcessor Set names = new HashSet<>(); names.addAll(Arrays.asList(BeanFactoryUtils.beanNamesForTypeIncludingAncestors( beanFactory, this.beanClass, true, false))); - for (String factoryBeanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors( - beanFactory, this.factoryBeanClass, true, false)) { - names.add(BeanFactoryUtils.transformedBeanName(factoryBeanName)); + if (this.factoryBeanClass != null) { + for (String factoryBeanName : BeanFactoryUtils + .beanNamesForTypeIncludingAncestors(beanFactory, + this.factoryBeanClass, true, false)) { + names.add(BeanFactoryUtils.transformedBeanName(factoryBeanName)); + } } return names; } 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 a93adbc15a6..98dd49b5255 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 @@ -41,6 +41,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.autoconfigure.jdbc.JdbcOperationsDependsOnPostProcessor; +import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; @@ -51,6 +53,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.ResourceLoader; +import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.MetaDataAccessException; import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; @@ -76,7 +79,7 @@ import org.springframework.util.StringUtils; @ConditionalOnBean(DataSource.class) @ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", matchIfMissing = true) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, - HibernateJpaAutoConfiguration.class }) + JdbcTemplateAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) public class FlywayAutoConfiguration { @Bean @@ -202,6 +205,22 @@ public class FlywayAutoConfiguration { } + /** + * Additional configuration to ensure that {@link JdbcOperations} beans depend-on + * the {@code flywayInitializer} bean. + */ + @Configuration + @ConditionalOnClass(JdbcOperations.class) + @ConditionalOnBean(JdbcOperations.class) + protected static class FlywayInitializerJdbcOperationsDependencyConfiguration + extends JdbcOperationsDependsOnPostProcessor { + + public FlywayInitializerJdbcOperationsDependencyConfiguration() { + super("flywayInitializer"); + } + + } + } /** @@ -220,6 +239,22 @@ public class FlywayAutoConfiguration { } + /** + * Additional configuration to ensure that {@link JdbcOperations} beans depend-on the + * {@code flyway} bean. + */ + @Configuration + @ConditionalOnClass(JdbcOperations.class) + @ConditionalOnBean(JdbcOperations.class) + protected static class FlywayJdbcDependencyConfiguration + extends JdbcOperationsDependsOnPostProcessor { + + public FlywayJdbcDependencyConfiguration() { + super("flyway"); + } + + } + private static class SpringBootFlyway extends Flyway { @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcOperationsDependsOnPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcOperationsDependsOnPostProcessor.java new file mode 100644 index 00000000000..8c5ee30994c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcOperationsDependsOnPostProcessor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; +import org.springframework.jdbc.core.JdbcOperations; + +/** + * {@link BeanFactoryPostProcessor} that can be used to dynamically declare that all + * {@link JdbcOperations} beans should "depend on" one or more specific beans. + * + * @author Marcel Overdijk + * @author Dave Syer + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.1.0 + * @see BeanDefinition#setDependsOn(String[]) + */ +public class JdbcOperationsDependsOnPostProcessor + extends AbstractDependsOnBeanFactoryPostProcessor { + + public JdbcOperationsDependsOnPostProcessor(String... dependsOn) { + super(JdbcOperations.class, dependsOn); + } + +} 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 0ef3992e0b6..e1e4500afe9 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 @@ -39,6 +39,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.autoconfigure.jdbc.JdbcOperationsDependsOnPostProcessor; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; @@ -47,6 +48,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.jdbc.core.JdbcOperations; import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.util.Assert; @@ -189,6 +191,22 @@ public class LiquibaseAutoConfiguration { } + /** + * Additional configuration to ensure that {@link JdbcOperations} beans depend-on the + * liquibase bean. + */ + @Configuration + @ConditionalOnClass(JdbcOperations.class) + @ConditionalOnBean(JdbcOperations.class) + protected static class LiquibaseJdbcOperationsDependencyConfiguration + extends JdbcOperationsDependsOnPostProcessor { + + public LiquibaseJdbcOperationsDependencyConfiguration() { + super("liquibase"); + } + + } + /** * A custom {@link SpringLiquibase} extension that closes the underlying * {@link DataSource} once the database has been migrated. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java index 5a585918c62..d918b4658f3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java @@ -21,6 +21,8 @@ import javax.sql.DataSource; import org.junit.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -159,6 +161,44 @@ public class JdbcTemplateAutoConfigurationTests { }); } + @Test + public void testDependencyToDataSourceInitialization() { + this.contextRunner.withUserConfiguration(DataSourceInitializationValidator.class) + .withPropertyValues("spring.datasource.initialization-mode=always") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context + .getBean(DataSourceInitializationValidator.class).count) + .isEqualTo(1); + }); + } + + @Test + public void testDependencyToFlyway() { + this.contextRunner.withUserConfiguration(DataSourceMigrationValidator.class) + .withPropertyValues("spring.flyway.locations:classpath:db/city") + .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context.getBean(DataSourceMigrationValidator.class).count) + .isEqualTo(0); + }); + } + + @Test + public void testDependencyToLiquibase() { + this.contextRunner.withUserConfiguration(DataSourceMigrationValidator.class) + .withPropertyValues( + "spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml") + .withConfiguration( + AutoConfigurations.of(LiquibaseAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context.getBean(DataSourceMigrationValidator.class).count) + .isEqualTo(0); + }); + } + @Configuration static class CustomConfiguration { @@ -216,4 +256,26 @@ public class JdbcTemplateAutoConfigurationTests { } + static class DataSourceInitializationValidator { + + private final Integer count; + + DataSourceInitializationValidator(JdbcTemplate jdbcTemplate) { + this.count = jdbcTemplate.queryForObject("SELECT COUNT(*) from BAR", + Integer.class); + } + + } + + static class DataSourceMigrationValidator { + + private final Integer count; + + DataSourceMigrationValidator(JdbcTemplate jdbcTemplate) { + this.count = jdbcTemplate.queryForObject("SELECT COUNT(*) from CITY", + Integer.class); + } + + } + }