Commit 8656512b authored by Stephane Nicoll's avatar Stephane Nicoll

Move DataSourceInitializer lifecycle

This commit separates the lifecycle of the datasource initialization
from DataSourceInitializer itself. It also makes sure that a @Primary
data source is no longer required.

Closes gh-10502
parent 89992ea4
......@@ -28,13 +28,10 @@ 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.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerPostProcessor.Registrar;
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
......@@ -56,16 +53,10 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSourceInitializer dataSourceInitializer(DataSourceProperties properties,
ApplicationContext applicationContext) {
return new DataSourceInitializer(properties, applicationContext);
}
@Configuration
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
......
/*
* Copyright 2012-2017 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.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
/**
* Configures DataSource initialization.
*
* @author Stephane Nicoll
*/
@Configuration
@Import({ DataSourceInitializerInvoker.class, DataSourceInitializationConfiguration.Registrar.class })
class DataSourceInitializationConfiguration {
/**
* {@link ImportBeanDefinitionRegistrar} to register the
* {@link DataSourceInitializerPostProcessor} without causing early bean instantiation
* issues.
*/
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)) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DataSourceInitializerPostProcessor.class);
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);
}
}
}
}
......@@ -20,7 +20,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
......@@ -28,104 +27,100 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.config.ResourceNotFoundException;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.config.SortedResourcesFactoryBean;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.util.StringUtils;
/**
* Bean to handle {@link DataSource} initialization by running {@literal schema-*.sql} on
* {@link PostConstruct} and {@literal data-*.sql} SQL scripts on a
* {@link DataSourceInitializedEvent}.
* Initialize a {@link DataSource} based on a matching {@link DataSourceProperties}
* config.
*
* @author Dave Syer
* @author Phillip Webb
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Kazuki Shimizu
* @see DataSourceAutoConfiguration
*/
class DataSourceInitializer implements ApplicationListener<DataSourceInitializedEvent> {
class DataSourceInitializer {
private static final Log logger = LogFactory.getLog(DataSourceInitializer.class);
private final DataSourceProperties properties;
private final DataSource dataSource;
private final ApplicationContext applicationContext;
private final DataSourceProperties properties;
private DataSource dataSource;
private final ResourceLoader resourceLoader;
/**
* 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
* @param resourceLoader the resource loader to use (can be null)
*/
DataSourceInitializer(DataSource dataSource, DataSourceProperties properties,
ResourceLoader resourceLoader) {
this.dataSource = dataSource;
this.properties = properties;
this.resourceLoader = (resourceLoader != null ? resourceLoader
: new DefaultResourceLoader());
}
private boolean initialized = false;
/**
* 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
*/
DataSourceInitializer(DataSource dataSource, DataSourceProperties properties) {
this(dataSource, properties, null);
}
DataSourceInitializer(DataSourceProperties properties,
ApplicationContext applicationContext) {
this.properties = properties;
this.applicationContext = applicationContext;
public DataSource getDataSource() {
return this.dataSource;
}
@PostConstruct
public void init() {
/**
* Create the schema if necessary.
* @return {@code true} if the schema was created
* @see DataSourceProperties#getSchema()
*/
public boolean createSchema() {
if (!this.properties.isInitialize()) {
logger.debug("Initialization disabled (not running DDL scripts)");
return;
}
if (this.applicationContext.getBeanNamesForType(DataSource.class, false,
false).length > 0) {
this.dataSource = this.applicationContext.getBean(DataSource.class);
return false;
}
if (this.dataSource == null) {
logger.debug("No DataSource found so not initializing");
return;
}
runSchemaScripts();
}
private void runSchemaScripts() {
List<Resource> scripts = getScripts("spring.datasource.schema",
this.properties.getSchema(), "schema");
if (!scripts.isEmpty()) {
String username = this.properties.getSchemaUsername();
String password = this.properties.getSchemaPassword();
runScripts(scripts, username, password);
try {
this.applicationContext
.publishEvent(new DataSourceInitializedEvent(this.dataSource));
// The listener might not be registered yet, so don't rely on it.
if (!this.initialized) {
runDataScripts();
this.initialized = true;
}
}
catch (IllegalStateException ex) {
logger.warn("Could not send event to complete DataSource initialization ("
+ ex.getMessage() + ")");
}
}
return !scripts.isEmpty();
}
@Override
public void onApplicationEvent(DataSourceInitializedEvent event) {
/**
* Initialize the schema if necessary.
* @see DataSourceProperties#getData()
*/
public void initSchema() {
if (!this.properties.isInitialize()) {
logger.debug("Initialization disabled (not running data scripts)");
return;
}
// NOTE the event can happen more than once and
// the event datasource is not used here
if (!this.initialized) {
runDataScripts();
this.initialized = true;
}
}
private void runDataScripts() {
List<Resource> scripts = getScripts("spring.datasource.data",
this.properties.getData(), "data");
String username = this.properties.getDataUsername();
String password = this.properties.getDataPassword();
runScripts(scripts, username, password);
if (!scripts.isEmpty()) {
String username = this.properties.getDataUsername();
String password = this.properties.getDataPassword();
runScripts(scripts, username, password);
}
}
private List<Resource> getScripts(String propertyName, List<String> resources,
......@@ -159,7 +154,7 @@ class DataSourceInitializer implements ApplicationListener<DataSourceInitialized
private Resource[] doGetResources(String location) {
try {
SortedResourcesFactoryBean factory = new SortedResourcesFactoryBean(
this.applicationContext, Collections.singletonList(location));
this.resourceLoader, Collections.singletonList(location));
factory.afterPropertiesSet();
return factory.getObject();
}
......
/*
* Copyright 2012-2017 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 javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
/**
* Bean to handle {@link DataSource} initialization by running {@literal schema-*.sql} on
* {@link InitializingBean#afterPropertiesSet()} and {@literal data-*.sql} SQL scripts on
* a {@link DataSourceInitializedEvent}.
*
* @author Stephane Nicoll
* @see DataSourceAutoConfiguration
*/
class DataSourceInitializerInvoker
implements ApplicationListener<DataSourceInitializedEvent>, InitializingBean {
private static final Log logger = LogFactory.getLog(DataSourceInitializerInvoker.class);
private final ObjectProvider<DataSource> dataSource;
private final DataSourceProperties properties;
private final ApplicationContext applicationContext;
private DataSourceInitializer dataSourceInitializer;
private boolean initialized;
DataSourceInitializerInvoker(ObjectProvider<DataSource> dataSource,
DataSourceProperties properties,
ApplicationContext applicationContext) {
this.dataSource = dataSource;
this.properties = properties;
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() {
DataSourceInitializer initializer = getDataSourceInitializer();
if (initializer != null) {
boolean schemaCreated = this.dataSourceInitializer.createSchema();
if (schemaCreated) {
try {
this.applicationContext
.publishEvent(new DataSourceInitializedEvent(
initializer.getDataSource()));
// The listener might not be registered yet, so don't rely on it.
if (!this.initialized) {
this.dataSourceInitializer.initSchema();
this.initialized = true;
}
}
catch (IllegalStateException ex) {
logger.warn("Could not send event to complete DataSource initialization ("
+ ex.getMessage() + ")");
}
}
}
}
@Override
public void onApplicationEvent(DataSourceInitializedEvent event) {
// NOTE the event can happen more than once and
// the event datasource is not used here
DataSourceInitializer initializer = getDataSourceInitializer();
if (!this.initialized && initializer != null) {
initializer.initSchema();
this.initialized = true;
}
}
private DataSourceInitializer getDataSourceInitializer() {
if (this.dataSourceInitializer == null) {
DataSource ds = this.dataSource.getIfUnique();
if (ds != null) {
this.dataSourceInitializer = new DataSourceInitializer(ds,
this.properties, this.applicationContext);
}
}
return this.dataSourceInitializer;
}
}
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2017 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.
......@@ -21,17 +21,12 @@ import javax.sql.DataSource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata;
/**
* {@link BeanPostProcessor} used to fire {@link DataSourceInitializedEvent}s. Should only
* be registered via the inner {@link Registrar} class.
* {@link BeanPostProcessor} used to ensure that {@link DataSourceInitializer} is
* initialized as soon as a {@link DataSource} is.
*
* @author Dave Syer
* @since 1.1.2
......@@ -59,34 +54,9 @@ class DataSourceInitializerPostProcessor implements BeanPostProcessor, Ordered {
throws BeansException {
if (bean instanceof DataSource) {
// force initialization of this bean as soon as we see a DataSource
this.beanFactory.getBean(DataSourceInitializer.class);
this.beanFactory.getBean(DataSourceInitializerInvoker.class);
}
return bean;
}
/**
* {@link ImportBeanDefinitionRegistrar} to register the
* {@link DataSourceInitializerPostProcessor} without causing early bean instantiation
* issues.
*/
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)) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DataSourceInitializerPostProcessor.class);
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);
}
}
}
}
......@@ -28,7 +28,6 @@ import com.zaxxer.hikari.HikariDataSource;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.apache.tomcat.jdbc.pool.DataSourceProxy;
import org.apache.tomcat.jdbc.pool.jmx.ConnectionPool;
import org.hsqldb.jdbc.JDBCDriver;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
......@@ -38,9 +37,6 @@ import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jmx.export.UnableToRegisterMBeanException;
import static org.assertj.core.api.Assertions.assertThat;
......@@ -63,13 +59,6 @@ public class DataSourceJmxConfigurationTests {
}
}
@Test
public void hikariCustomCannotUseRegisterMBeansByDefault() {
this.thrown.expect(UnableToRegisterMBeanException.class);
this.thrown.expectMessage("myDataSource");
load(CustomHikariConfiguration.class);
}
@Test
public void hikariAutoConfiguredCanUseRegisterMBeans()
throws MalformedObjectNameException {
......@@ -140,18 +129,4 @@ public class DataSourceJmxConfigurationTests {
this.context = context;
}
@Configuration
static class CustomHikariConfiguration {
@Bean
public HikariDataSource myDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:hsqldb:mem:custom");
dataSource.setDriverClassName(JDBCDriver.class.getName());
dataSource.setRegisterMbeans(true);
return dataSource;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment