Skip to content

Commit

Permalink
Rework DataSource initialization
Browse files Browse the repository at this point in the history
Previously, DataSource initialization was triggered via a
BeanPostProcessor or a schema created event from JPA. This caused
numerous problems with circular dependencies, bean lifecycle, etc and
added significant complexity.

This commit reworks DataSource initialization to remove the use of a
BeanPostProcessor entirely. In its place, DataSource initialization is
now driven by an InitializingBean with dependency relationships
between beans ensuring that initialization has been performed before
the DataSource is used. This aligns with the approach that's worked
well with Flyway and Liquibase.

More changes are planned to further simplify DataSource initialization.
The changes in this commit are a foundation for those changes. Any new
public API in this commit is highly likely to change before the next
GA.

Fixes gh-13042
Fixes gh-23736
  • Loading branch information
wilkinsona committed Feb 17, 2021
1 parent b3d73a1 commit 2f83a67
Show file tree
Hide file tree
Showing 19 changed files with 539 additions and 538 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2012-2021 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 javax.sql.DataSource;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;

/**
* {@link InitializingBean} that performs {@link DataSource} initialization using DDL and
* DML scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public class DataSourceInitialization implements InitializingBean, ResourceLoaderAware {

private final DataSource dataSource;

private final DataSourceProperties properies;

private volatile ResourceLoader resourceLoader;

/**
* Creates a new {@link DataSourceInitialization} that will initialize the given
* {@code DataSource} using the settings from the given {@code properties}.
* @param dataSource the DataSource to initialize
* @param properies the properties containing the initialization settings
*/
public DataSourceInitialization(DataSource dataSource, DataSourceProperties properies) {
this.dataSource = dataSource;
this.properies = properies;
}

@Override
public void afterPropertiesSet() throws Exception {
new DataSourceInitializer(this.dataSource, this.properies, this.resourceLoader).initializeDataSource();
}

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
Expand All @@ -16,47 +16,82 @@

package org.springframework.boot.autoconfigure.jdbc;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;

/**
* Configures DataSource initialization.
* Configuration for {@link DataSource} initialization using DDL and DML scripts.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
*/
@Configuration(proxyBeanMethods = false)
@Import({ DataSourceInitializerInvoker.class, DataSourceInitializationConfiguration.Registrar.class })
@ConditionalOnSingleCandidate(DataSource.class)
class DataSourceInitializationConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.datasource", name = "initialization-order", havingValue = "before-jpa",
matchIfMissing = true)
@Import({ DataSourceInitializationJdbcOperationsDependsOnPostProcessor.class,
DataSourceInitializationNamedParameterJdbcOperationsDependsOnPostProcessor.class,
DataSourceInitializationEntityManagerFactoryDependsOnPostProcessor.class })
static class BeforeJpaDataSourceInitializationConfiguration {

@Bean
DataSourceInitialization dataSourceInitialization(DataSource dataSource, DataSourceProperties properties) {
return new DataSourceInitialization(dataSource, properties);
}

}

/**
* {@link ImportBeanDefinitionRegistrar} to register the
* {@link DataSourceInitializerPostProcessor} without causing early bean instantiation
* issues.
* Post processor to ensure that {@link EntityManagerFactory} beans depend on any
* {@link DataSourceInitialization} beans.
*/
static class Registrar implements ImportBeanDefinitionRegistrar {

private static final String BEAN_NAME = "dataSourceInitializerPostProcessor";

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (!registry.containsBeanDefinition(BEAN_NAME)) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(DataSourceInitializerPostProcessor.class,
DataSourceInitializerPostProcessor::new)
.getBeanDefinition();
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
// We don't need this one to be post processed otherwise it can cause a
// cascade of bean instantiation that we would rather avoid.
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(BEAN_NAME, beanDefinition);
}
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManagerFactory.class })
static class DataSourceInitializationEntityManagerFactoryDependsOnPostProcessor
extends EntityManagerFactoryDependsOnPostProcessor {

DataSourceInitializationEntityManagerFactoryDependsOnPostProcessor() {
super(DataSourceInitialization.class);
}

}

/**
* Post processor to ensure that {@link JdbcOperations} beans depend on any
* {@link DataSourceInitialization} beans.
*/
@ConditionalOnClass(JdbcOperations.class)
static class DataSourceInitializationJdbcOperationsDependsOnPostProcessor
extends JdbcOperationsDependsOnPostProcessor {

DataSourceInitializationJdbcOperationsDependsOnPostProcessor() {
super(DataSourceInitialization.class);
}

}

/**
* Post processor to ensure that {@link NamedParameterJdbcOperations} beans depend on
* any {@link DataSourceInitialization} beans.
*/
@ConditionalOnClass(NamedParameterJdbcOperations.class)
protected static class DataSourceInitializationNamedParameterJdbcOperationsDependsOnPostProcessor
extends NamedParameterJdbcOperationsDependsOnPostProcessor {

public DataSourceInitializationNamedParameterJdbcOperationsDependsOnPostProcessor() {
super(DataSourceInitialization.class);
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
Expand Down Expand Up @@ -46,8 +46,9 @@
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Kazuki Shimizu
* @since 2.5.0
*/
class DataSourceInitializer {
public class DataSourceInitializer {

private static final Log logger = LogFactory.getLog(DataSourceInitializer.class);

Expand All @@ -64,32 +65,25 @@ class DataSourceInitializer {
* @param properties the matching configuration
* @param resourceLoader the resource loader to use (can be null)
*/
DataSourceInitializer(DataSource dataSource, DataSourceProperties properties, ResourceLoader resourceLoader) {
public DataSourceInitializer(DataSource dataSource, DataSourceProperties properties,
ResourceLoader resourceLoader) {
this.dataSource = dataSource;
this.properties = properties;
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
}

/**
* Create a new instance with the {@link DataSource} to initialize and its matching
* {@link DataSourceProperties configuration}.
* @param dataSource the datasource to initialize
* @param properties the matching configuration
* Initializes the {@link DataSource} by running DDL and DML scripts.
* @return {@code true} if one or more scripts were applied to the database, otherwise
* {@code false}
*/
DataSourceInitializer(DataSource dataSource, DataSourceProperties properties) {
this(dataSource, properties, null);
}

DataSource getDataSource() {
return this.dataSource;
public boolean initializeDataSource() {
boolean initialized = createSchema();
initialized = initSchema() && initialized;
return initialized;
}

/**
* Create the schema if necessary.
* @return {@code true} if the schema was created
* @see DataSourceProperties#getSchema()
*/
boolean createSchema() {
private boolean createSchema() {
List<Resource> scripts = getScripts("spring.datasource.schema", this.properties.getSchema(), "schema");
if (!scripts.isEmpty()) {
if (!isEnabled()) {
Expand All @@ -103,21 +97,18 @@ boolean createSchema() {
return !scripts.isEmpty();
}

/**
* Initialize the schema if necessary.
* @see DataSourceProperties#getData()
*/
void initSchema() {
private boolean initSchema() {
List<Resource> scripts = getScripts("spring.datasource.data", this.properties.getData(), "data");
if (!scripts.isEmpty()) {
if (!isEnabled()) {
logger.debug("Initialization disabled (not running data scripts)");
return;
return false;
}
String username = this.properties.getDataUsername();
String password = this.properties.getDataPassword();
runScripts(scripts, username, password);
}
return !scripts.isEmpty();
}

private boolean isEnabled() {
Expand Down

This file was deleted.

Loading

0 comments on commit 2f83a67

Please sign in to comment.