diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotEntityManagerFactoryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotEntityManagerFactoryCreator.java new file mode 100644 index 000000000..26131f9ed --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotEntityManagerFactoryCreator.java @@ -0,0 +1,144 @@ +/* + * 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 + * + * 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.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.ObjectUtils; + +/** + * Wrapper for {@link EntityManagerFactory}. The wrapper object implements equality checks based on its creation + * arguments and can be conveniently used as cache key. + *

+ * Factory methods provide ways to create instances based on provided {@link EntityManagerFactory} or contextual holders + * to extract managed types and create an in-memory {@link EntityManagerFactory} variant for metamodel introspection + * during AOT processing. + * + * @author Mark Paluch + * @since 4.0 + */ +public class AotEntityManagerFactoryCreator { + + private final Supplier factory; + private final Object key; + + private AotEntityManagerFactoryCreator(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. + *

+ * The underlying {@link jakarta.persistence.metamodel.Metamodel} requires Hibernate to build metamodel information. + * + * @param repositoryContext repository context providing classes. + */ + public static AotEntityManagerFactoryCreator from(AotRepositoryContext repositoryContext) { + + List typeNames = repositoryContext.getResolvedTypes().stream() + .filter(AotEntityManagerFactoryCreator::isJakartaAnnotated).map(Class::getName).toList(); + + return from(PersistenceManagedTypes.of(typeNames, List.of()), typeNames); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link PersistenceUnitInfo}. + *

+ * The underlying {@link jakarta.persistence.metamodel.Metamodel} requires Hibernate to build metamodel information. + * + * @param persistenceUnitInfo persistence unit info to use. + */ + public static AotEntityManagerFactoryCreator from(PersistenceUnitInfo persistenceUnitInfo) { + return from(() -> new AotMetamodel(persistenceUnitInfo), persistenceUnitInfo); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link PersistenceManagedTypes}. + *

+ * The underlying {@link jakarta.persistence.metamodel.Metamodel} requires Hibernate to build metamodel information. + * + * @param managedTypes managed types to use. + */ + public static AotEntityManagerFactoryCreator from(PersistenceManagedTypes managedTypes) { + return from(managedTypes, managedTypes); + } + + private static AotEntityManagerFactoryCreator from(PersistenceManagedTypes managedTypes, Object cacheKey) { + return from(() -> new AotMetamodel(managedTypes), cacheKey); + } + + /** + * Create a {@code PersistenceUnitContext} from the given {@link EntityManagerFactory}. + * + * @param entityManagerFactory the entity manager factory to use. + */ + public static AotEntityManagerFactoryCreator just(EntityManagerFactory entityManagerFactory) { + return new AotEntityManagerFactoryCreator(() -> entityManagerFactory, entityManagerFactory.getMetamodel()); + } + + private static AotEntityManagerFactoryCreator from(Supplier metamodel, Object key) { + return new AotEntityManagerFactoryCreator(() -> metamodel.get().getEntityManagerFactory(), key); + } + + private static boolean isJakartaAnnotated(Class cls) { + + return cls.isAnnotationPresent(Entity.class) // + || cls.isAnnotationPresent(Embeddable.class) // + || cls.isAnnotationPresent(MappedSuperclass.class) // + || cls.isAnnotationPresent(Converter.class); + } + + /** + * Return the {@link EntityManagerFactory}. + * + * @return the entity manager factory to use during AOT processing. + */ + public EntityManagerFactory getEntityManagerFactory() { + return factory.get(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AotEntityManagerFactoryCreator that)) { + return false; + } + return ObjectUtils.nullSafeEquals(key, that.key); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(key); + } + + @Override + public String toString() { + return "AotEntityManagerFactory{" + key + '}'; + } + +} 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 238d270de..812abe5e6 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.HibernatePersistenceProvider; 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 2c169efac..2627bc7e7 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 @@ -19,7 +19,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.PersistenceUnitUtil; import jakarta.persistence.metamodel.Metamodel; -import jakarta.persistence.spi.PersistenceUnitInfo; import java.lang.reflect.Method; import java.util.Map; @@ -61,7 +60,6 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ReturnedType; 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.StringUtils; @@ -77,39 +75,24 @@ import org.springframework.util.StringUtils; */ public class JpaRepositoryContributor extends RepositoryContributor { + private final AotRepositoryContext context; + private final EntityManagerFactory entityManagerFactory; 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, AotEntityManagerFactoryCreator.from(repositoryContext).getEntityManagerFactory()); } public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { - this(repositoryContext, entityManagerFactory, entityManagerFactory.getMetamodel()); - } - - private JpaRepositoryContributor(AotRepositoryContext repositoryContext, AotMetamodel metamodel) { - this(repositoryContext, metamodel.getEntityManagerFactory(), metamodel); - } - - private JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory, - Metamodel metamodel) { super(repositoryContext); - this.metamodel = metamodel; + this.entityManagerFactory = entityManagerFactory; + this.metamodel = entityManagerFactory.getMetamodel(); this.persistenceUnitUtil = entityManagerFactory.getPersistenceUnitUtil(); this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), entityManagerFactory, @@ -118,6 +101,10 @@ public class JpaRepositoryContributor extends RepositoryContributor { this.context = repositoryContext; } + public EntityManagerFactory getEntityManagerFactory() { + return entityManagerFactory; + } + @Override protected void customizeClass(AotRepositoryClassBuilder classBuilder) { classBuilder.customize(builder -> builder.superclass(TypeName.get(AotRepositoryFragmentSupport.class))); @@ -258,16 +245,13 @@ public class JpaRepositoryContributor extends RepositoryContributor { }); } - public Metamodel getMetamodel() { - return metamodel; - } - record StoredProcedureMetadata(String procedure) implements QueryMetadata { @Override public Map serialize() { return Map.of("procedure", procedure()); } + } record NamedStoredProcedureMetadata(String procedureName) implements QueryMetadata { @@ -276,6 +260,7 @@ public class JpaRepositoryContributor extends RepositoryContributor { 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 e61f0e13a..bd55bf3ec 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 @@ -60,6 +60,7 @@ import org.springframework.dao.DataAccessException; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.data.aot.AotContext; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.aot.AotEntityManagerFactoryCreator; import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor; import org.springframework.data.jpa.repository.support.DefaultJpaContext; import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension; @@ -75,6 +76,7 @@ import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; 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 class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi private static final String MODULE_NAME = "jpa"; + private final ConcurrentLruCache factoryCache = new ConcurrentLruCache<>( + 16, AotEntityManagerFactoryCreator::getEntityManagerFactory); + @Override protected void configureTypeContributions(AotRepositoryContext repositoryContext, GenerationContext generationContext) { @@ -421,7 +426,7 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi if (managedTypes != null) { log.debug("Using PersistenceManagedTypes for AOT repository generation"); - return new JpaRepositoryContributor(repositoryContext, managedTypes); + return contribute(repositoryContext, AotEntityManagerFactoryCreator.from(managedTypes)); } ObjectProvider infoProvider = beanFactory.getBeanProvider(PersistenceUnitInfo.class); @@ -430,11 +435,16 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi if (unitInfo != null) { log.debug("Using PersistenceUnitInfo for AOT repository generation"); - return new JpaRepositoryContributor(repositoryContext, unitInfo); + return contribute(repositoryContext, AotEntityManagerFactoryCreator.from(unitInfo)); } log.debug("Using scanned types for AOT repository generation"); - return new JpaRepositoryContributor(repositoryContext); + return contribute(repositoryContext, AotEntityManagerFactoryCreator.from(repositoryContext)); + } + + private JpaRepositoryContributor contribute(AotRepositoryContext repositoryContext, + AotEntityManagerFactoryCreator factory) { + return new JpaRepositoryContributor(repositoryContext, factoryCache.get(factory)); } } 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 25277bad6..e3f84b36d 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 @@ class JpaRepositoryRegistrationAotProcessorUnitTests { JpaRepositoryContributor contributor = new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() .contributeAotRepository(new DummyAotRepositoryContext(context)); - assertThat(contributor.getMetamodel().managedType(Person.class)).isNotNull(); + assertThat(contributor.getEntityManagerFactory().getMetamodel().managedType(Person.class)).isNotNull(); } @Test // GH-3899