Rework DataSource initialization

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
This commit is contained in:
Andy Wilkinson 2021-02-17 12:08:19 +00:00
parent b3d73a1d06
commit 2f83a6714b
19 changed files with 537 additions and 536 deletions

View File

@ -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;
}
}

View File

@ -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 {
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManagerFactory.class })
static class DataSourceInitializationEntityManagerFactoryDependsOnPostProcessor
extends EntityManagerFactoryDependsOnPostProcessor {
private static final String BEAN_NAME = "dataSourceInitializerPostProcessor";
DataSourceInitializationEntityManagerFactoryDependsOnPostProcessor() {
super(DataSourceInitialization.class);
}
@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);
}
}
/**
* 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);
}
}

View File

@ -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);
public boolean initializeDataSource() {
boolean initialized = createSchema();
initialized = initSchema() && initialized;
return initialized;
}
DataSource getDataSource() {
return this.dataSource;
}
/**
* 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()) {
@ -103,21 +97,18 @@ class DataSourceInitializer {
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() {

View File

@ -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<DataSourceSchemaCreatedEvent>, 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) {
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;
}
}

View File

@ -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;
}
}

View File

@ -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 {
/**

View File

@ -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 {
}

View File

@ -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<String> defaultDdlAuto = () -> (EmbeddedDatabaseConnection.isEmbedded(dataSource) ? "create-drop"
: "none");
Map<String, Object> 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<ContextRefreshedEvent>, 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<String, ?> getJpaPropertyMap(PersistenceUnitInfo pui) {
return this.delegate.getJpaPropertyMap(pui);
}
@Override
public Map<String, ?> getJpaPropertyMap() {
return this.delegate.getJpaPropertyMap();
}
@Override
public JpaDialect getJpaDialect() {
return this.delegate.getJpaDialect();
}
@Override
public Class<? extends EntityManagerFactory> getEntityManagerFactoryInterface() {
return this.delegate.getEntityManagerFactoryInterface();
}
@Override
public Class<? extends EntityManager> 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));
}
}
}
}

View File

@ -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)));
}
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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": [

View File

@ -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;

View File

@ -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> T unwrap(Class<T> 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();
}
}
}

View File

@ -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() {

View File

@ -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<ApplicationEvent> dataSourceSchemaCreatedEventsReceivedBy(EventCapturingApplicationListener listener) {
return listener.events.stream().filter(DataSourceSchemaCreatedEvent.class::isInstance)
.collect(Collectors.toList());
}
@Configuration(proxyBeanMethods = false)
@TestAutoConfigurationPackage(City.class)
static class TestInitializedJpaConfiguration {

View File

@ -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`.

View File

@ -1 +1,2 @@
spring.test.mockmvc.print=none
spring.datasource.initialization-order=after-jpa