From a184af4abdaee38d19d3e7eeaa81e52c059fd98d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 7 Nov 2025 10:01:30 +0100 Subject: [PATCH 1/3] Prepare issue branch. --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index bf477e770c..f513cd1cbc 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-4068-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 0bdf2c8e7e..822c162b4a 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 4.0.0-SNAPSHOT + 4.0.0-GH-4068-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-4068-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..e6b8e58c2c 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-4068-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index bb01d9f6f6..91ddf67d76 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 4.0.0-SNAPSHOT + 4.0.0-GH-4068-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-4068-SNAPSHOT ../pom.xml From 4118196de30f789ad1cf5cc530c8e34a2b09fc39 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 7 Nov 2025 10:02:48 +0100 Subject: [PATCH 2/3] Avoid repeated creation of `EntityManagerFactory` in AOT repository generation. We use now a cached variant by deferring EntityManagerFactory creation. --- .../data/jpa/repository/aot/AotMetamodel.java | 15 +- .../aot/JpaRepositoryContributor.java | 229 ++++++++++++++++-- .../config/JpaRepositoryConfigExtension.java | 18 +- ...toryRegistrationAotProcessorUnitTests.java | 2 +- 4 files changed, 226 insertions(+), 38 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java index 238d270de5..812abe5e6f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java @@ -37,7 +37,7 @@ import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; import org.jspecify.annotations.Nullable; -import org.springframework.data.repository.config.AotRepositoryContext; + import org.springframework.data.util.Lazy; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo; @@ -55,19 +55,6 @@ class AotMetamodel implements Metamodel { private final Lazy entityManagerFactory; private final Lazy entityManager = Lazy.of(() -> getEntityManagerFactory().createEntityManager()); - public AotMetamodel(AotRepositoryContext repositoryContext) { - this(repositoryContext.getResolvedTypes().stream().filter(AotMetamodel::isJakartaAnnotated).map(Class::getName) - .toList(), null); - } - - private static boolean isJakartaAnnotated(Class cls) { - - return cls.isAnnotationPresent(jakarta.persistence.Entity.class) - || cls.isAnnotationPresent(jakarta.persistence.Embeddable.class) - || cls.isAnnotationPresent(jakarta.persistence.MappedSuperclass.class) - || cls.isAnnotationPresent(jakarta.persistence.Converter.class); - } - public AotMetamodel(PersistenceManagedTypes managedTypes) { this(managedTypes.getManagedClassNames(), managedTypes.getPersistenceUnitRootUrl()); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 2c169efac0..eec9e87d08 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -15,16 +15,22 @@ */ package org.springframework.data.jpa.repository.aot; +import jakarta.persistence.Converter; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.MappedSuperclass; import jakarta.persistence.PersistenceUnitUtil; import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.spi.PersistenceUnitInfo; import java.lang.reflect.Method; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; +import java.util.function.Supplier; import org.jspecify.annotations.Nullable; @@ -59,10 +65,12 @@ import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.util.Lazy; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -77,39 +85,31 @@ */ public class JpaRepositoryContributor extends RepositoryContributor { + private final AotRepositoryContext context; + private final PersistenceUnitContext persistenceUnit; private final Metamodel metamodel; private final PersistenceUnitUtil persistenceUnitUtil; private final PersistenceProvider persistenceProvider; private final QueriesFactory queriesFactory; private final EntityGraphLookup entityGraphLookup; - private final AotRepositoryContext context; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { - this(repositoryContext, new AotMetamodel(repositoryContext)); - } - - public JpaRepositoryContributor(AotRepositoryContext repositoryContext, PersistenceUnitInfo unitInfo) { - this(repositoryContext, new AotMetamodel(unitInfo)); - } - - public JpaRepositoryContributor(AotRepositoryContext repositoryContext, PersistenceManagedTypes managedTypes) { - this(repositoryContext, new AotMetamodel(managedTypes)); + this(repositoryContext, PersistenceUnitContextFactory.from(repositoryContext).create()); } public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { - this(repositoryContext, entityManagerFactory, entityManagerFactory.getMetamodel()); + this(repositoryContext, PersistenceUnitContext.just(entityManagerFactory)); } - private JpaRepositoryContributor(AotRepositoryContext repositoryContext, AotMetamodel metamodel) { - this(repositoryContext, metamodel.getEntityManagerFactory(), metamodel); - } - - private JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory, - Metamodel metamodel) { + public JpaRepositoryContributor(AotRepositoryContext repositoryContext, PersistenceUnitContext persistenceUnit) { super(repositoryContext); - this.metamodel = metamodel; + this.persistenceUnit = persistenceUnit; + this.metamodel = persistenceUnit.getMetamodel(); + + EntityManagerFactory entityManagerFactory = persistenceUnit.getEntityManagerFactory(); + this.persistenceUnitUtil = entityManagerFactory.getPersistenceUnitUtil(); this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), entityManagerFactory, @@ -258,8 +258,195 @@ private Optional> getQueryEnhancerSelectorClass() { }); } - public Metamodel getMetamodel() { - return metamodel; + public PersistenceUnitContext getPersistenceUnit() { + return persistenceUnit; + } + + /** + * Factory for deferred {@link PersistenceUnitContext} creation. Factory objects implement equality checks based on + * their creation and can be used conveniently as cache keys. + */ + public static class PersistenceUnitContextFactory { + + private final Supplier factory; + private final Object key; + + private PersistenceUnitContextFactory(Supplier factory, Object key) { + this.factory = Lazy.of(factory); + this.key = key; + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link AotRepositoryContext} using Jakarta + * Persistence-annotated classes. + * + * @param repositoryContext repository context providing classes. + */ + public static PersistenceUnitContextFactory from(AotRepositoryContext repositoryContext) { + + List typeNames = repositoryContext.getResolvedTypes().stream() + .filter(PersistenceUnitContextFactory::isJakartaAnnotated).map(Class::getName).toList(); + + return from(() -> new AotMetamodel(PersistenceManagedTypes.of(typeNames, List.of())), typeNames); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link PersistenceUnitInfo}. + * + * @param persistenceUnitInfo persistence unit info to use. + */ + public static PersistenceUnitContextFactory from(PersistenceUnitInfo persistenceUnitInfo) { + return from(() -> new AotMetamodel(persistenceUnitInfo), persistenceUnitInfo); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link PersistenceManagedTypes}. + * + * @param managedTypes managed types to use. + */ + public static PersistenceUnitContextFactory from(PersistenceManagedTypes managedTypes) { + return from(() -> new AotMetamodel(managedTypes), managedTypes); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link EntityManagerFactory} and its {@link Metamodel}. + * + * @param entityManagerFactory the entity manager factory to use. + */ + public static PersistenceUnitContextFactory just(EntityManagerFactory entityManagerFactory) { + return new PersistenceUnitContextFactory(() -> new EntityManagerPersistenceUnitContext(entityManagerFactory), + entityManagerFactory.getMetamodel()); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link EntityManagerFactory} and its {@link Metamodel}. + * + * @param metamodel the metamodel to use. + * @param entityManagerFactory the entity manager factory to use. + */ + public static PersistenceUnitContextFactory just(EntityManagerFactory entityManagerFactory, Metamodel metamodel) { + return new PersistenceUnitContextFactory( + () -> new EntityManagerPersistenceUnitContext(entityManagerFactory, metamodel), metamodel); + } + + private static PersistenceUnitContextFactory from(Supplier metamodel, Object key) { + return new PersistenceUnitContextFactory(() -> new AotMetamodelContext(metamodel.get()), key); + } + + private static boolean isJakartaAnnotated(Class cls) { + + return cls.isAnnotationPresent(Entity.class) // + || cls.isAnnotationPresent(Embeddable.class) // + || cls.isAnnotationPresent(MappedSuperclass.class) // + || cls.isAnnotationPresent(Converter.class); + } + + public PersistenceUnitContext create() { + return factory.get(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PersistenceUnitContextFactory that)) { + return false; + } + return ObjectUtils.nullSafeEquals(key, that.key); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(key); + } + + @Override + public String toString() { + return "PersistenceUnitContextFactory{" + key + '}'; + } + + } + + /** + * Strategy interface representing a JPA PersistenceUnit providing access to {@link EntityManagerFactory} and + * {@link Metamodel} for AOT repository generation. + */ + public interface PersistenceUnitContext { + + /** + * @return the entity manager factory. + */ + EntityManagerFactory getEntityManagerFactory(); + + /** + * @return metamodel describing managed types used in the persistence unit. + */ + Metamodel getMetamodel(); + + /** + * Create a {@code PersistenceUnitContext} from the given {@link PersistenceUnitInfo}. + * + * @param persistenceUnitInfo persistence unit info to use. + */ + static PersistenceUnitContext from(PersistenceUnitInfo persistenceUnitInfo) { + return new AotMetamodelContext(new AotMetamodel(persistenceUnitInfo)); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link PersistenceManagedTypes}. + * + * @param managedTypes managed types to use. + */ + static PersistenceUnitContext from(PersistenceManagedTypes managedTypes) { + return new AotMetamodelContext(new AotMetamodel(managedTypes)); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link EntityManagerFactory} and its {@link Metamodel}. + * + * @param entityManagerFactory the entity manager factory to use. + */ + static PersistenceUnitContext just(EntityManagerFactory entityManagerFactory) { + return new EntityManagerPersistenceUnitContext(entityManagerFactory); + } + + } + + /** + * Persistence unit context backed by an {@link EntityManagerFactory}. + */ + record EntityManagerPersistenceUnitContext(EntityManagerFactory factory, + Metamodel metamodel) implements PersistenceUnitContext { + + public EntityManagerPersistenceUnitContext(EntityManagerFactory factory) { + this(factory, factory.getMetamodel()); + } + + @Override + public Metamodel getMetamodel() { + return metamodel(); + } + + @Override + public EntityManagerFactory getEntityManagerFactory() { + return factory(); + } + + } + + /** + * Persistence unit context backed by an {@link AotMetamodel}. + */ + private record AotMetamodelContext(AotMetamodel metamodel) implements PersistenceUnitContext { + + @Override + public EntityManagerFactory getEntityManagerFactory() { + return metamodel.getEntityManagerFactory(); + } + + @Override + public Metamodel getMetamodel() { + return metamodel; + } + } record StoredProcedureMetadata(String procedure) implements QueryMetadata { @@ -268,6 +455,7 @@ record StoredProcedureMetadata(String procedure) implements QueryMetadata { public Map serialize() { return Map.of("procedure", procedure()); } + } record NamedStoredProcedureMetadata(String procedureName) implements QueryMetadata { @@ -276,6 +464,7 @@ record NamedStoredProcedureMetadata(String procedureName) implements QueryMetada public Map serialize() { return Map.of("procedure-name", procedureName()); } + } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index e61f0e13ab..c7f4182a32 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -61,6 +61,7 @@ import org.springframework.data.aot.AotContext; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor; +import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor.PersistenceUnitContextFactory; import org.springframework.data.jpa.repository.support.DefaultJpaContext; import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; @@ -75,6 +76,7 @@ import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -378,6 +380,9 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi private static final String MODULE_NAME = "jpa"; + private static final ConcurrentLruCache factoryCache = new ConcurrentLruCache<>( + 16, PersistenceUnitContextFactory::create); + @Override protected void configureTypeContributions(AotRepositoryContext repositoryContext, GenerationContext generationContext) { @@ -421,7 +426,7 @@ protected void configureTypeContribution(Class type, AotContext aotContext) { if (managedTypes != null) { log.debug("Using PersistenceManagedTypes for AOT repository generation"); - return new JpaRepositoryContributor(repositoryContext, managedTypes); + return contribute(repositoryContext, PersistenceUnitContextFactory.from(managedTypes)); } ObjectProvider infoProvider = beanFactory.getBeanProvider(PersistenceUnitInfo.class); @@ -430,11 +435,18 @@ protected void configureTypeContribution(Class type, AotContext aotContext) { if (unitInfo != null) { log.debug("Using PersistenceUnitInfo for AOT repository generation"); - return new JpaRepositoryContributor(repositoryContext, unitInfo); + return contribute(repositoryContext, PersistenceUnitContextFactory.from(unitInfo)); } log.debug("Using scanned types for AOT repository generation"); - return new JpaRepositoryContributor(repositoryContext); + return contribute(repositoryContext, PersistenceUnitContextFactory.from(repositoryContext)); + } + + private JpaRepositoryContributor contribute(AotRepositoryContext repositoryContext, + PersistenceUnitContextFactory factory) { + + JpaRepositoryContributor.PersistenceUnitContext persistenceUnitContext = factoryCache.get(factory); + return new JpaRepositoryContributor(repositoryContext, persistenceUnitContext); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java index 25277bad6c..66f2ff0095 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java @@ -131,7 +131,7 @@ public List getManagedPackages() { JpaRepositoryContributor contributor = new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() .contributeAotRepository(new DummyAotRepositoryContext(context)); - assertThat(contributor.getMetamodel().managedType(Person.class)).isNotNull(); + assertThat(contributor.getPersistenceUnit().getMetamodel().managedType(Person.class)).isNotNull(); } @Test // GH-3899 From 2add393ed6261555af481edf25ed7d5a1692e4cf Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 7 Nov 2025 10:03:18 +0100 Subject: [PATCH 3/3] Polishing. Refine docs, use records for non-public API. --- .../data/jpa/repository/aot/EntityGraphLookup.java | 8 +------- .../data/jpa/repository/aot/QueriesFactory.java | 3 ++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/EntityGraphLookup.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/EntityGraphLookup.java index 7e715f9e24..33dd14e189 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/EntityGraphLookup.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/EntityGraphLookup.java @@ -42,13 +42,7 @@ * @author Mark Paluch * @since 4.0 */ -class EntityGraphLookup { - - private final EntityManagerFactory entityManagerFactory; - - public EntityGraphLookup(EntityManagerFactory entityManagerFactory) { - this.entityManagerFactory = entityManagerFactory; - } +record EntityGraphLookup(EntityManagerFactory entityManagerFactory) { @SuppressWarnings("unchecked") public @Nullable AotEntityGraph findEntityGraph(MergedAnnotation entityGraph, diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java index 7dd293b313..83a1d3dce7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java @@ -54,7 +54,8 @@ import org.springframework.util.StringUtils; /** - * Factory for {@link AotQueries}. + * Factory for {@link AotQueries}. Requires {@link EntityManagerFactory} for named query resolution and + * {@link Metamodel} for query derivation to navigate the entity model. * * @author Mark Paluch * @author Christoph Strobl