Browse Source

Refine Spring Data JPA background bootstrapping mode support

Update `spring.data.jpa.repositories.bootstrap-mode` logic
so that:

- A value of `lazy` no longer sets a bootstrap executor.

- A value of `deferred` requires that a bootstrap executor is
  ultimately set, and fails with a meaningful message if it is
  not.

The logic used to find the AsyncTaskExecutor has now moved to
the `JpaBaseConfiguration` and is triggered by the
`EntityManagerFactoryBuilder` only when background
bootstrapping is required.

Closes gh-49688
pull/49740/head
Phillip Webb 2 weeks ago
parent
commit
e76c6c5488
  1. 3
      module/spring-boot-data-jpa-test/src/test/java/org/springframework/boot/data/jpa/test/autoconfigure/DataJpaTestAttributesIntegrationTests.java
  2. 31
      module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java
  3. 15
      module/spring-boot-data-jpa/src/test/java/org/springframework/boot/data/jpa/autoconfigure/AbstractDataJpaRepositoriesAutoConfigurationTests.java
  4. 2
      module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java
  5. 59
      module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java
  6. 13
      module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java
  7. 60
      module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java
  8. 46
      module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java
  9. 40
      module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java

3
module/spring-boot-data-jpa-test/src/test/java/org/springframework/boot/data/jpa/test/autoconfigure/DataJpaTestAttributesIntegrationTests.java

