Fix NamedParameterJdbcTemplate precedence with database migration tools

See gh-16047
This commit is contained in:
Dan Zheng 2019-02-27 20:16:34 +08:00 committed by Stephane Nicoll
parent 2236e959b6
commit 2c4afb3bd8
7 changed files with 328 additions and 0 deletions

View File

@ -46,6 +46,7 @@ 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.jdbc.NamedParameterJdbcOperationsDependsOnPostProcessor;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -57,6 +58,7 @@ 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.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
@ -76,6 +78,7 @@ import org.springframework.util.StringUtils;
* @author Jacques-Etienne Beaudet
* @author Eddú Meléndez
* @author Dominic Gunn
* @author Dan Zheng
* @since 1.1.0
*/
@SuppressWarnings("deprecation")
@ -321,6 +324,23 @@ public class FlywayAutoConfiguration {
public FlywayInitializerJdbcOperationsDependencyConfiguration() {
super("flywayInitializer");
}
}
/**
* Additional configuration to ensure that {@link NamedParameterJdbcOperations}
* beans depend on the {@code flywayInitializer} bean.
*/
@Configuration
@ConditionalOnClass(NamedParameterJdbcOperations.class)
@ConditionalOnBean(NamedParameterJdbcOperations.class)
protected static class FlywayInitializerNamedParameterJdbcOperationsDependencyConfiguration
extends NamedParameterJdbcOperationsDependsOnPostProcessor {
public FlywayInitializerNamedParameterJdbcOperationsDependencyConfiguration() {
super("flywayInitializer");
}
}
@ -359,6 +379,22 @@ public class FlywayAutoConfiguration {
}
/**
* Additional configuration to ensure that {@link NamedParameterJdbcOperations} beans
* depend on the {@code flyway} bean.
*/
@Configuration
@ConditionalOnClass(NamedParameterJdbcOperations.class)
@ConditionalOnBean(NamedParameterJdbcOperations.class)
protected static class FlywayNamedParameterJdbcOperationsDependencyConfiguration
extends NamedParameterJdbcOperationsDependsOnPostProcessor {
public FlywayNamedParameterJdbcOperationsDependencyConfiguration() {
super("flyway");
}
}
private static class LocationResolver {
private static final String VENDOR_PLACEHOLDER = "{vendor}";

View File

@ -0,0 +1,40 @@
/*
* Copyright 2012-2019 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.namedparam.NamedParameterJdbcOperations;
/**
* {@link BeanFactoryPostProcessor} that can be used to dynamically declare that all
* {@link NamedParameterJdbcOperations} beans should "depend on" one or more specific
* beans.
*
* @author Dan Zheng
* @since 2.1.x
* @see BeanDefinition#setDependsOn(String[])
*/
public class NamedParameterJdbcOperationsDependsOnPostProcessor
extends AbstractDependsOnBeanFactoryPostProcessor {
public NamedParameterJdbcOperationsDependsOnPostProcessor(String... dependsOn) {
super(NamedParameterJdbcOperations.class, dependsOn);
}
}

View File

@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDepen
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.NamedParameterJdbcOperationsDependsOnPostProcessor;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
@ -45,6 +46,7 @@ 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.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.util.Assert;
@ -58,6 +60,7 @@ import org.springframework.util.Assert;
* @author Eddú Meléndez
* @author Andy Wilkinson
* @author Dominic Gunn
* @author Dan Zheng
* @since 1.1.0
*/
@Configuration
@ -208,4 +211,20 @@ public class LiquibaseAutoConfiguration {
}
/**
* Additional configuration to ensure that {@link NamedParameterJdbcOperations} beans
* depend on the liquibase bean.
*/
@Configuration
@ConditionalOnClass(NamedParameterJdbcOperations.class)
@ConditionalOnBean(NamedParameterJdbcOperations.class)
protected static class LiquibaseNamedParameterJdbcOperationsDependencyConfiguration
extends NamedParameterJdbcOperationsDependsOnPostProcessor {
public LiquibaseNamedParameterJdbcOperationsDependencyConfiguration() {
super("liquibase");
}
}
}

View File

