From a184af4abdaee38d19d3e7eeaa81e52c059fd98d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 7 Nov 2025 10:01:30 +0100 Subject: [PATCH 1/4] 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/4] 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/4] 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 From a8c42405eb1d5b0b3944d177c58b2b131530006e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 10 Nov 2025 10:29:20 +0100 Subject: [PATCH 4/4] Polishing? --- .../aot/JpaRepositoryContributor.java | 201 +---------------- .../aot/PersistenceUnitContext.java | 36 +++ .../aot/PersistenceUnitContexts.java | 210 ++++++++++++++++++ .../config/JpaRepositoryConfigExtension.java | 37 +-- .../aot/PersistenceUnitContextsUnitTests.java | 41 ++++ 5 files changed, 312 insertions(+), 213 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContext.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContexts.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContextsUnitTests.java 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 eec9e87d08..75a8078682 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,22 +15,15 @@ */ 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; @@ -65,12 +58,9 @@ 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; /** @@ -94,11 +84,11 @@ public class JpaRepositoryContributor extends RepositoryContributor { private final EntityGraphLookup entityGraphLookup; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { - this(repositoryContext, PersistenceUnitContextFactory.from(repositoryContext).create()); + this(repositoryContext, PersistenceUnitContexts.from(repositoryContext)); } public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { - this(repositoryContext, PersistenceUnitContext.just(entityManagerFactory)); + this(repositoryContext, PersistenceUnitContexts.from(entityManagerFactory)); } public JpaRepositoryContributor(AotRepositoryContext repositoryContext, PersistenceUnitContext persistenceUnit) { @@ -262,193 +252,6 @@ 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 { @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContext.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContext.java new file mode 100644 index 0000000000..23eade324c --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContext.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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 + * + * http://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.data.jpa.repository.aot; + +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.metamodel.Metamodel; + +/** + * 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(); +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContexts.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContexts.java new file mode 100644 index 0000000000..d61047f689 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContexts.java @@ -0,0 +1,210 @@ +/* + * Copyright 2025-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.data.jpa.repository.aot; + +import jakarta.persistence.Converter; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.metamodel.Metamodel; +import jakarta.persistence.spi.PersistenceUnitInfo; + +import java.util.List; +import java.util.function.Supplier; + +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.util.Lazy; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; +import org.springframework.util.ConcurrentLruCache; + +/** + * @author Christoph Strobl + */ +public class PersistenceUnitContexts { + + private static final ConcurrentLruCache PERSISTENCE_UNIT_FACTORY_CACHE = new ConcurrentLruCache<>( + 16, PersistenceUnitContexts::createFactory); + + private static PersistenceUnitContextFactory createFactory(Object source) { + + if (source instanceof AotRepositoryContext ctx) { + + List typeNames = ctx.getResolvedTypes().stream() + .filter(DefaultPersistenceUnitContextFactory::isJakartaAnnotated).map(Class::getName).toList(); + + return from(() -> new AotMetamodel(PersistenceManagedTypes.of(typeNames, List.of()))); + } + if (source instanceof PersistenceUnitInfo pui) { + return from(() -> new AotMetamodel(pui)); + } + if (source instanceof PersistenceManagedTypes managedTypes) { + return from(() -> new AotMetamodel(managedTypes)); + } + if (source instanceof EntityManagerFactory emf) { + return new DefaultPersistenceUnitContextFactory( + () -> new EntityManagerPersistenceUnitContext(emf, emf.getMetamodel())); + } + + throw new IllegalArgumentException(String.format("Cannot create PersistenceUnitContexts for %s", source)); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link AotRepositoryContext} using Jakarta + * Persistence-annotated classes. + * + * @param repositoryContext repository context providing classes. + */ + public static PersistenceUnitContext from(AotRepositoryContext repositoryContext) { + return factory().from(repositoryContext).create(); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link PersistenceUnitInfo}. + * + * @param persistenceUnitInfo persistence unit info to use. + */ + public static PersistenceUnitContext from(PersistenceUnitInfo persistenceUnitInfo) { + return factory().from(persistenceUnitInfo).create(); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link PersistenceManagedTypes}. + * + * @param managedTypes managed types to use. + */ + public static PersistenceUnitContext from(PersistenceManagedTypes managedTypes) { + return factory().from(managedTypes).create(); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link EntityManagerFactory} and its {@link Metamodel}. + * + * @param entityManagerFactory the entity manager factory to use. + */ + public static PersistenceUnitContext from(EntityManagerFactory entityManagerFactory) { + return factory().from(entityManagerFactory).create(); + } + + private static PersistenceUnitContextFactory from(Supplier metamodel) { + return new DefaultPersistenceUnitContextFactory(() -> new AotMetamodelContext(metamodel.get())); + } + + public static PeristenceUnitFactoryBuilder factory() { + + return new PeristenceUnitFactoryBuilder() { + + @Override + public PersistenceUnitContextFactory from(EntityManagerFactory entityManagerFactory) { + return PERSISTENCE_UNIT_FACTORY_CACHE.get(entityManagerFactory); + } + + @Override + public PersistenceUnitContextFactory from(PersistenceManagedTypes managedTypes) { + return PERSISTENCE_UNIT_FACTORY_CACHE.get(managedTypes); + } + + @Override + public PersistenceUnitContextFactory from(PersistenceUnitInfo persistenceUnitInfo) { + return PERSISTENCE_UNIT_FACTORY_CACHE.get(persistenceUnitInfo); + } + + @Override + public PersistenceUnitContextFactory from(AotRepositoryContext repositoryContext) { + return PERSISTENCE_UNIT_FACTORY_CACHE.get(repositoryContext); + } + }; + } + + public interface PeristenceUnitFactoryBuilder { + PersistenceUnitContextFactory from(EntityManagerFactory entityManagerFactory); + + PersistenceUnitContextFactory from(PersistenceManagedTypes entityManagerFactory); + + PersistenceUnitContextFactory from(PersistenceUnitInfo entityManagerFactory); + + PersistenceUnitContextFactory from(AotRepositoryContext entityManagerFactory); + } + + /** + * Persistence unit context backed by an {@link AotMetamodel}. + */ + record AotMetamodelContext(AotMetamodel metamodel) implements PersistenceUnitContext { + + @Override + public EntityManagerFactory getEntityManagerFactory() { + return metamodel.getEntityManagerFactory(); + } + + @Override + public Metamodel getMetamodel() { + return metamodel; + } + } + + /** + * 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(); + } + + } + + public interface PersistenceUnitContextFactory { + PersistenceUnitContext create(); + } + + /** + * 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 DefaultPersistenceUnitContextFactory implements PersistenceUnitContextFactory { + + private final Supplier persistenceUnitContextSupplier; + + private DefaultPersistenceUnitContextFactory( + Supplier persistenceUnitContextSupplier) { + + this.persistenceUnitContextSupplier = Lazy.of(persistenceUnitContextSupplier); + } + + 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 persistenceUnitContextSupplier.get(); + } + } +} 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 c7f4182a32..057b47f3ca 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 @@ -15,7 +15,8 @@ */ package org.springframework.data.jpa.repository.config; -import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*; +import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_CONTEXT_BEAN_NAME; +import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_MAPPING_CONTEXT_BEAN_NAME; import jakarta.persistence.Entity; import jakarta.persistence.EntityManager; @@ -39,7 +40,6 @@ import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; @@ -61,7 +61,9 @@ 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.aot.PersistenceUnitContexts; +import org.springframework.data.jpa.repository.aot.PersistenceUnitContexts.PersistenceUnitContextFactory; +import org.springframework.data.jpa.repository.aot.PersistenceUnitContext; import org.springframework.data.jpa.repository.support.DefaultJpaContext; import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; @@ -380,7 +382,7 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi private static final String MODULE_NAME = "jpa"; - private static final ConcurrentLruCache factoryCache = new ConcurrentLruCache<>( + private static final ConcurrentLruCache PERSISTENCE_UNIT_CACHE = new ConcurrentLruCache<>( 16, PersistenceUnitContextFactory::create); @Override @@ -410,8 +412,10 @@ protected void configureTypeContribution(Class type, AotContext aotContext) { Optional entityManagerFactoryRef = repositoryContext.getConfigurationSource() .getAttribute("entityManagerFactoryRef"); - log.debug( - "Using EntityManager '%s' for AOT repository generation".formatted(entityManagerFactoryRef.orElse(""))); + if (log.isDebugEnabled()) { + log.debug( + "Using EntityManager '%s' for AOT repository generation".formatted(entityManagerFactoryRef.orElse(""))); + } EntityManagerFactory emf = entityManagerFactoryRef .map(it -> beanFactory.getBean(it, EntityManagerFactory.class)) @@ -425,8 +429,10 @@ protected void configureTypeContribution(Class type, AotContext aotContext) { if (managedTypes != null) { - log.debug("Using PersistenceManagedTypes for AOT repository generation"); - return contribute(repositoryContext, PersistenceUnitContextFactory.from(managedTypes)); + if (log.isDebugEnabled()) { + log.debug("Using PersistenceManagedTypes for AOT repository generation"); + } + return contribute(repositoryContext, PersistenceUnitContexts.from(managedTypes)); } ObjectProvider infoProvider = beanFactory.getBeanProvider(PersistenceUnitInfo.class); @@ -434,18 +440,21 @@ protected void configureTypeContribution(Class type, AotContext aotContext) { if (unitInfo != null) { - log.debug("Using PersistenceUnitInfo for AOT repository generation"); - return contribute(repositoryContext, PersistenceUnitContextFactory.from(unitInfo)); + if (log.isDebugEnabled()) { + log.debug("Using PersistenceUnitInfo for AOT repository generation"); + } + return contribute(repositoryContext, PersistenceUnitContexts.from(unitInfo)); } - log.debug("Using scanned types for AOT repository generation"); - return contribute(repositoryContext, PersistenceUnitContextFactory.from(repositoryContext)); + if (log.isDebugEnabled()) { + log.debug("Using scanned types for AOT repository generation"); + } + return contribute(repositoryContext, PersistenceUnitContexts.from(repositoryContext)); } private JpaRepositoryContributor contribute(AotRepositoryContext repositoryContext, - PersistenceUnitContextFactory factory) { + PersistenceUnitContext persistenceUnitContext) { - 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/aot/PersistenceUnitContextsUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContextsUnitTests.java new file mode 100644 index 0000000000..ccea4ed061 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/PersistenceUnitContextsUnitTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025-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.data.jpa.repository.aot; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import jakarta.persistence.spi.PersistenceUnitInfo; + +import org.junit.jupiter.api.Test; +import org.springframework.data.jpa.repository.aot.PersistenceUnitContexts.PersistenceUnitContextFactory; + +/** + * @author Christoph Strobl + */ +public class PersistenceUnitContextsUnitTests { + + @Test // GH-4068 + void cachesPersistenceUnitContextFactory() { + + PersistenceUnitInfo persistenceUnitInfo = mock(PersistenceUnitInfo.class); + + PersistenceUnitContextFactory ctxFactory = PersistenceUnitContexts.factory().from(persistenceUnitInfo); + + assertThat(PersistenceUnitContexts.factory().from(persistenceUnitInfo)).isSameAs(ctxFactory); + assertThat(PersistenceUnitContexts.factory().from(mock(PersistenceUnitInfo.class))).isNotSameAs(ctxFactory); + } +}