@ -19,6 +19,8 @@ package org.springframework.boot.data.jpa.test.autoconfigure;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.data.repository.config.BootstrapMode; import org.springframework.data.repository.config.BootstrapMode;
@ -31,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Scott Frederick * @author Scott Frederick
*/ */
@DataJpaTest(properties = "spring.profiles.active=test", bootstrapMode = BootstrapMode.DEFERRED) @DataJpaTest(properties = "spring.profiles.active=test", bootstrapMode = BootstrapMode.DEFERRED)
@ImportAutoConfiguration(TaskExecutionAutoConfiguration.class)
class DataJpaTestAttributesIntegrationTests { class DataJpaTestAttributesIntegrationTests {
@Autowired @Autowired

31
module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java

@ -25,7 +25,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.LazyInitializationExcludeFilter; import org.springframework.boot.LazyInitializationExcludeFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -35,8 +34,8 @@ import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguratio
import org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration.JpaRepositoriesImportSelector; import org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration.JpaRepositoriesImportSelector;
import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration;
import org.springframework.boot.jpa.autoconfigure.EntityManagerFactoryBuilderCustomizer; import org.springframework.boot.jpa.autoconfigure.EntityManagerFactoryBuilderCustomizer;
import org.springframework.boot.jpa.autoconfigure.PropertyBasedRequiredBackgroundBootstrapping;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector; import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor;
@ -83,15 +82,11 @@ import org.springframework.util.ClassUtils;
public final class DataJpaRepositoriesAutoConfiguration { public final class DataJpaRepositoriesAutoConfiguration {
@Bean @Bean
@Conditional(BootstrapExecutorCondition.class) @ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred")
EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer( EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer(
Map<String, AsyncTaskExecutor> taskExecutors) { Map<String, AsyncTaskExecutor> taskExecutors) {
return (builder) -> { return (builder) -> builder.requireBootstrapExecutor(new PropertyBasedRequiredBackgroundBootstrapping(
AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors); "spring.data.jpa.repositories.bootstrap-mode", "deferred"));
if (bootstrapExecutor != null) {
builder.setBootstrapExecutor(bootstrapExecutor);
}
};
} }
@Bean @Bean
@ -106,24 +101,6 @@ public final class DataJpaRepositoriesAutoConfiguration {
return taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); return taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
} }
private static final class BootstrapExecutorCondition extends AnyNestedCondition {
BootstrapExecutorCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred")
static class DeferredBootstrapMode {
}
@ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "lazy")
static class LazyBootstrapMode {
}
}
static class JpaRepositoriesImportSelector implements ImportSelector { static class JpaRepositoriesImportSelector implements ImportSelector {
private static final boolean ENVERS_AVAILABLE = ClassUtils.isPresent( private static final boolean ENVERS_AVAILABLE = ClassUtils.isPresent(

15
module/spring-boot-data-jpa/src/test/java/org/springframework/boot/data/jpa/autoconfigure/AbstractDataJpaRepositoriesAutoConfigurationTests.java

@ -88,23 +88,12 @@ abstract class AbstractDataJpaRepositoriesAutoConfigurationTests {
} }
@Test @Test
void whenBootstrapModeIsLazyWithMultipleAsyncExecutorBootstrapExecutorIsConfigured() { void whenBootstrapModeDoesNotUseFallbackBootstrapExecutor() {
this.contextRunner.withUserConfiguration(MultipleAsyncTaskExecutorConfiguration.class)
.withConfiguration(
AutoConfigurations.of(TaskExecutionAutoConfiguration.class, TaskSchedulingAutoConfiguration.class))
.withPropertyValues("spring.data.jpa.repositories.bootstrap-mode=lazy")
.run((context) -> assertThat(
context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor())
.isEqualTo(context.getBean("applicationTaskExecutor")));
}
@Test
void whenBootstrapModeIsLazyWithSingleAsyncExecutorBootstrapExecutorIsConfigured() {
this.contextRunner.withUserConfiguration(SingleAsyncTaskExecutorConfiguration.class) this.contextRunner.withUserConfiguration(SingleAsyncTaskExecutorConfiguration.class)
.withPropertyValues("spring.data.jpa.repositories.bootstrap-mode=lazy") .withPropertyValues("spring.data.jpa.repositories.bootstrap-mode=lazy")
.run((context) -> assertThat( .run((context) -> assertThat(
context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor())
.isEqualTo(context.getBean("testAsyncTaskExecutor"))); .isNull());
} }
@Test @Test

2
module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java

@ -126,7 +126,7 @@ import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
* Base for JPA tests and tests for {@link JpaBaseConfiguration}. * Test for {@link HibernateJpaAutoConfiguration} and {@link JpaBaseConfiguration}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Dave Syer * @author Dave Syer

59
module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java

@ -26,6 +26,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import javax.sql.DataSource; import javax.sql.DataSource;
@ -35,9 +36,11 @@ import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.LocalEntityManagerFactoryBean;
import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -65,10 +68,14 @@ public class EntityManagerFactoryBuilder {
private final @Nullable URL persistenceUnitRootLocation; private final @Nullable URL persistenceUnitRootLocation;
private final @Nullable AsyncTaskExecutor fallbackBootstrapExecutor;
private @Nullable AsyncTaskExecutor bootstrapExecutor; private @Nullable AsyncTaskExecutor bootstrapExecutor;
private @Nullable List<PersistenceUnitPostProcessor> persistenceUnitPostProcessors; private @Nullable List<PersistenceUnitPostProcessor> persistenceUnitPostProcessors;
private @Nullable Supplier<? extends RuntimeException> requireBootstrapExecutorExceptionSupplier;
/** /**
* Create a new instance passing in the common pieces that will be shared if multiple * Create a new instance passing in the common pieces that will be shared if multiple
* EntityManagerFactory instances are created. * EntityManagerFactory instances are created.
@ -98,10 +105,32 @@ public class EntityManagerFactoryBuilder {
public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
Function<DataSource, Map<String, ?>> jpaPropertiesFactory, Function<DataSource, Map<String, ?>> jpaPropertiesFactory,
@Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation) { @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation) {
this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, null);
}
/**
* Create a new instance passing in the common pieces that will be shared if multiple
* EntityManagerFactory instances are created.
* @param jpaVendorAdapter a vendor adapter
* @param jpaPropertiesFactory the JPA properties to be passed to the persistence
* provider, based on the {@linkplain #dataSource(DataSource) configured data source}
* @param persistenceUnitManager optional source of persistence unit information (can
* be null)
* @param persistenceUnitRootLocation the persistence unit root location to use as a
* fallback or {@code null}
* @param fallbackBootstrapExecutor the fallback executor to use when background
* bootstrapping is required but no explicit executor has been set
* @since 4.1.0
*/
public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
Function<DataSource, Map<String, ?>> jpaPropertiesFactory,
@Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation,
@Nullable AsyncTaskExecutor fallbackBootstrapExecutor) {
this.jpaVendorAdapter = jpaVendorAdapter; this.jpaVendorAdapter = jpaVendorAdapter;
this.persistenceUnitManager = persistenceUnitManager; this.persistenceUnitManager = persistenceUnitManager;
this.jpaPropertiesFactory = jpaPropertiesFactory; this.jpaPropertiesFactory = jpaPropertiesFactory;
this.persistenceUnitRootLocation = persistenceUnitRootLocation; this.persistenceUnitRootLocation = persistenceUnitRootLocation;
this.fallbackBootstrapExecutor = fallbackBootstrapExecutor;
} }
/** /**
@ -123,6 +152,20 @@ public class EntityManagerFactoryBuilder {
this.bootstrapExecutor = bootstrapExecutor; this.bootstrapExecutor = bootstrapExecutor;
} }
/**
* Require a bootstrap executor to be configured when the
* {@link LocalEntityManagerFactoryBean} is built. If no bootstrap executor is
* {@link #setBootstrapExecutor(AsyncTaskExecutor) explicitly set} and no
* {@link #EntityManagerFactoryBuilder(JpaVendorAdapter, Function, PersistenceUnitManager, URL, AsyncTaskExecutor)
* fallbackBootstrapExecutor} is available then the supplied exception is thrown.
* @param exceptionSupplier a supplier providing the exception to throw
* @since 4.1.0
*/
public void requireBootstrapExecutor(Supplier<? extends RuntimeException> exceptionSupplier) {
Assert.notNull(exceptionSupplier, "'exceptionSupplier' must not be null");
this.requireBootstrapExecutorExceptionSupplier = exceptionSupplier;
}
/** /**
* Set the {@linkplain PersistenceUnitPostProcessor persistence unit post processors} * Set the {@linkplain PersistenceUnitPostProcessor persistence unit post processors}
* to be applied to the PersistenceUnitInfo used for creating the * to be applied to the PersistenceUnitInfo used for creating the
@ -281,7 +324,7 @@ public class EntityManagerFactoryBuilder {
map.from(EntityManagerFactoryBuilder.this.persistenceUnitRootLocation) map.from(EntityManagerFactoryBuilder.this.persistenceUnitRootLocation)
.as(Object::toString) .as(Object::toString)
.to(factory::setPersistenceUnitRootLocation); .to(factory::setPersistenceUnitRootLocation);
map.from(EntityManagerFactoryBuilder.this.bootstrapExecutor).to(factory::setBootstrapExecutor); map.from(this::bootstrapExecutor).to(factory::setBootstrapExecutor);
map.from(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors) map.from(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors)
.as((postProcessors) -> postProcessors.toArray(PersistenceUnitPostProcessor[]::new)) .as((postProcessors) -> postProcessors.toArray(PersistenceUnitPostProcessor[]::new))
.to(factory::setPersistenceUnitPostProcessors); .to(factory::setPersistenceUnitPostProcessors);
@ -295,6 +338,20 @@ public class EntityManagerFactoryBuilder {
return jpaPropertyMap; return jpaPropertyMap;
} }
private @Nullable AsyncTaskExecutor bootstrapExecutor() {
if (EntityManagerFactoryBuilder.this.bootstrapExecutor != null) {
return EntityManagerFactoryBuilder.this.bootstrapExecutor;
}
if (EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier != null) {
if (EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor != null) {
return EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor;
}
RuntimeException ex = EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier.get();
throw (ex != null) ? ex : new IllegalStateException("A bootstrap executor is required");
}
return null;
}
} }
} }

13
module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java

@ -37,6 +37,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jpa.EntityManagerFactoryBuilder; import org.springframework.boot.jpa.EntityManagerFactoryBuilder;
import org.springframework.boot.persistence.autoconfigure.EntityScanPackages; import org.springframework.boot.persistence.autoconfigure.EntityScanPackages;
@ -45,6 +46,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
@ -120,13 +122,20 @@ public abstract class JpaBaseConfiguration {
@ConditionalOnMissingBean @ConditionalOnMissingBean
public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
ObjectProvider<PersistenceUnitManager> persistenceUnitManager, ObjectProvider<PersistenceUnitManager> persistenceUnitManager,
ObjectProvider<EntityManagerFactoryBuilderCustomizer> customizers) { ObjectProvider<EntityManagerFactoryBuilderCustomizer> customizers,
Map<String, AsyncTaskExecutor> taskExecutors) {
@Nullable AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors);
EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter, EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter,
this::buildJpaProperties, persistenceUnitManager.getIfAvailable()); this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder; return builder;
} }
private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map<String, AsyncTaskExecutor> taskExecutors) {
return (taskExecutors.size() == 1) ? taskExecutors.values().iterator().next()
: taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
}
private Map<String, ?> buildJpaProperties(DataSource dataSource) { private Map<String, ?> buildJpaProperties(DataSource dataSource) {
Map<String, Object> properties = new HashMap<>(this.properties.getProperties()); Map<String, Object> properties = new HashMap<>(this.properties.getProperties());
Map<String, Object> vendorProperties = getVendorProperties(dataSource); Map<String, Object> vendorProperties = getVendorProperties(dataSource);

60
module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java

@ -0,0 +1,60 @@
/*
* Copyright 2012-present 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.jpa.autoconfigure;
import java.util.function.Supplier;
import org.springframework.boot.diagnostics.FailureAnalyzedException;
import org.springframework.boot.jpa.EntityManagerFactoryBuilder;
import org.springframework.util.Assert;
/**
* {@link Supplier} to use with
* {@link EntityManagerFactoryBuilder#requireBootstrapExecutor} when a property indicates
* background bootstrapping is required.
*
* @author Phillip Webb
* @since 4.1.0
*/
public class PropertyBasedRequiredBackgroundBootstrapping implements Supplier<RuntimeException> {
private final String propertyName;
private final String propertyValue;
public PropertyBasedRequiredBackgroundBootstrapping(String propertyName, String propertyValue) {
Assert.notNull(propertyName, "'propertyName' must not be null");
Assert.notNull(propertyValue, "'propertyValue' must not be null");
this.propertyName = propertyName;
this.propertyValue = propertyValue;
}
@Override
public RuntimeException get() {
String description = "A LocalContainerEntityManagerFactoryBean bootstrap executor is required when '%s' is set to '%s'"
.formatted(this.propertyName, this.propertyValue);
StringBuilder action = new StringBuilder();
action.append("Use a different '%s' or provide a bootstrap executor using one of the following methods:\n"
.formatted(this.propertyName));
action.append("\tWith an auto-configured task executor "
+ "(you may need to set 'spring.task.execution.mode' to 'force').");
action.append("\tWith an AsyncTaskExecutor bean named 'applicationTaskExecutor.'");
action.append("\tUsing a EntityManagerFactoryBuilderCustomizer.");
return new FailureAnalyzedException(description, action.toString());
}
}

46
module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java

@ -24,12 +24,17 @@ import javax.sql.DataSource;
import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceProvider;
import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.InstanceOfAssertFactories;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor;
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
@ -86,9 +91,48 @@ class EntityManagerFactoryBuilderTests {
.containsExactly(postProcessor, postProcessor2); .containsExactly(postProcessor, postProcessor2);
} }
@Test
void requireBootstrapExecutorWhenExecutorProvidedDoesNotThrow() {
EntityManagerFactoryBuilder builder = createEmptyBuilder();
builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD"));
builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor());
DataSource dataSource = mock();
assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build);
}
@Test
void requireBootstrapExecutorWhenFallbackExecutorProvidesExecutorDoesNotThrow() {
EntityManagerFactoryBuilder builder = createEmptyBuilder(new SimpleAsyncTaskExecutor());
builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD"));
DataSource dataSource = mock();
assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build);
}
@Test
void requireBootstrapExecutorWhenExecutorAndNoFallbackExecutorThrowsException() {
EntityManagerFactoryBuilder builder = createEmptyBuilder();
builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD"));
DataSource dataSource = mock();
assertThatIllegalStateException().isThrownBy(builder.dataSource(dataSource)::build).withMessage("BAD");
}
@Test
void requireBootstrapExecutorWhenSupplierReturnsNullExecutorAndNoFallbackExecutorThrowsException() {
EntityManagerFactoryBuilder builder = createEmptyBuilder();
builder.requireBootstrapExecutor(() -> null);
DataSource dataSource = mock();
assertThatIllegalStateException().isThrownBy(builder.dataSource(dataSource)::build)
.withMessage("A bootstrap executor is required");
}
private EntityManagerFactoryBuilder createEmptyBuilder() { private EntityManagerFactoryBuilder createEmptyBuilder() {
return createEmptyBuilder(null);
}
private EntityManagerFactoryBuilder createEmptyBuilder(@Nullable AsyncTaskExecutor fallbackBootstrapExecutor) {
Function<DataSource, Map<String, ?>> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap(); Function<DataSource, Map<String, ?>> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap();
return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null); return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null, null,
fallbackBootstrapExecutor);
} }
static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter { static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter {

40
module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java

@ -0,0 +1,40 @@
/*
* Copyright 2012-present 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.jpa.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.diagnostics.FailureAnalyzedException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PropertyBasedRequiredBackgroundBootstrapping}.
*
* @author Phillip Webb
*/
class PropertyBasedRequiredBackgroundBootstrappingTests {
@Test
void getReturnsFailureAnalyzableException() {
RuntimeException exception = new PropertyBasedRequiredBackgroundBootstrapping("test.bootstrap", "true").get();
assertThat(exception).isInstanceOf(FailureAnalyzedException.class)
.hasMessage("A LocalContainerEntityManagerFactoryBean bootstrap executor is required "
+ "when 'test.bootstrap' is set to 'true'");
}
}
Loading…
Cancel
Save