diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java new file mode 100644 index 00000000000..6bcd2e91523 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java index 9da2bca5938..2273d76a345 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java @@ -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. @@ -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); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java index 86c3baa1b9a..ef8fe986a65 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java @@ -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. @@ -46,8 +46,9 @@ import org.springframework.util.StringUtils; * @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); @@ -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 scripts = getScripts("spring.datasource.schema", this.properties.getSchema(), "schema"); if (!scripts.isEmpty()) { if (!isEnabled()) { @@ -103,21 +97,18 @@ class DataSourceInitializer { return !scripts.isEmpty(); } - /** - * Initialize the schema if necessary. - * @see DataSourceProperties#getData() - */ - void initSchema() { + private boolean initSchema() { List 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() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvoker.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvoker.java deleted file mode 100644 index a6ad430c211..00000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvoker.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 - * - * 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.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; -import org.springframework.core.log.LogMessage; - -/** - * Bean to handle {@link DataSource} initialization by running {@literal schema-*.sql} on - * {@link InitializingBean#afterPropertiesSet()} and {@literal data-*.sql} SQL scripts on - * a {@link DataSourceSchemaCreatedEvent}. - * - * @author Stephane Nicoll - * @see DataSourceAutoConfiguration - */ -class DataSourceInitializerInvoker implements ApplicationListener, InitializingBean { - - private static final Log logger = LogFactory.getLog(DataSourceInitializerInvoker.class); - - private final ObjectProvider dataSource; - - private final DataSourceProperties properties; - - private final ApplicationContext applicationContext; - - private DataSourceInitializer dataSourceInitializer; - - private boolean initialized; - - DataSourceInitializerInvoker(ObjectProvider 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) { - initialize(initializer); - } - } - } - - private void initialize(DataSourceInitializer initializer) { - try { - this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(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(LogMessage.format("Could not send event to complete DataSource initialization (%s)", - ex.getMessage())); - } - } - - @Override - public void onApplicationEvent(DataSourceSchemaCreatedEvent 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; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerPostProcessor.java deleted file mode 100644 index 62f97377799..00000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerPostProcessor.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2012-2020 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.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.core.Ordered; - -/** - * {@link BeanPostProcessor} used to ensure that {@link DataSourceInitializer} is - * initialized as soon as a {@link DataSource} is. - * - * @author Dave Syer - */ -class DataSourceInitializerPostProcessor implements BeanPostProcessor, Ordered, BeanFactoryAware { - - @Override - public int getOrder() { - return Ordered.HIGHEST_PRECEDENCE + 1; - } - - private BeanFactory beanFactory; - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof DataSource) { - // force initialization of this bean as soon as we see a DataSource - this.beanFactory.getBean(DataSourceInitializerInvoker.class); - } - return bean; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceSchemaCreatedEvent.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceSchemaCreatedEvent.java index 8da579a3e5a..ac23a0017db 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceSchemaCreatedEvent.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceSchemaCreatedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 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. @@ -28,8 +28,10 @@ import org.springframework.context.ApplicationEvent; * @author Dave Syer * @author Stephane Nicoll * @since 2.0.0 + * @deprecated since 2.5.0 with no replacement as the event is no longer published */ @SuppressWarnings("serial") +@Deprecated public class DataSourceSchemaCreatedEvent extends ApplicationEvent { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DependsOnDataSourceInitialization.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DependsOnDataSourceInitialization.java new file mode 100644 index 00000000000..81f7a7157bb --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DependsOnDataSourceInitialization.java @@ -0,0 +1,43 @@ +/* + * 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 java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.sql.DataSource; + +import org.springframework.context.annotation.DependsOn; + +/** + * Annotation used to indicate that a bean depends upon the auto-configured DataSource + * initialization having completed before the {@link DataSource} is injected. If the + * DataSource is not used during startup, no such dependency is required. + * + * @author Andy Wilkinson + * @since 2.5.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@DependsOn("dataSourceInitialization") +public @interface DependsOnDataSourceInitialization { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java deleted file mode 100644 index b935b6d0974..00000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright 2012-2020 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.orm.jpa; - -import java.util.Map; -import java.util.concurrent.Future; -import java.util.function.Supplier; - -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.spi.PersistenceProvider; -import javax.persistence.spi.PersistenceUnitInfo; -import javax.sql.DataSource; - -import org.springframework.beans.BeansException; -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.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.boot.autoconfigure.jdbc.DataSourceSchemaCreatedEvent; -import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.core.Ordered; -import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.orm.jpa.JpaDialect; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; - -/** - * {@link BeanPostProcessor} used to fire {@link DataSourceSchemaCreatedEvent}s. Should - * only be registered via the inner {@link Registrar} class. - * - * @author Dave Syer - */ -class DataSourceInitializedPublisher implements BeanPostProcessor { - - @Autowired - private ApplicationContext applicationContext; - - private DataSource dataSource; - - private JpaProperties jpaProperties; - - private HibernateProperties hibernateProperties; - - private DataSourceSchemaCreatedPublisher schemaCreatedPublisher; - - private DataSourceInitializationCompletionListener initializationCompletionListener; - - DataSourceInitializedPublisher(DataSourceInitializationCompletionListener completionListener) { - this.initializationCompletionListener = completionListener; - } - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof LocalContainerEntityManagerFactoryBean) { - LocalContainerEntityManagerFactoryBean factory = (LocalContainerEntityManagerFactoryBean) bean; - if (factory.getBootstrapExecutor() != null && factory.getJpaVendorAdapter() != null) { - this.schemaCreatedPublisher = new DataSourceSchemaCreatedPublisher(factory); - factory.setJpaVendorAdapter(this.schemaCreatedPublisher); - } - } - return bean; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof DataSource) { - // Normally this will be the right DataSource - this.dataSource = (DataSource) bean; - } - if (bean instanceof JpaProperties) { - this.jpaProperties = (JpaProperties) bean; - } - if (bean instanceof HibernateProperties) { - this.hibernateProperties = (HibernateProperties) bean; - } - if (bean instanceof LocalContainerEntityManagerFactoryBean && this.schemaCreatedPublisher == null) { - LocalContainerEntityManagerFactoryBean factoryBean = (LocalContainerEntityManagerFactoryBean) bean; - EntityManagerFactory entityManagerFactory = factoryBean.getNativeEntityManagerFactory(); - publishEventIfRequired(factoryBean, entityManagerFactory); - } - return bean; - } - - private void publishEventIfRequired(LocalContainerEntityManagerFactoryBean factoryBean, - EntityManagerFactory entityManagerFactory) { - DataSource dataSource = findDataSource(factoryBean, entityManagerFactory); - if (dataSource != null && isInitializingDatabase(dataSource)) { - this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(dataSource)); - } - } - - private DataSource findDataSource(LocalContainerEntityManagerFactoryBean factoryBean, - EntityManagerFactory entityManagerFactory) { - Object dataSource = entityManagerFactory.getProperties().get("javax.persistence.nonJtaDataSource"); - if (dataSource == null) { - dataSource = factoryBean.getPersistenceUnitInfo().getNonJtaDataSource(); - } - return (dataSource instanceof DataSource) ? (DataSource) dataSource : this.dataSource; - } - - private boolean isInitializingDatabase(DataSource dataSource) { - if (this.jpaProperties == null || this.hibernateProperties == null) { - return true; // better safe than sorry - } - Supplier defaultDdlAuto = () -> (EmbeddedDatabaseConnection.isEmbedded(dataSource) ? "create-drop" - : "none"); - Map hibernate = this.hibernateProperties.determineHibernateProperties( - this.jpaProperties.getProperties(), new HibernateSettings().ddlAuto(defaultDdlAuto)); - return hibernate.containsKey("hibernate.hbm2ddl.auto"); - } - - /** - * {@link ApplicationListener} that, upon receiving {@link ContextRefreshedEvent}, - * blocks until any asynchronous DataSource initialization has completed. - */ - static class DataSourceInitializationCompletionListener - implements ApplicationListener, Ordered, ApplicationContextAware { - - private volatile ApplicationContext applicationContext; - - private volatile Future dataSourceInitialization; - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - if (!event.getApplicationContext().equals(this.applicationContext)) { - return; - } - Future dataSourceInitialization = this.dataSourceInitialization; - if (dataSourceInitialization != null) { - try { - dataSourceInitialization.get(); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - } - - @Override - public int getOrder() { - return Ordered.HIGHEST_PRECEDENCE; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - } - - /** - * {@link ImportBeanDefinitionRegistrar} to register the - * {@link DataSourceInitializedPublisher} without causing early bean instantiation - * issues. - */ - static class Registrar implements ImportBeanDefinitionRegistrar { - - private static final String PUBLISHER_BEAN_NAME = "dataSourceInitializedPublisher"; - - private static final String COMPLETION_LISTENER_BEAN_BEAN = DataSourceInitializationCompletionListener.class - .getName(); - - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, - BeanDefinitionRegistry registry) { - if (!registry.containsBeanDefinition(PUBLISHER_BEAN_NAME)) { - DataSourceInitializationCompletionListener completionListener = new DataSourceInitializationCompletionListener(); - DataSourceInitializedPublisher publisher = new DataSourceInitializedPublisher(completionListener); - AbstractBeanDefinition publisherDefinition = BeanDefinitionBuilder - .genericBeanDefinition(DataSourceInitializedPublisher.class, () -> publisher) - .getBeanDefinition(); - publisherDefinition.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. - publisherDefinition.setSynthetic(true); - registry.registerBeanDefinition(PUBLISHER_BEAN_NAME, publisherDefinition); - AbstractBeanDefinition listenerDefinition = BeanDefinitionBuilder.genericBeanDefinition( - DataSourceInitializationCompletionListener.class, () -> completionListener).getBeanDefinition(); - listenerDefinition.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. - listenerDefinition.setSynthetic(true); - registry.registerBeanDefinition(COMPLETION_LISTENER_BEAN_BEAN, listenerDefinition); - } - } - - } - - final class DataSourceSchemaCreatedPublisher implements JpaVendorAdapter { - - private final LocalContainerEntityManagerFactoryBean factoryBean; - - private final JpaVendorAdapter delegate; - - private DataSourceSchemaCreatedPublisher(LocalContainerEntityManagerFactoryBean factoryBean) { - this.factoryBean = factoryBean; - this.delegate = factoryBean.getJpaVendorAdapter(); - } - - @Override - public PersistenceProvider getPersistenceProvider() { - return this.delegate.getPersistenceProvider(); - } - - @Override - public String getPersistenceProviderRootPackage() { - return this.delegate.getPersistenceProviderRootPackage(); - } - - @Override - public Map getJpaPropertyMap(PersistenceUnitInfo pui) { - return this.delegate.getJpaPropertyMap(pui); - } - - @Override - public Map getJpaPropertyMap() { - return this.delegate.getJpaPropertyMap(); - } - - @Override - public JpaDialect getJpaDialect() { - return this.delegate.getJpaDialect(); - } - - @Override - public Class getEntityManagerFactoryInterface() { - return this.delegate.getEntityManagerFactoryInterface(); - } - - @Override - public Class getEntityManagerInterface() { - return this.delegate.getEntityManagerInterface(); - } - - @Override - public void postProcessEntityManagerFactory(EntityManagerFactory entityManagerFactory) { - this.delegate.postProcessEntityManagerFactory(entityManagerFactory); - AsyncTaskExecutor bootstrapExecutor = this.factoryBean.getBootstrapExecutor(); - if (bootstrapExecutor != null) { - DataSourceInitializedPublisher.this.initializationCompletionListener.dataSourceInitialization = bootstrapExecutor - .submit(() -> DataSourceInitializedPublisher.this.publishEventIfRequired(this.factoryBean, - entityManagerFactory)); - } - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java index ab4f1c6bff5..e36a1b4fc65 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 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. @@ -17,16 +17,23 @@ package org.springframework.boot.autoconfigure.orm.jpa; import javax.persistence.EntityManager; +import javax.sql.DataSource; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.engine.spi.SessionImplementor; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.io.ResourceLoader; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; /** @@ -45,4 +52,18 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @Import(HibernateJpaConfiguration.class) public class HibernateJpaAutoConfiguration { + @ConditionalOnProperty(prefix = "spring.datasource", name = "initialization-order", havingValue = "after-jpa", + matchIfMissing = false) + static class AfterJpaDataSourceInitializationConfiguration { + + @Bean + HibernatePropertiesCustomizer dataSourceInitializationCustomizer(DataSource dataSource, + DataSourceProperties properties, ResourceLoader resourceLoader) { + return (hibernateProperties) -> hibernateProperties.put(AvailableSettings.SCHEMA_MANAGEMENT_TOOL, + new SpringBootSchemaManagementTool( + new DataSourceInitializer(dataSource, properties, resourceLoader))); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java index 836d61a3862..8526773988c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java @@ -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. @@ -44,7 +44,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; @@ -74,7 +73,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(JpaProperties.class) -@Import(DataSourceInitializedPublisher.Registrar.class) public abstract class JpaBaseConfiguration implements BeanFactoryAware { private final DataSource dataSource; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/SpringBootSchemaCreator.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/SpringBootSchemaCreator.java new file mode 100644 index 00000000000..29bef57c39f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/SpringBootSchemaCreator.java @@ -0,0 +1,87 @@ +/* + * 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.orm.jpa; + +import org.hibernate.boot.Metadata; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.resource.transaction.spi.DdlTransactionIsolator; +import org.hibernate.tool.schema.TargetType; +import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool; +import org.hibernate.tool.schema.internal.exec.GenerationTarget; +import org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase; +import org.hibernate.tool.schema.internal.exec.JdbcContext; +import org.hibernate.tool.schema.spi.ExecutionOptions; +import org.hibernate.tool.schema.spi.SchemaCreator; +import org.hibernate.tool.schema.spi.SourceDescriptor; +import org.hibernate.tool.schema.spi.TargetDescriptor; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer; + +/** + * Spring Boot {@link SchemaCreator}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class SpringBootSchemaCreator implements SchemaCreator { + + private static final CoreMessageLogger log = CoreLogging.messageLogger(SpringBootSchemaCreator.class); + + private final HibernateSchemaManagementTool tool; + + private final DataSourceInitializer dataSourceInitializer; + + private final SchemaCreator creator; + + SpringBootSchemaCreator(HibernateSchemaManagementTool tool, SchemaCreator creator, + DataSourceInitializer dataSourceInitializer) { + this.tool = tool; + this.creator = creator; + this.dataSourceInitializer = dataSourceInitializer; + } + + @Override + public void doCreation(Metadata metadata, ExecutionOptions options, SourceDescriptor sourceDescriptor, + TargetDescriptor targetDescriptor) { + if (!targetDescriptor.getTargetTypes().contains(TargetType.DATABASE)) { + this.creator.doCreation(metadata, options, sourceDescriptor, targetDescriptor); + return; + } + GenerationTarget databaseTarget = getDatabaseTarget(options, targetDescriptor); + databaseTarget.prepare(); + try { + this.creator.doCreation(metadata, options, sourceDescriptor, targetDescriptor); + this.dataSourceInitializer.initializeDataSource(); + } + finally { + try { + databaseTarget.release(); + } + catch (Exception ex) { + log.debugf("Problem releasing GenerationTarget [%s] : %s", databaseTarget, ex.getMessage()); + } + } + } + + private GenerationTarget getDatabaseTarget(ExecutionOptions options, TargetDescriptor targetDescriptor) { + JdbcContext jdbcContext = this.tool.resolveJdbcContext(options.getConfigurationValues()); + DdlTransactionIsolator ddlTransactionIsolator = this.tool.getDdlTransactionIsolator(jdbcContext); + return new GenerationTargetToDatabase(ddlTransactionIsolator); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/SpringBootSchemaManagementTool.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/SpringBootSchemaManagementTool.java new file mode 100644 index 00000000000..4da61af3d10 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/SpringBootSchemaManagementTool.java @@ -0,0 +1,48 @@ +/* + * 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.orm.jpa; + +import java.util.Map; + +import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool; +import org.hibernate.tool.schema.spi.SchemaCreator; +import org.hibernate.tool.schema.spi.SchemaManagementTool; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer; + +/** + * Spring Boot {@link SchemaManagementTool}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class SpringBootSchemaManagementTool extends HibernateSchemaManagementTool { + + private final DataSourceInitializer dataSourceInitializer; + + SpringBootSchemaManagementTool(DataSourceInitializer dataSourceInitializer) { + this.dataSourceInitializer = dataSourceInitializer; + } + + @Override + @SuppressWarnings("rawtypes") + public SchemaCreator getSchemaCreator(Map options) { + SchemaCreator creator = super.getSchemaCreator(options); + return new SpringBootSchemaCreator(this, creator, this.dataSourceInitializer); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index cb6fa6771e8..ba8527ebbae 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -756,6 +756,10 @@ "name": "spring.datasource.initialization-mode", "defaultValue": "embedded" }, + { + "name": "spring.datasource.initialization-order", + "defaultValue": "before-jpa" + }, { "name": "spring.datasource.jmx-enabled", "type": "java.lang.Boolean", @@ -1901,6 +1905,17 @@ } ] }, + { + "name": "spring.datasource.initialization-order", + "values": [ + { + "value": "before-jpa" + }, + { + "value": "after-jpa" + } + ] + }, { "name": "spring.datasource.schema", "providers": [ 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 4c0fd37f75a..f1612febef0 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 @@ -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. @@ -274,6 +274,7 @@ class DataSourceAutoConfigurationTests { } @Configuration(proxyBeanMethods = false) + @DependsOnDataSourceInitialization static class TestInitializedDataSourceConfiguration { private boolean called; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvokerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java similarity index 73% rename from spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvokerTests.java rename to spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java index d16de5db599..1e3e3a013c4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvokerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java @@ -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. @@ -17,25 +17,36 @@ package org.springframework.boot.autoconfigure.jdbc; import java.io.IOException; +import java.io.PrintWriter; +import java.sql.Connection; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.util.Arrays; import java.util.Comparator; import java.util.Random; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; import org.junit.jupiter.api.Test; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.support.SimpleThreadScope; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -50,12 +61,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Tests for {@link DataSourceInitializerInvoker}. + * Integration tests for DataSource initialization. * * @author Dave Syer * @author Stephane Nicoll */ -class DataSourceInitializerInvokerTests { +class DataSourceInitializationIntegrationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) @@ -151,7 +162,6 @@ class DataSourceInitializerInvokerTests { return (context) -> { assertThat(context).hasSingleBean(DataSource.class); DataSource dataSource = context.getBean(DataSource.class); - context.publishEvent(new DataSourceSchemaCreatedEvent(dataSource)); assertDataSourceNotInitialized(dataSource); }; } @@ -238,6 +248,31 @@ class DataSourceInitializerInvokerTests { }); } + @Test + void whenDataSourceIsProxiedByABeanPostProcessorThenDataSourceInitializationUsesTheProxy() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always") + .withUserConfiguration(DataSourceProxyConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(DataSource.class); + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceProxy.class); + assertThat(((DataSourceProxy) dataSource).connectionsRetrieved).hasPositiveValue(); + assertDataSourceIsInitialized(dataSource); + }); + } + + @Test + // gh-13042 + void whenDataSourceIsScopedAndJpaIsInvolvedThenInitializationCompletesSuccessfully() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always") + .withConfiguration(AutoConfigurations.of(HibernateJpaAutoConfiguration.class)) + .withUserConfiguration(ScopedDataSourceConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(DataSource.class); + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + assertDataSourceIsInitialized(dataSource); + }); + } + private String getRelativeLocationFor(String resource) { return ClassUtils.addResourcePathToPackagePath(getClass(), resource); } @@ -293,4 +328,99 @@ class DataSourceInitializerInvokerTests { } + @Configuration(proxyBeanMethods = true) + static class DataSourceProxyConfiguration { + + @Bean + static BeanPostProcessor dataSourceProxy() { + return new BeanPostProcessor() { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof DataSource) { + return new DataSourceProxy((DataSource) bean); + } + return bean; + } + + }; + } + + } + + static class DataSourceProxy implements DataSource { + + private final AtomicInteger connectionsRetrieved = new AtomicInteger(); + + private final DataSource delegate; + + DataSourceProxy(DataSource delegate) { + this.delegate = delegate; + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return this.delegate.getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + this.delegate.setLogWriter(out); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return this.delegate.isWrapperFor(iface); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return this.delegate.unwrap(iface); + } + + @Override + public Connection getConnection() throws SQLException { + this.connectionsRetrieved.incrementAndGet(); + return this.delegate.getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + this.connectionsRetrieved.incrementAndGet(); + return this.delegate.getConnection(username, password); + } + + @Override + public int getLoginTimeout() throws SQLException { + return this.delegate.getLoginTimeout(); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + this.delegate.setLoginTimeout(seconds); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return this.delegate.getParentLogger(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ScopedDataSourceConfiguration { + + @Bean + static BeanFactoryPostProcessor fooScope() { + return (beanFactory) -> beanFactory.registerScope("test", new SimpleThreadScope()); + } + + @Bean + @Scope("test") + HikariDataSource dataSource(DataSourceProperties properties) { + return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java index a9e8d212cd6..4fd27fa7214 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 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. @@ -33,6 +33,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; /** @@ -45,11 +46,9 @@ class DataSourceInitializerTests { @Test void initializeEmbeddedByDefault() { try (HikariDataSource dataSource = createDataSource()) { - DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties()); + DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties(), null); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); - assertThat(initializer.createSchema()).isTrue(); - assertNumberOfRows(jdbcTemplate, 0); - initializer.initSchema(); + initializer.initializeDataSource(); assertNumberOfRows(jdbcTemplate, 1); } } @@ -59,11 +58,9 @@ class DataSourceInitializerTests { try (HikariDataSource dataSource = createDataSource()) { DataSourceProperties properties = new DataSourceProperties(); properties.setInitializationMode(DataSourceInitializationMode.ALWAYS); - DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties); + DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties, null); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); - assertThat(initializer.createSchema()).isTrue(); - assertNumberOfRows(jdbcTemplate, 0); - initializer.initSchema(); + initializer.initializeDataSource(); assertNumberOfRows(jdbcTemplate, 1); } } @@ -77,8 +74,8 @@ class DataSourceInitializerTests { try (HikariDataSource dataSource = createDataSource()) { DataSourceProperties properties = new DataSourceProperties(); properties.setInitializationMode(DataSourceInitializationMode.NEVER); - DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties); - assertThat(initializer.createSchema()).isFalse(); + DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties, null); + assertThat(initializer.initializeDataSource()).isFalse(); } } @@ -90,9 +87,9 @@ class DataSourceInitializerTests { given(connection.getMetaData()).willReturn(metadata); DataSource dataSource = mock(DataSource.class); given(dataSource.getConnection()).willReturn(connection); - DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties()); - assertThat(initializer.createSchema()).isFalse(); - verify(dataSource).getConnection(); + DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties(), null); + assertThat(initializer.initializeDataSource()).isFalse(); + verify(dataSource, times(2)).getConnection(); } private HikariDataSource createDataSource() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index 484a4677873..09be7865e77 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -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. @@ -19,7 +19,6 @@ package org.springframework.boot.autoconfigure.orm.jpa; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -28,7 +27,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; -import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; @@ -39,7 +37,6 @@ import javax.transaction.TransactionManager; import javax.transaction.UserTransaction; import com.zaxxer.hikari.HikariDataSource; -import org.awaitility.Awaitility; import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy; import org.hibernate.cfg.AvailableSettings; @@ -55,14 +52,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceSchemaCreatedEvent; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener; import org.springframework.boot.autoconfigure.orm.jpa.mapping.NonAnnotatedEntity; import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration; +import org.springframework.boot.logging.LogLevel; import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy; import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy; @@ -72,6 +70,7 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -79,8 +78,8 @@ import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.entry; -import static org.hamcrest.Matchers.hasSize; import static org.mockito.Mockito.mock; /** @@ -124,7 +123,8 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class) .withClassLoader(new HideDataScriptClassLoader()) .withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop", - "spring.datasource.data:classpath:/city.sql") + "spring.datasource.data:classpath:/city.sql", + "spring.datasource.initialization-order=after-jpa") .run((context) -> assertThat(context.getBean(TestInitializedJpaConfiguration.class).called).isTrue()); } @@ -286,7 +286,8 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes void customResourceMapping() { contextRunner().withClassLoader(new HideDataScriptClassLoader()) .withPropertyValues("spring.datasource.data:classpath:/db/non-annotated-data.sql", - "spring.jpa.mapping-resources=META-INF/mappings/non-annotated.xml") + "spring.jpa.mapping-resources=META-INF/mappings/non-annotated.xml", + "spring.datasource.initialization-order=after-jpa") .run((context) -> { EntityManager em = context.getBean(EntityManagerFactory.class).createEntityManager(); NonAnnotatedEntity found = em.find(NonAnnotatedEntity.class, 2000L); @@ -355,9 +356,12 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes @Test void eventListenerCanBeRegisteredAsBeans() { - contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class) - .withClassLoader(new HideDataScriptClassLoader()).withPropertyValues("spring.jpa.show-sql=true", - "spring.jpa.hibernate.ddl-auto:create-drop", "spring.datasource.data:classpath:/city.sql") + contextRunner().withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)) + .withUserConfiguration(TestInitializedJpaConfiguration.class) + .withClassLoader(new HideDataScriptClassLoader()) + .withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop", + "spring.datasource.data:classpath:/city.sql", + "spring.datasource.initialization-order=after-jpa") .run((context) -> { // See CityListener assertThat(context).hasSingleBean(City.class); @@ -376,8 +380,10 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes contextRunner().withUserConfiguration(JpaUsingApplicationListenerConfiguration.class) .withPropertyValues("spring.datasource.initialization-mode=never").run((context) -> { assertThat(context).hasNotFailed(); - assertThat(context.getBean(EventCapturingApplicationListener.class).events.stream() - .filter(DataSourceSchemaCreatedEvent.class::isInstance)).hasSize(1); + EventCapturingApplicationListener listener = context + .getBean(EventCapturingApplicationListener.class); + assertThat(listener.events).hasSize(1); + assertThat(listener.events).hasOnlyElementsOfType(ContextRefreshedEvent.class); }); } @@ -390,8 +396,11 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes assertThat(context).hasNotFailed(); EventCapturingApplicationListener listener = context .getBean(EventCapturingApplicationListener.class); - Awaitility.waitAtMost(Duration.ofSeconds(30)) - .until(() -> dataSourceSchemaCreatedEventsReceivedBy(listener), hasSize(1)); + assertThat(listener.events).hasSize(1); + assertThat(listener.events).hasOnlyElementsOfType(ContextRefreshedEvent.class); + // createEntityManager requires Hibernate bootstrapping to be complete + assertThatNoException() + .isThrownBy(() -> context.getBean(EntityManagerFactory.class).createEntityManager()); }); } @@ -407,11 +416,6 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes }); } - private List dataSourceSchemaCreatedEventsReceivedBy(EventCapturingApplicationListener listener) { - return listener.events.stream().filter(DataSourceSchemaCreatedEvent.class::isInstance) - .collect(Collectors.toList()); - } - @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(City.class) static class TestInitializedJpaConfiguration { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc index 7fff03eec22..78eb8cb4da3 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc @@ -2029,6 +2029,11 @@ In addition, Spring Boot processes the `schema-$\{platform}.sql` and `data-$\{pl This allows you to switch to database-specific scripts if necessary. For example, you might choose to set it to the vendor name of the database (`hsqldb`, `h2`, `oracle`, `mysql`, `postgresql`, and so on). +By default, script-based `DataSource` initialization is performed before any JPA `EntityManagerFactory` beans are created. +`schema.sql` can be used to create the schema for JPA-managed entities and `data.sql` can be used to populate it. +If you want script-based `DataSource` initialization to be performed after the `EntityManagerFactory`, set `spring.datasource.initialization-order` to `after-jpa`. +`schema.sql` can then be used to make additions to any schema creation performed by Hibernate and `data.sql` can be used to populate it. + [NOTE] ==== When only basic SQL scripts are used, Spring Boot automatically creates the schema of an embedded `DataSource`. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-test/src/test/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-test/src/test/resources/application.properties index 24dfc94a0ce..357931f5509 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-test/src/test/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-test/src/test/resources/application.properties @@ -1 +1,2 @@ spring.test.mockmvc.print=none +spring.datasource.initialization-order=after-jpa