From b8cefd2b9cf2c7364fa92ed682a4a8025cc879db Mon Sep 17 00:00:00 2001 From: Lansana DIOMANDE Date: Mon, 27 Oct 2025 07:51:53 +0100 Subject: [PATCH 1/2] Add support for configuring additional PersistenceUnitPostProcessor See gh-47802 Signed-off-by: Lansana DIOMANDE --- .../HibernateJpaAutoConfigurationTests.java | 32 +++++++++++++++++++ .../boot/jpa/EntityManagerFactoryBuilder.java | 21 ++++++++++++ 2 files changed, 53 insertions(+) diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index bce4c30cde6..d1b259f6057 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -114,6 +114,7 @@ import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager import org.springframework.orm.jpa.persistenceunit.ManagedClassNameFilter; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; @@ -351,6 +352,19 @@ class HibernateJpaAutoConfigurationTests { }); } + @Test + void shouldProcessAllPersistenceUnitPostProcessorsDeclaredAsBeans() { + this.contextRunner.withUserConfiguration(TestConfigurationWithMultipleCustomPersistenceUnitPostProcessors.class) + .run((context) -> { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = context + .getBean(LocalContainerEntityManagerFactoryBean.class); + PersistenceUnitInfo persistenceUnitInfo = entityManagerFactoryBean.getPersistenceUnitInfo(); + assertThat(persistenceUnitInfo).isNotNull(); + assertThat(persistenceUnitInfo.getManagedClassNames()).contains( + "customized.attribute.converter.class.name", "customized.attribute.converter.class.othername"); + }); + } + @Test void customManagedClassNameFilter() { this.contextRunner.withBean(ManagedClassNameFilter.class, () -> (s) -> !s.endsWith("City")) @@ -1168,6 +1182,24 @@ class HibernateJpaAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @TestAutoConfigurationPackage(HibernateJpaAutoConfigurationTests.class) + static class TestConfigurationWithMultipleCustomPersistenceUnitPostProcessors { + + @Bean + EntityManagerFactoryBuilderCustomizer entityManagerFactoryBuilderCustomizer() { + return (builder) -> builder.addPersistenceUnitPostProcessors( + (pui) -> pui.addManagedClassName("customized.attribute.converter.class.name")); + } + + @Bean + EntityManagerFactoryBuilderCustomizer otherEntityManagerFactoryBuilderCustomizer() { + return (builder) -> builder.addPersistenceUnitPostProcessors( + (pui) -> pui.addManagedClassName("customized.attribute.converter.class.othername")); + } + + } + static class CustomJpaTransactionManager extends JpaTransactionManager { } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index 91ea4f9993c..405dbadcbfd 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -17,9 +17,12 @@ package org.springframework.boot.jpa; import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -129,6 +132,24 @@ public class EntityManagerFactoryBuilder { this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; } + /** + * Add some {@linkplain PersistenceUnitPostProcessor persistence unit post processors} + * to be applied to the PersistenceUnitInfo used for creating the + * {@link LocalContainerEntityManagerFactoryBean}. + * @param persistenceUnitPostProcessors the persistence unit post processors to use + */ + public void addPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... persistenceUnitPostProcessors) { + if (this.persistenceUnitPostProcessors == null) { + this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; + return; + } + + var combined = new ArrayList<>(Arrays.asList(this.persistenceUnitPostProcessors)); + combined.addAll(Arrays.asList(persistenceUnitPostProcessors)); + + this.persistenceUnitPostProcessors = combined.toArray(PersistenceUnitPostProcessor[]::new); + } + /** * A fluent builder for a LocalContainerEntityManagerFactoryBean. */ From 9602be20aaace7395eefc974afd55e8b9aaad361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 17 Nov 2025 15:09:56 +0100 Subject: [PATCH 2/2] Polish contribution See gh-47802 --- .../HibernateJpaAutoConfigurationTests.java | 9 +- module/spring-boot-jpa/build.gradle | 1 - .../boot/jpa/EntityManagerFactoryBuilder.java | 27 +++-- .../jpa/EntityManagerFactoryBuilderTests.java | 104 ++++++++++++++++++ 4 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index d1b259f6057..d4970653db9 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -114,7 +114,6 @@ import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager import org.springframework.orm.jpa.persistenceunit.ManagedClassNameFilter; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; -import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; @@ -353,7 +352,7 @@ class HibernateJpaAutoConfigurationTests { } @Test - void shouldProcessAllPersistenceUnitPostProcessorsDeclaredAsBeans() { + void customPersistenceUnitProcessorsAddedByServeralContributors() { this.contextRunner.withUserConfiguration(TestConfigurationWithMultipleCustomPersistenceUnitPostProcessors.class) .run((context) -> { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = context @@ -361,7 +360,7 @@ class HibernateJpaAutoConfigurationTests { PersistenceUnitInfo persistenceUnitInfo = entityManagerFactoryBean.getPersistenceUnitInfo(); assertThat(persistenceUnitInfo).isNotNull(); assertThat(persistenceUnitInfo.getManagedClassNames()).contains( - "customized.attribute.converter.class.name", "customized.attribute.converter.class.othername"); + "customized.attribute.converter.class.name", "customized.attribute.converter.class.anotherName"); }); } @@ -1193,9 +1192,9 @@ class HibernateJpaAutoConfigurationTests { } @Bean - EntityManagerFactoryBuilderCustomizer otherEntityManagerFactoryBuilderCustomizer() { + EntityManagerFactoryBuilderCustomizer anotherEntityManagerFactoryBuilderCustomizer() { return (builder) -> builder.addPersistenceUnitPostProcessors( - (pui) -> pui.addManagedClassName("customized.attribute.converter.class.othername")); + (pui) -> pui.addManagedClassName("customized.attribute.converter.class.anotherName")); } } diff --git a/module/spring-boot-jpa/build.gradle b/module/spring-boot-jpa/build.gradle index 80e53b1ab8c..c0641897c37 100644 --- a/module/spring-boot-jpa/build.gradle +++ b/module/spring-boot-jpa/build.gradle @@ -36,7 +36,6 @@ dependencies { testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(testFixtures(project(":core:spring-boot-autoconfigure"))) - testImplementation("org.springframework:spring-context-support") } tasks.named("compileTestJava") { diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index 405dbadcbfd..c23760d5e42 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -66,7 +66,7 @@ public class EntityManagerFactoryBuilder { private @Nullable AsyncTaskExecutor bootstrapExecutor; - private PersistenceUnitPostProcessor @Nullable [] persistenceUnitPostProcessors; + private @Nullable List persistenceUnitPostProcessors; /** * Create a new instance passing in the common pieces that will be shared if multiple @@ -129,25 +129,23 @@ public class EntityManagerFactoryBuilder { * @param persistenceUnitPostProcessors the persistence unit post processors to use */ public void setPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... persistenceUnitPostProcessors) { - this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; + this.persistenceUnitPostProcessors = new ArrayList<>(Arrays.asList(persistenceUnitPostProcessors)); } /** - * Add some {@linkplain PersistenceUnitPostProcessor persistence unit post processors} - * to be applied to the PersistenceUnitInfo used for creating the + * Add {@linkplain PersistenceUnitPostProcessor persistence unit post processors} to + * be applied to the PersistenceUnitInfo used for creating the * {@link LocalContainerEntityManagerFactoryBean}. - * @param persistenceUnitPostProcessors the persistence unit post processors to use + * @param persistenceUnitPostProcessors the persistence unit post processors to add + * @since 4.1.0 */ public void addPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... persistenceUnitPostProcessors) { - if (this.persistenceUnitPostProcessors == null) { - this.persistenceUnitPostProcessors = persistenceUnitPostProcessors; - return; + if (this.persistenceUnitPostProcessors != null) { + this.persistenceUnitPostProcessors.addAll(Arrays.asList(persistenceUnitPostProcessors)); + } + else { + setPersistenceUnitPostProcessors(persistenceUnitPostProcessors); } - - var combined = new ArrayList<>(Arrays.asList(this.persistenceUnitPostProcessors)); - combined.addAll(Arrays.asList(persistenceUnitPostProcessors)); - - this.persistenceUnitPostProcessors = combined.toArray(PersistenceUnitPostProcessor[]::new); } /** @@ -301,7 +299,8 @@ public class EntityManagerFactoryBuilder { } if (EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors != null) { entityManagerFactoryBean - .setPersistenceUnitPostProcessors(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors); + .setPersistenceUnitPostProcessors(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors + .toArray(PersistenceUnitPostProcessor[]::new)); } return entityManagerFactoryBean; } diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java new file mode 100644 index 00000000000..8e153dae603 --- /dev/null +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java @@ -0,0 +1,104 @@ +/* + * 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; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +import javax.sql.DataSource; + +import jakarta.persistence.spi.PersistenceProvider; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; +import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EntityManagerFactoryBuilder}. + * + * @author Stephane Nicoll + */ +class EntityManagerFactoryBuilderTests { + + @Test + void setPersistenceUnitPostProcessorsWhenEmpty() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + PersistenceUnitPostProcessor postProcessor = mock(); + PersistenceUnitPostProcessor postProcessor2 = mock(); + builder.setPersistenceUnitPostProcessors(postProcessor, postProcessor2); + assertThat(builder).extracting("persistenceUnitPostProcessors") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor, postProcessor2); + } + + @Test + void addPersistenceUnitPostProcessorsWhenEmpty() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + PersistenceUnitPostProcessor postProcessor = mock(); + PersistenceUnitPostProcessor postProcessor2 = mock(); + builder.addPersistenceUnitPostProcessors(postProcessor, postProcessor2); + assertThat(builder).extracting("persistenceUnitPostProcessors") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor, postProcessor2); + } + + @Test + void setPersistenceUnitPostProcessorsWhenNotEmpty() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + PersistenceUnitPostProcessor postProcessor = mock(); + builder.addPersistenceUnitPostProcessors(postProcessor); + PersistenceUnitPostProcessor postProcessor2 = mock(); + PersistenceUnitPostProcessor postProcessor3 = mock(); + builder.setPersistenceUnitPostProcessors(postProcessor2, postProcessor3); + assertThat(builder).extracting("persistenceUnitPostProcessors") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor2, postProcessor3); + } + + @Test + void addPersistenceUnitPostProcessorsWhenNotEmpty() { + EntityManagerFactoryBuilder builder = createEmptyBuilder(); + PersistenceUnitPostProcessor postProcessor = mock(); + builder.addPersistenceUnitPostProcessors(postProcessor); + PersistenceUnitPostProcessor postProcessor2 = mock(); + builder.addPersistenceUnitPostProcessors(postProcessor2); + assertThat(builder).extracting("persistenceUnitPostProcessors") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(postProcessor, postProcessor2); + } + + private EntityManagerFactoryBuilder createEmptyBuilder() { + Function> jpaPropertiesFactory = (dataSource) -> Collections + .emptyMap(); + return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null); + } + + static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter { + + @Override + public PersistenceProvider getPersistenceProvider() { + return mock(); + } + + } + +}