@ -16,13 +16,27 @@
package org.springframework.boot.autoconfigure.jdbc;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -33,6 +47,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
/**
@ -41,6 +56,7 @@ import static org.mockito.Mockito.mock;
* @author Dave Syer
* @author Stephane Nicoll
* @author Kazuki Shimizu
* @author Dan Zheng
*/
public class JdbcTemplateAutoConfigurationTests {
@ -185,6 +201,52 @@ public class JdbcTemplateAutoConfigurationTests {
});
}
@Test
public void testDependencyToFlywayWithJdbcTemplateMixed() {
this.contextRunner
.withUserConfiguration(NamedParameterDataSourceMigrationValidator.class)
.withPropertyValues("spring.flyway.locations:classpath:db/city_np")
.withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context.getBean(JdbcTemplate.class)).isNotNull();
assertThat(context.getBean(
NamedParameterDataSourceMigrationValidator.class).count)
.isEqualTo(1);
});
}
@Test
public void testDependencyToFlywayWithOnlyNamedParameterJdbcTemplate() {
ApplicationContextRunner contextRunner1 = new ApplicationContextRunner()
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.generate-unique-name=true")
.withConfiguration(
AutoConfigurations.of(DataSourceAutoConfiguration.class,
JdbcTemplateAutoConfiguration.class,
OnlyNamedParameterJdbcTemplateAutoConfiguration.class));
contextRunner1
.withUserConfiguration(NamedParameterDataSourceMigrationValidator.class)
.withPropertyValues("spring.flyway.locations:classpath:db/city_np")
.withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context.containsBean("jdbcTemplate")).isFalse();
try {
JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class);
fail("org.springframework.boot.autoconfigure.jdbc.JdcTemplate should not exist in the application context");
}
catch (NoSuchBeanDefinitionException ex) {
}
assertThat(context.getBean(NamedParameterJdbcTemplate.class))
.isNotNull();
assertThat(context.getBean(
NamedParameterDataSourceMigrationValidator.class).count)
.isEqualTo(1);
});
}
@Test
public void testDependencyToLiquibase() {
this.contextRunner.withUserConfiguration(DataSourceMigrationValidator.class)
@ -199,6 +261,50 @@ public class JdbcTemplateAutoConfigurationTests {
});
}
@Test
public void testDependencyToLiquibaseWithJdbcTemplateMixed() {
this.contextRunner
.withUserConfiguration(NamedParameterDataSourceMigrationValidator.class)
.withPropertyValues(
"spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city-np.yaml")
.withConfiguration(
AutoConfigurations.of(LiquibaseAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context.getBean(JdbcTemplate.class)).isNotNull();
assertThat(context.getBean(
NamedParameterDataSourceMigrationValidator.class).count)
.isEqualTo(1);
});
}
@Test
public void testDependencyToLiquibaseWithOnlyNamedParameterJdbcTemplate() {
this.contextRunner
.withUserConfiguration(NamedParameterDataSourceMigrationValidator.class)
.withPropertyValues(
"spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city-np.yaml")
.withConfiguration(AutoConfigurations.of(
OnlyNamedParameterJdbcTemplateAutoConfiguration.class,
LiquibaseAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context.containsBean("jdbcTemplate")).isFalse();
try {
JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class);
fail("org.springframework.boot.autoconfigure.jdbc.JdcTemplate should not exist in the application context");
}
catch (NoSuchBeanDefinitionException ex) {
}
assertThat(context.getBean(NamedParameterJdbcTemplate.class))
.isNotNull();
assertThat(context.getBean(
NamedParameterDataSourceMigrationValidator.class).count)
.isEqualTo(1);
});
}
@Configuration
static class CustomConfiguration {
@ -278,4 +384,66 @@ public class JdbcTemplateAutoConfigurationTests {
}
static class NamedParameterDataSourceMigrationValidator {
private final Integer count;
NamedParameterDataSourceMigrationValidator(
NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
String sql = "SELECT COUNT(*) from CITY WHERE id = :id";
Map<String, Long> param = new HashMap<>();
param.put("id", 1L);
this.count = namedParameterJdbcTemplate.queryForObject(sql, param,
Integer.class);
}
}
@Configuration
@ConditionalOnClass({ DataSource.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class,
JdbcTemplateAutoConfiguration.class })
@AutoConfigureBefore({ FlywayAutoConfiguration.class,
LiquibaseAutoConfiguration.class })
@EnableConfigurationProperties(JdbcProperties.class)
static class OnlyNamedParameterJdbcTemplateAutoConfiguration
implements BeanDefinitionRegistryPostProcessor {
@Bean
public NamedParameterJdbcTemplate myNamedParameterJdbcTemplate(
DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
// do nothing
}
/**
* <p>
* we should remove the jdbc template bean definition to keep only
* NamedParameterJdbcTemplate is registerd in the bean container
* </p>
* @param registry the bean definition registry.
* @throws BeansException if the bean registry have any exception.
*/
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
throws BeansException {
String[] excludeBeanNames = new String[] { "jdbcTemplate",
"namedParameterJdbcTemplate" };
for (String beanName : excludeBeanNames) {
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
if (beanDefinition != null) {
registry.removeBeanDefinition(beanName);
}
}
}
}
}

View File

@ -0,0 +1,55 @@
databaseChangeLog:
- changeSet:
id: 1
author: dan-zheng
changes:
- createSequence:
sequenceName: hibernate_sequence
- createTable:
tableName: city
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: varchar(50)
constraints:
nullable: false
- column:
name: state
type: varchar(50)
constraints:
nullable: false
- column:
name: country
type: varchar(50)
constraints:
nullable: false
- column:
name: map
type: varchar(50)
constraints:
nullable: true
- insert:
tableName: city
columns:
- column:
name: id
value: 1
- column:
name: name
value: Hangzhou
- column:
name: state
value: Zhejiang
- column:
name: country
value: China
- column:
name: map
value: map

View File

@ -0,0 +1,9 @@
CREATE SEQUENCE HIBERNATE_SEQUENCE;
CREATE TABLE CITY (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
name VARCHAR(30),
state VARCHAR(30),
country VARCHAR(30),
map VARCHAR(30)
);

View File

@ -0,0 +1 @@
INSERT INTO CITY(id, name, state, country, map) VALUES(1, 'Hangzhou', 'Zhejiang', 'China', 'map');