From 8af529d590ec2bbd34c87840d4b3c214776686d0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 30 Jun 2025 09:12:26 +0200 Subject: [PATCH 1/2] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a6dc167a03..36976bdf5b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.0-GH-2595-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From 33939eb29320b57606f64169541eed9c1aeac934 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 30 Jun 2025 11:42:23 +0200 Subject: [PATCH 2/2] Explore including generated PersistentPropertyAccessorFactory and EntityInstantiator classes. We now pre-initialize ClassGeneratingPropertyAccessorFactory and ClassGeneratingEntityInstantiator infrastructure to generate bytecode for their respective classes so that we include the generated code for the target AOT package. Also, we check for presence of these types to conditionally load generated classes if these are on the classpath. This change required a stable class name therefore, we're hashing the fully-qualified class name and have aligned the class name from _Accessor to __Accessor (two underscores instead of one, same for Instantiator). --- .../data/aot/AotMappingContext.java | 83 +++++++++++++++++++ ...agedTypesBeanRegistrationAotProcessor.java | 7 ++ .../context/AbstractMappingContext.java | 3 +- ...backPersistentPropertyAccessorFactory.java | 49 +++++++++++ .../ClassGeneratingEntityInstantiator.java | 35 ++++++-- ...lassGeneratingPropertyAccessorFactory.java | 72 +++++++++++----- .../PersistentEntityClassInitializer.java | 26 ++++++ ...RepositoryRegistrationAotContribution.java | 65 +++++++++------ .../RepositoryRegistrationAotProcessor.java | 19 ++++- .../data/aot/CodeContributionAssert.java | 19 ++++- .../data/repository/aot/AotUtil.java | 76 +++++++++++++++++ ...neratedClassesCaptureIntegrationTests.java | 82 ++++++++++++++++++ ...toryRegistrationAotContributionAssert.java | 25 +++++- ...istrationAotProcessorIntegrationTests.java | 52 +----------- 14 files changed, 507 insertions(+), 106 deletions(-) create mode 100644 src/main/java/org/springframework/data/aot/AotMappingContext.java create mode 100644 src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java create mode 100644 src/main/java/org/springframework/data/mapping/model/PersistentEntityClassInitializer.java create mode 100644 src/test/java/org/springframework/data/repository/aot/AotUtil.java create mode 100644 src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java diff --git a/src/main/java/org/springframework/data/aot/AotMappingContext.java b/src/main/java/org/springframework/data/aot/AotMappingContext.java new file mode 100644 index 0000000000..3a240684dd --- /dev/null +++ b/src/main/java/org/springframework/data/aot/AotMappingContext.java @@ -0,0 +1,83 @@ +/* + * 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.aot; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.context.AbstractMappingContext; +import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.PersistentEntityClassInitializer; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.TypeInformation; + +/** + * Simple {@link AbstractMappingContext} for processing of AOT contributions. + * + * @author Mark Paluch + * @since 4.0 + */ +public class AotMappingContext extends + AbstractMappingContext, AotMappingContext.BasicPersistentProperty> { + + private final EntityInstantiators instantiators = new EntityInstantiators(); + private final ClassGeneratingPropertyAccessorFactory propertyAccessorFactory = new ClassGeneratingPropertyAccessorFactory(); + + /** + * Contribute entity instantiators and property accessors for the given {@link PersistentEntity} that are captured + * through Spring's {@code CglibClassHandler}. Otherwise, this is a no-op if contributions are not ran through + * {@code CglibClassHandler}. + * + * @param entity + */ + public void contribute(PersistentEntity entity) { + EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); + if (instantiator instanceof PersistentEntityClassInitializer pec) { + pec.initialize(entity); + } + propertyAccessorFactory.initialize(entity); + } + + @Override + protected BasicPersistentEntity createPersistentEntity( + TypeInformation typeInformation) { + return new BasicPersistentEntity<>(typeInformation); + } + + @Override + protected BasicPersistentProperty createPersistentProperty(Property property, + BasicPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + return new BasicPersistentProperty(property, owner, simpleTypeHolder); + } + + static class BasicPersistentProperty extends AnnotationBasedPersistentProperty { + + public BasicPersistentProperty(Property property, PersistentEntity owner, + SimpleTypeHolder simpleTypeHolder) { + super(property, owner, simpleTypeHolder); + } + + @Override + protected Association createAssociation() { + return null; + } + } + +} diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index 0bc6cd3ba6..0b865a8767 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -36,6 +36,7 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; import org.springframework.data.domain.ManagedTypes; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.util.Lazy; import org.springframework.data.util.QTypeContributor; import org.springframework.data.util.TypeContributor; @@ -56,6 +57,7 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio private final Log logger = LogFactory.getLog(getClass()); private @Nullable String moduleIdentifier; private Lazy environment = Lazy.of(StandardEnvironment::new); + private final AotMappingContext aotMappingContext = new AotMappingContext(); public void setModuleIdentifier(@Nullable String moduleIdentifier) { this.moduleIdentifier = moduleIdentifier; @@ -150,6 +152,11 @@ protected void contributeType(ResolvableType type, GenerationContext generationC TypeContributor.contribute(resolvedType, annotationNamespaces, generationContext); QTypeContributor.contributeEntityPath(resolvedType, generationContext, resolvedType.getClassLoader()); + PersistentEntity entity = aotMappingContext.getPersistentEntity(resolvedType); + if (entity != null) { + aotMappingContext.contribute(entity); + } + TypeUtils.resolveUsedAnnotations(resolvedType).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); } diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index 5ba05b4a02..9a312ca380 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -56,7 +56,6 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.InstantiationAwarePropertyAccessorFactory; @@ -125,7 +124,7 @@ protected AbstractMappingContext() { EntityInstantiators instantiators = new EntityInstantiators(); PersistentPropertyAccessorFactory accessorFactory = NativeDetector.inNativeImage() - ? BeanWrapperPropertyAccessorFactory.INSTANCE + ? new ReflectionFallbackPersistentPropertyAccessorFactory() : new ClassGeneratingPropertyAccessorFactory(); this.persistentPropertyAccessorFactory = new InstantiationAwarePropertyAccessorFactory(accessorFactory, diff --git a/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java new file mode 100644 index 0000000000..6640f925e3 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java @@ -0,0 +1,49 @@ +/* + * 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.mapping.context; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; +import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory; +import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory; + +/** + * {@link PersistentPropertyAccessorFactory} that uses {@link ClassGeneratingPropertyAccessorFactory} if + * {@link ClassGeneratingPropertyAccessorFactory#isSupported(PersistentEntity) supported} and falls back to reflection. + * + * @author Mark Paluch + * @since 4.0 + */ +class ReflectionFallbackPersistentPropertyAccessorFactory implements PersistentPropertyAccessorFactory { + + private final ClassGeneratingPropertyAccessorFactory accessorFactory = new ClassGeneratingPropertyAccessorFactory(); + + @Override + public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { + + if (accessorFactory.isSupported(entity)) { + return accessorFactory.getPropertyAccessor(entity, bean); + } + + return BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(entity, bean); + } + + @Override + public boolean isSupported(PersistentEntity entity) { + return true; + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java index 952fa0e9a6..353336f816 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java @@ -51,7 +51,7 @@ * An {@link EntityInstantiator} that can generate byte code to speed-up dynamic object instantiation. Uses the * {@link PersistentEntity}'s {@link PreferredConstructor} to instantiate an instance of the entity by dynamically * generating factory methods with appropriate constructor invocations via ASM. If we cannot generate byte code for a - * type, we gracefully fallback to the {@link ReflectionEntityInstantiator}. + * type, we gracefully fall back to the {@link ReflectionEntityInstantiator}. * * @author Thomas Darimont * @author Oliver Gierke @@ -60,7 +60,7 @@ * @author Mark Paluch * @since 1.11 */ -class ClassGeneratingEntityInstantiator implements EntityInstantiator { +class ClassGeneratingEntityInstantiator implements EntityInstantiator, PersistentEntityClassInitializer { private static final Log LOGGER = LogFactory.getLog(ClassGeneratingEntityInstantiator.class); @@ -87,17 +87,29 @@ public ClassGeneratingEntityInstantiator() { this.fallbackToReflectionOnError = fallbackToReflectionOnError; } + @Override + public void initialize(PersistentEntity entity) { + getEntityInstantiator(entity); + } + @Override public , P extends PersistentProperty

> T createInstance(E entity, ParameterValueProvider

provider) { + EntityInstantiator instantiator = getEntityInstantiator(entity); + return instantiator.createInstance(entity, provider); + } + + private , P extends PersistentProperty

> EntityInstantiator getEntityInstantiator( + E entity) { + EntityInstantiator instantiator = this.entityInstantiators.get(entity.getTypeInformation()); if (instantiator == null) { instantiator = potentiallyCreateAndRegisterEntityInstantiator(entity); } - return instantiator.createInstance(entity, provider); + return instantiator; } /** @@ -170,10 +182,19 @@ protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity e */ boolean shouldUseReflectionEntityInstantiator(PersistentEntity entity) { + String accessorClassName = ObjectInstantiatorClassGenerator.generateClassName(entity); + + // already present in classloader + if (ClassUtils.isPresent(accessorClassName, entity.getType().getClassLoader())) { + return false; + } + if (NativeDetector.inNativeImage()) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("graalvm.nativeimage - fall back to reflection for %s", entity.getName())); + LOGGER.debug(String.format( + "[org.graalvm.nativeimage.imagecode=true] and no AOT-generated EntityInstantiator for %s. Falling back to reflection.", + entity.getName())); } return true; @@ -388,7 +409,7 @@ public , P extends PersistentPrope static class ObjectInstantiatorClassGenerator { private static final String INIT = ""; - private static final String TAG = "_Instantiator_"; + private static final String TAG = "__Instantiator_"; private static final String JAVA_LANG_OBJECT = Type.getInternalName(Object.class); private static final String CREATE_METHOD_NAME = "newInstance"; @@ -431,8 +452,8 @@ public Class generateCustomInstantiatorClass(PersistentEntity entity, * @param entity * @return */ - private String generateClassName(PersistentEntity entity) { - return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36); + static String generateClassName(PersistentEntity entity) { + return entity.getType().getName() + TAG + Integer.toString(Math.abs(entity.getType().getName().hashCode()), 36); } /** diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java index 0e6047d7aa..8c874d47f4 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java @@ -76,7 +76,8 @@ * @author Johannes Englmeier * @since 1.13 */ -public class ClassGeneratingPropertyAccessorFactory implements PersistentPropertyAccessorFactory { +public class ClassGeneratingPropertyAccessorFactory + implements PersistentPropertyAccessorFactory, PersistentEntityClassInitializer { // Pooling of parameter arrays to prevent excessive object allocation. private final ThreadLocal argumentCache = ThreadLocal.withInitial(() -> new Object[1]); @@ -89,20 +90,14 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert 256, KotlinValueBoxingAdapter::getWrapper); @Override - public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { - - Constructor constructor = constructorMap.get(entity); + public void initialize(PersistentEntity entity) { + getPropertyAccessorConstructor(entity); + } - if (constructor == null) { + @Override + public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { - Class> accessorClass = potentiallyCreateAndRegisterPersistentPropertyAccessorClass( - entity); - constructor = accessorClass.getConstructors()[0]; - - Map, Constructor> constructorMap = new HashMap<>(this.constructorMap); - constructorMap.put(entity, constructor); - this.constructorMap = constructorMap; - } + Constructor constructor = getPropertyAccessorConstructor(entity); Object[] args = argumentCache.get(); args[0] = bean; @@ -123,6 +118,24 @@ public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity getPropertyAccessorConstructor(PersistentEntity entity) { + + Constructor constructor = constructorMap.get(entity); + + if (constructor == null) { + + Class> accessorClass = potentiallyCreateAndRegisterPersistentPropertyAccessorClass( + entity); + constructor = accessorClass.getConstructors()[0]; + + Map, Constructor> constructorMap = new HashMap<>(this.constructorMap); + constructorMap.put(entity, constructor); + this.constructorMap = constructorMap; + } + + return constructor; + } + /** * Checks whether an accessor class can be generated. * @@ -136,6 +149,11 @@ public boolean isSupported(PersistentEntity entity) { Assert.notNull(entity, "PersistentEntity must not be null"); + // already present in classloader + if (findAccessorClass(entity) != null) { + return true; + } + return isClassLoaderDefineClassAvailable(entity) && isTypeInjectable(entity) && hasUniquePropertyHashCodes(entity); } @@ -184,7 +202,7 @@ private boolean hasUniquePropertyHashCodes(PersistentEntity entity) { /** * @param entity must not be {@literal null}. */ - private synchronized Class> potentiallyCreateAndRegisterPersistentPropertyAccessorClass( + protected synchronized Class> potentiallyCreateAndRegisterPersistentPropertyAccessorClass( PersistentEntity entity) { Map, Class>> map = this.propertyAccessorClasses; @@ -194,7 +212,7 @@ private synchronized Class> potentiallyCreateAndRe return propertyAccessorClass; } - propertyAccessorClass = createAccessorClass(entity); + propertyAccessorClass = loadOrCreateAccessorClass(entity); map = new HashMap<>(map); map.put(entity.getTypeInformation(), propertyAccessorClass); @@ -204,16 +222,29 @@ private synchronized Class> potentiallyCreateAndRe return propertyAccessorClass; } - @SuppressWarnings("unchecked") - private Class> createAccessorClass(PersistentEntity entity) { + @SuppressWarnings({ "unchecked" }) + private Class> loadOrCreateAccessorClass(PersistentEntity entity) { try { + + Class accessorClass = findAccessorClass(entity); + if (accessorClass != null) { + return (Class>) accessorClass; + } + return (Class>) PropertyAccessorClassGenerator.generateCustomAccessorClass(entity); } catch (Exception e) { throw new RuntimeException(e); } } + private static @Nullable Class findAccessorClass(PersistentEntity entity) { + + String accessorClassName = PropertyAccessorClassGenerator.generateClassName(entity); + + return org.springframework.data.util.ClassUtils.loadIfPresent(accessorClassName, entity.getType().getClassLoader()); + } + /** * Generates {@link PersistentPropertyAccessor} classes to access properties of a {@link PersistentEntity}. This code * uses {@code private static final} held method handles which perform about the speed of native method invocations @@ -306,7 +337,7 @@ static class PropertyAccessorClassGenerator { private static final String INIT = ""; private static final String CLINIT = ""; - private static final String TAG = "_Accessor_"; + private static final String TAG = "__Accessor_"; private static final String JAVA_LANG_OBJECT = "java/lang/Object"; private static final String JAVA_LANG_STRING = "java/lang/String"; private static final String JAVA_LANG_REFLECT_METHOD = "java/lang/reflect/Method"; @@ -347,7 +378,6 @@ static Class generateCustomAccessorClass(PersistentEntity entity) { try { return ReflectUtils.defineClass(className, bytecode, classLoader, type.getProtectionDomain(), type); - } catch (Exception o_O) { throw new IllegalStateException(o_O); } @@ -1372,8 +1402,8 @@ private static int classVariableIndex5(List> list, Class item) { return 5 + list.indexOf(item); } - private static String generateClassName(PersistentEntity entity) { - return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36); + static String generateClassName(PersistentEntity entity) { + return entity.getType().getName() + TAG + Integer.toString(Math.abs(entity.getType().getName().hashCode()), 36); } } diff --git a/src/main/java/org/springframework/data/mapping/model/PersistentEntityClassInitializer.java b/src/main/java/org/springframework/data/mapping/model/PersistentEntityClassInitializer.java new file mode 100644 index 0000000000..cdf025bd73 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityClassInitializer.java @@ -0,0 +1,26 @@ +/* + * 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.mapping.model; + +import org.springframework.data.mapping.PersistentEntity; + +/** + * @author Mark Paluch + */ +public interface PersistentEntityClassInitializer { + + void initialize(PersistentEntity entity); +} diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java index e8e102e315..df14e74519 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -43,6 +43,8 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; import org.springframework.data.aot.AotContext; +import org.springframework.data.aot.AotMappingContext; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.projection.EntityProjectionIntrospector; import org.springframework.data.projection.TargetAware; import org.springframework.data.repository.Repository; @@ -72,6 +74,8 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = "org.springframework.data.repository.kotlin.CoroutineCrudRepository"; + private final AotMappingContext aotMappingContext = new AotMappingContext(); + private final RepositoryRegistrationAotProcessor aotProcessor; private final AotRepositoryContext repositoryContext; @@ -277,33 +281,16 @@ private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, Ge QTypeContributor.contributeEntityPath(repositoryInformation.getDomainType(), contribution, repositoryContext.getClassLoader()); - // Repository Fragments - for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { - - Class repositoryFragmentType = fragment.getSignatureContributor(); - Optional> implementation = fragment.getImplementationClass(); - - contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { - - hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - - if (!repositoryFragmentType.isInterface()) { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - } - }); - - implementation.ifPresent(typeToRegister -> { - contribution.getRuntimeHints().reflection().registerType(typeToRegister, hint -> { - - hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - - if (!typeToRegister.isInterface()) { - hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - } - }); - }); + // TODO: what about embedded types or entity types that are entity types references from properties? + PersistentEntity persistentEntity = aotMappingContext + .getPersistentEntity(repositoryInformation.getDomainType()); + if (persistentEntity != null) { + aotMappingContext.contribute(persistentEntity); } + // Repository Fragments + contributeFragments(contribution); + // Repository Proxy contribution.getRuntimeHints().proxies().registerJdkProxy(repositoryInformation.getRepositoryInterface(), SpringProxy.class, Advised.class, DecoratingProxy.class); @@ -345,6 +332,34 @@ private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, Ge }); } + private void contributeFragments(GenerationContext contribution) { + for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { + + Class repositoryFragmentType = fragment.getSignatureContributor(); + Optional> implementation = fragment.getImplementationClass(); + + contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { + + hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); + + if (!repositoryFragmentType.isInterface()) { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } + }); + + implementation.ifPresent(typeToRegister -> { + contribution.getRuntimeHints().reflection().registerType(typeToRegister, hint -> { + + hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); + + if (!typeToRegister.isInterface()) { + hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } + }); + }); + } + } + private boolean isComponentAnnotatedRepository(RepositoryInformation repositoryInformation) { return AnnotationUtils.findAnnotation(repositoryInformation.getRepositoryInterface(), Component.class) != null; } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java index 4fbb086106..535940d0d8 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -44,6 +44,8 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; +import org.springframework.data.aot.AotMappingContext; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -77,6 +79,8 @@ public class RepositoryRegistrationAotProcessor private final Log logger = LogFactory.getLog(getClass()); + private final AotMappingContext aotMappingContext = new AotMappingContext(); + private @Nullable ConfigurableListableBeanFactory beanFactory; private Environment environment = new StandardEnvironment(); @@ -88,6 +92,7 @@ public class RepositoryRegistrationAotProcessor return isRepositoryBean(bean) ? newRepositoryRegistrationAotContribution(bean) : null; } + @Nullable protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { @@ -109,6 +114,8 @@ protected RepositoryContributor contribute(AotRepositoryContext repositoryContex * @param repositoryContext must not be {@literal null}. * @param generationContext must not be {@literal null}. */ + // TODO: Can we merge #contribute, #registerReflectiveForAggregateRoot into RepositoryRegistrationAotContribution? + // hints and types are contributed from everywhere. private void registerReflectiveForAggregateRoot(AotRepositoryContext repositoryContext, GenerationContext generationContext) { @@ -117,7 +124,16 @@ private void registerReflectiveForAggregateRoot(AotRepositoryContext repositoryC RuntimeHints hints = generationContext.getRuntimeHints(); Stream.concat(Stream.of(information.getDomainType()), information.getAlternativeDomainTypes().stream()) - .forEach(it -> registrar.registerRuntimeHints(hints, it)); + .forEach(it -> { + + // arent we already registering the types in RepositoryRegistrationAotContribution#contributeRepositoryInfo? + registrar.registerRuntimeHints(hints, it); + + PersistentEntity persistentEntity = aotMappingContext.getPersistentEntity(it); + if (persistentEntity != null) { + aotMappingContext.contribute(persistentEntity); + } + }); } private boolean isRepositoryBean(RegisteredBean bean) { @@ -186,6 +202,7 @@ protected ConfigurableListableBeanFactory getBeanFactory() { protected void contributeType(Class type, GenerationContext generationContext) { TypeContributor.contribute(type, it -> true, generationContext); + } protected Log getLogger() { diff --git a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java index 1bf8817bb8..ac15597ed6 100644 --- a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java +++ b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java @@ -15,15 +15,18 @@ */ package org.springframework.data.aot; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.assertj.core.api.AbstractAssert; + import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.JdkProxyHint; +import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; @@ -33,6 +36,7 @@ * * @author Christoph Strobl * @author John Blum + * @author Mark Paluch * @since 3.0 */ @SuppressWarnings("UnusedReturnValue") @@ -52,6 +56,19 @@ public CodeContributionAssert contributesReflectionFor(Class... types) { return this; } + public CodeContributionAssert contributesReflectionFor(TypeReference typeReference) { + + assertThat(this.actual.getRuntimeHints()).describedAs(() -> { + + return "Existing hints: " + System.lineSeparator() + this.actual().getRuntimeHints().reflection().typeHints() + .map(TypeHint::toString).map(" - "::concat).collect(Collectors.joining(System.lineSeparator())); + + }).matches(RuntimeHintsPredicates.reflection().onType(typeReference), + String.format("No reflection entry found for [%s]", typeReference)); + + return this; + } + public CodeContributionAssert contributesReflectionFor(String... types) { for (String type : types) { diff --git a/src/test/java/org/springframework/data/repository/aot/AotUtil.java b/src/test/java/org/springframework/data/repository/aot/AotUtil.java new file mode 100644 index 0000000000..0ce1763a40 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/AotUtil.java @@ -0,0 +1,76 @@ +/* + * 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.repository.aot; + +import static org.assertj.core.api.Assertions.*; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; +import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; + +/** + * Utility class to create {@link RepositoryRegistrationAotContribution} instances for a given configuration class. + * + * @author Mark Paluch + */ +class AotUtil { + + static RepositoryRegistrationAotContributionBuilder contributionFor(Class configuration) { + return contributionFor(configuration, new AnnotationConfigApplicationContext()); + } + + static RepositoryRegistrationAotContributionBuilder contributionFor(Class configuration, + AnnotationConfigApplicationContext applicationContext) { + + applicationContext.register(configuration); + applicationContext.refreshForAotProcessing(new RuntimeHints()); + + return repositoryType -> { + + String[] repositoryBeanNames = applicationContext.getBeanNamesForType(repositoryType); + + assertThat(repositoryBeanNames) + .describedAs("Unable to find repository [%s] in configuration [%s]", repositoryType, configuration) + .hasSize(1); + + String repositoryBeanName = repositoryBeanNames[0]; + + ConfigurableListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); + + RepositoryRegistrationAotProcessor repositoryAotProcessor = applicationContext + .getBean(RepositoryRegistrationAotProcessor.class); + + repositoryAotProcessor.setBeanFactory(beanFactory); + + RegisteredBean bean = RegisteredBean.of(beanFactory, repositoryBeanName); + + BeanRegistrationAotContribution beanContribution = repositoryAotProcessor.processAheadOfTime(bean); + + assertThat(beanContribution).isInstanceOf(RepositoryRegistrationAotContribution.class); + + return (RepositoryRegistrationAotContribution) beanContribution; + }; + } + + @FunctionalInterface + interface RepositoryRegistrationAotContributionBuilder { + RepositoryRegistrationAotContribution forRepository(Class repositoryInterface); + } +} diff --git a/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java b/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java new file mode 100644 index 0000000000..39674dd06a --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022-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.repository.aot; + +import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.*; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.TypeReference; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.EnableRepositories; +import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; +import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; + +/** + * Integration Tests for {@link RepositoryRegistrationAotProcessor} to verify capturing generated instantiations and + * property accessors. + * + * @author Mark Paluch + */ +public class GeneratedClassesCaptureIntegrationTests { + + @Test // GH-2595 + void registersGeneratedPropertyAccessorsEntityInstantiators() { + + RepositoryRegistrationAotContribution repositoryBeanContribution = AotUtil.contributionFor(Config.class) + .forRepository(Config.MyRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + contribution.contributesReflectionFor(TypeReference.of( + "org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests$Config$Person__Accessor_xj7ohs")); + contribution.contributesReflectionFor(TypeReference.of( + "org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests$Config$Person__Instantiator_xj7ohs")); + + // TODO: These should also appear + /* + contribution.contributesReflectionFor(TypeReference.of( + "org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests$Config$Address__Accessor_xj7ohs")); + contribution.contributesReflectionFor(TypeReference.of( + "org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests$Config$Address__Instantiator_xj7ohs")); + */ + }); + } + + @EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = Config.MyRepo.class) }, + basePackageClasses = Config.class, considerNestedRepositories = true) + public class Config { + + public interface MyRepo extends CrudRepository { + + } + + public static class Person { + + @Nullable Address address; + + } + + public static class Address { + String street; + } + + } + +} diff --git a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java index 55c2d86ea4..8e73433867 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java @@ -18,15 +18,20 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import org.assertj.core.api.AbstractAssert; import org.junit.jupiter.api.function.ThrowingConsumer; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.context.aot.ApplicationContextAotGenerator; import org.springframework.data.aot.CodeContributionAssert; import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; import org.springframework.data.repository.core.RepositoryInformation; @@ -113,9 +118,27 @@ public RepositoryRegistrationAotContributionAssert codeContributionSatisfies( GenerationContext generationContext = new TestGenerationContext(Object.class); - this.actual.applyTo(generationContext, mockBeanRegistrationCode); + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); try { + Class handlerClass = Class.forName("org.springframework.context.aot.CglibClassHandler"); + Constructor constructor = handlerClass.getDeclaredConstructors()[0]; + constructor.setAccessible(true); + Object handler = BeanUtils.instantiateClass(constructor, generationContext); + + Method withCglibClassHandler = generator.getClass().getDeclaredMethod("withCglibClassHandler", handlerClass, + Supplier.class); + withCglibClassHandler.setAccessible(true); + withCglibClassHandler.invoke(generator, handler, new Supplier() { + + @Override + public Object get() { + + actual.applyTo(generationContext, mockBeanRegistrationCode); + return null; + } + }); + assertWith.accept(new CodeContributionAssert(generationContext)); } catch (Throwable o_O) { fail(o_O.getMessage(), o_O); diff --git a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java index bb71245359..d71d0325fb 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -15,7 +15,6 @@ */ package org.springframework.data.repository.aot; -import static org.assertj.core.api.Assertions.*; import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.*; import java.io.Serializable; @@ -24,11 +23,6 @@ import org.springframework.aop.SpringProxy; import org.springframework.aop.framework.Advised; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; import org.springframework.core.DecoratingProxy; @@ -65,6 +59,7 @@ * @author Christoph Strobl * @author John Blum */ +// TODO: This is verifying repository.config code. Move to repository.config package? public class RepositoryRegistrationAotProcessorIntegrationTests { @Test // GH-2593 @@ -296,8 +291,7 @@ void registersQTypeIfPresent() { assertThatContribution(repositoryBeanContribution) // .codeContributionSatisfies(contribution -> { contribution.contributesReflectionFor(Person.class); - contribution.contributesReflectionFor( - QConfigWithQuerydslPredicateExecutor_Person.class); + contribution.contributesReflectionFor(QConfigWithQuerydslPredicateExecutor_Person.class); }); } @@ -325,46 +319,8 @@ void registersReflectionForInheritedDomainPublicationAnnotations() { }); } - RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration) { - return computeAotConfiguration(configuration, new AnnotationConfigApplicationContext()); - } - - RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration, - AnnotationConfigApplicationContext applicationContext) { - - applicationContext.register(configuration); - applicationContext.refreshForAotProcessing(new RuntimeHints()); - - return repositoryType -> { - - String[] repositoryBeanNames = applicationContext.getBeanNamesForType(repositoryType); - - assertThat(repositoryBeanNames) - .describedAs("Unable to find repository [%s] in configuration [%s]", repositoryType, configuration) - .hasSize(1); - - String repositoryBeanName = repositoryBeanNames[0]; - - ConfigurableListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); - - RepositoryRegistrationAotProcessor repositoryAotProcessor = applicationContext - .getBean(RepositoryRegistrationAotProcessor.class); - - repositoryAotProcessor.setBeanFactory(beanFactory); - - RegisteredBean bean = RegisteredBean.of(beanFactory, repositoryBeanName); - - BeanRegistrationAotContribution beanContribution = repositoryAotProcessor.processAheadOfTime(bean); - - assertThat(beanContribution).isInstanceOf(RepositoryRegistrationAotContribution.class); - - return (RepositoryRegistrationAotContribution) beanContribution; - }; - } - - @FunctionalInterface - interface RepositoryRegistrationAotContributionBuilder { - RepositoryRegistrationAotContribution forRepository(Class repositoryInterface); + AotUtil.RepositoryRegistrationAotContributionBuilder computeAotConfiguration(Class configuration) { + return AotUtil.contributionFor(configuration); } @EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = SampleRepository.class) },