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. 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) },