From 53d8739fcf7f48c6edbc8f7e228da7a33cb0fc8d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 30 Jun 2025 09:12:26 +0200 Subject: [PATCH 1/6] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13143c9f6f..948f658b11 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 465feaecfa020e54b0a12d305013ec2171806a66 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 30 Jun 2025 11:42:23 +0200 Subject: [PATCH 2/6] 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 bd2abb6706..1a53247cce 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; @@ -73,6 +75,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; @@ -275,33 +279,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); @@ -343,6 +330,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) }, From c1e19154e18d4f3ebe9993274a3f0fdb01086953 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 21 Aug 2025 14:23:19 +0200 Subject: [PATCH 3/6] Add AOT type configuration API Update tests and add convenience API for registering configurations required to interact with certain types. --- .../springframework/data/aot/AotContext.java | 22 ++- .../data/aot/AotMappingContext.java | 24 ++- .../data/aot/AotTypeConfiguration.java | 110 ++++++++++++ .../data/aot/DefaultAotContext.java | 162 ++++++++++++++++++ ...agedTypesBeanRegistrationAotProcessor.java | 26 +-- ...nagedTypesRegistrationAotContribution.java | 4 +- .../context/AbstractMappingContext.java | 1 + ...backPersistentPropertyAccessorFactory.java | 6 +- .../config/AotRepositoryContext.java | 2 + .../config/DefaultAotRepositoryContext.java | 27 +++ ...RepositoryRegistrationAotContribution.java | 88 ++-------- .../RepositoryRegistrationAotProcessor.java | 10 +- .../data/aot/AotContextUnitTests.java | 19 ++ .../data/aot/AotMappingContextUnitTests.java | 44 +++++ .../data/repository/aot/AotUtil.java | 37 ++-- ...neratedClassesCaptureIntegrationTests.java | 104 +++++++++-- ...toryRegistrationAotContributionAssert.java | 3 +- ...istrationAotProcessorIntegrationTests.java | 8 +- .../DummyModuleAotRepositoryContext.java | 23 +++ 19 files changed, 577 insertions(+), 143 deletions(-) create mode 100644 src/main/java/org/springframework/data/aot/AotTypeConfiguration.java create mode 100644 src/test/java/org/springframework/data/aot/AotMappingContextUnitTests.java diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java index 67f423ae60..7febea0da5 100644 --- a/src/main/java/org/springframework/data/aot/AotContext.java +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -22,14 +22,17 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; import org.jspecify.annotations.Nullable; +import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ResolvableType; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; @@ -213,6 +216,20 @@ default IntrospectedBeanDefinition introspectBeanDefinition(BeanReference refere */ IntrospectedBeanDefinition introspectBeanDefinition(String beanName); + InstantiationCreator instantiationCreator(TypeReference typeReference); + + default AotTypeConfiguration typeConfiguration(ResolvableType resolvableType) { + return typeConfiguration(resolvableType.toClass()); + } + + default AotTypeConfiguration typeConfiguration(Class type) { + return typeConfiguration(TypeReference.of(type)); + } + + AotTypeConfiguration typeConfiguration(TypeReference typeReference); + + Collection typeConfigurations(); + /** * Type-based introspector to resolve {@link Class} from a type name and to introspect the bean factory for presence * of beans. @@ -272,7 +289,6 @@ default void ifTypePresent(Consumer> action) { * @return a {@link List} of bean names. The list is empty if the bean factory does not hold any beans of this type. */ List getBeanNames(); - } /** @@ -326,7 +342,11 @@ interface IntrospectedBeanDefinition { */ @Nullable Class resolveType(); + } + interface InstantiationCreator { + boolean isAvailable(); + void create(); } } diff --git a/src/main/java/org/springframework/data/aot/AotMappingContext.java b/src/main/java/org/springframework/data/aot/AotMappingContext.java index 3a240684dd..4b18eb77d4 100644 --- a/src/main/java/org/springframework/data/aot/AotMappingContext.java +++ b/src/main/java/org/springframework/data/aot/AotMappingContext.java @@ -15,6 +15,8 @@ */ package org.springframework.data.aot; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.AbstractMappingContext; @@ -26,6 +28,7 @@ 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.repository.aot.generate.RepositoryContributor; import org.springframework.data.util.TypeInformation; /** @@ -34,9 +37,12 @@ * @author Mark Paluch * @since 4.0 */ -public class AotMappingContext extends +class AotMappingContext extends // TODO: hide this one and delegate to other component - can we use the + // AotContext for it? AbstractMappingContext, AotMappingContext.BasicPersistentProperty> { + private static final Log logger = LogFactory.getLog(AotMappingContext.class); + private final EntityInstantiators instantiators = new EntityInstantiators(); private final ClassGeneratingPropertyAccessorFactory propertyAccessorFactory = new ClassGeneratingPropertyAccessorFactory(); @@ -55,15 +61,18 @@ public void contribute(PersistentEntity entity) { propertyAccessorFactory.initialize(entity); } + // TODO: can we extract some util for this using only type @Override protected BasicPersistentEntity createPersistentEntity( TypeInformation typeInformation) { + logger.debug("I hate gradle: create persistent entity for type: " + typeInformation); return new BasicPersistentEntity<>(typeInformation); } @Override protected BasicPersistentProperty createPersistentProperty(Property property, BasicPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + logger.info("creating property: " + property.getName()); return new BasicPersistentProperty(property, owner, simpleTypeHolder); } @@ -74,10 +83,21 @@ public BasicPersistentProperty(Property property, PersistentEntity createAssociation() { - return null; + return new Association<>(this, null); } + + @Override + public Association getRequiredAssociation() { + return new Association<>(this, null); + } + } } diff --git a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java new file mode 100644 index 0000000000..1dbc60a69f --- /dev/null +++ b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java @@ -0,0 +1,110 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.aot; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.aop.SpringProxy; +import org.springframework.aop.framework.Advised; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.TypeReference; +import org.springframework.core.DecoratingProxy; +import org.springframework.core.env.Environment; +import org.springframework.data.projection.TargetAware; + +/** + * @author Christoph Strobl + */ +public interface AotTypeConfiguration { + + AotTypeConfiguration forDataBinding(); + + AotTypeConfiguration forReflectiveAccess(MemberCategory... categories); + + AotTypeConfiguration generateEntityInstantiator(); + + // TODO: ? should this be a global condition for the entire configuration or do we need it for certain aspects ? + AotTypeConfiguration conditional(Predicate filter); + + default AotTypeConfiguration usedAsProjectionInterface() { + return proxyInterface(TargetAware.class, SpringProxy.class, DecoratingProxy.class); + } + + default AotTypeConfiguration springProxy() { + return proxyInterface(SpringProxy.class, Advised.class, DecoratingProxy.class); + } + + default AotTypeConfiguration repositoryProxy() { + + springProxy(); + + List transactionalProxy = List.of(TypeReference.of("org.springframework.data.repository.Repository"), + TypeReference.of("org.springframework.transaction.interceptor.TransactionalProxy"), + TypeReference.of("org.springframework.aop.framework.Advised"), TypeReference.of(DecoratingProxy.class)); + proxyInterface(transactionalProxy); + + proxyInterface( + Stream.concat(transactionalProxy.stream(), Stream.of(TypeReference.of(Serializable.class))).toList()); + + return this; + } + + AotTypeConfiguration proxyInterface(List proxyInterfaces); + + default AotTypeConfiguration proxyInterface(TypeReference... proxyInterfaces) { + return proxyInterface(List.of(proxyInterfaces)); + } + + default AotTypeConfiguration proxyInterface(Class... proxyInterfaces) { + return proxyInterface(Stream.of(proxyInterfaces).map(TypeReference::of).toList()); + } + + AotTypeConfiguration forQuerydsl(); + + void contribute(GenerationContext generationContext); + + static Predicate userConfiguredCondition(Environment environment) { + + return new Predicate() { + + private final List allowedAccessorTypes = environment.getProperty("spring.data.aot.generate.accessor", + List.class, List.of()); + + @Override + @SuppressWarnings("unchecked") + public boolean test(TypeReference typeReference) { + + if (!allowedAccessorTypes.isEmpty()) { + if (allowedAccessorTypes.contains("none") || allowedAccessorTypes.contains("false") + || allowedAccessorTypes.contains("off")) { + return false; + } + if (!allowedAccessorTypes.contains(typeReference.getName())) { + return false; + } + } + + return true; + } + }; + } + +} diff --git a/src/main/java/org/springframework/data/aot/DefaultAotContext.java b/src/main/java/org/springframework/data/aot/DefaultAotContext.java index 65cb8ab544..a60f87af4a 100644 --- a/src/main/java/org/springframework/data/aot/DefaultAotContext.java +++ b/src/main/java/org/springframework/data/aot/DefaultAotContext.java @@ -15,20 +15,37 @@ */ package org.springframework.data.aot; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; import org.jspecify.annotations.Nullable; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.data.aot.AotMappingContext.BasicPersistentProperty; +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.util.Lazy; import org.springframework.core.env.Environment; +import org.springframework.data.util.QTypeContributor; +import org.springframework.data.util.TypeContributor; import org.springframework.util.ClassUtils; /** @@ -39,8 +56,13 @@ */ class DefaultAotContext implements AotContext { + private final AotMappingContext mappingContext = new AotMappingContext(); private final ConfigurableListableBeanFactory factory; + // TODO: should we reuse the config or potentially have multiple ones with different settings - somehow targets the + // filtering issue + private final Map typeConfigurations = new HashMap<>(); + private final Environment environment; public DefaultAotContext(BeanFactory beanFactory, Environment environment) { @@ -69,6 +91,21 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return new DefaultIntrospectedBeanDefinition(beanName); } + @Override + public InstantiationCreator instantiationCreator(TypeReference typeReference) { + return new DefaultInstantiationCreator(introspectType(typeReference.getName())); + } + + @Override + public AotTypeConfiguration typeConfiguration(TypeReference typeReference) { + return typeConfigurations.computeIfAbsent(typeReference, it -> new ContextualTypeConfiguration(typeReference)); + } + + @Override + public Collection typeConfigurations() { + return typeConfigurations.values(); + } + class DefaultTypeIntrospector implements TypeIntrospector { private final String typeName; @@ -108,6 +145,29 @@ public List getBeanNames() { } } + class DefaultInstantiationCreator implements InstantiationCreator { + + Lazy> entity; + + public DefaultInstantiationCreator(TypeIntrospector typeIntrospector) { + this.entity = Lazy.of(() -> mappingContext.getPersistentEntity(typeIntrospector.resolveRequiredType())); + } + + @Override + public boolean isAvailable() { + return entity.getNullable() != null; + } + + @Override + public void create() { + + BasicPersistentEntity persistentEntity = entity.getNullable(); + if (persistentEntity != null) { + mappingContext.contribute(persistentEntity); + } + } + } + class DefaultIntrospectedBeanDefinition implements IntrospectedBeanDefinition { private final String beanName; @@ -148,4 +208,106 @@ public RootBeanDefinition getRootBeanDefinition() throws NoSuchBeanDefinitionExc } } + class ContextualTypeConfiguration implements AotTypeConfiguration { + + private final TypeReference type; + private boolean forDataBinding = false; + private final Set categories = new HashSet<>(5); + private boolean generateEntityInstantiator = false; + private boolean forQuerydsl = false; + private final List> proxies = new ArrayList<>(); + private Predicate filter; + + ContextualTypeConfiguration(TypeReference type) { + this.type = type; + } + + @Override + public AotTypeConfiguration forDataBinding() { + this.forDataBinding = true; + return this; + } + + @Override + public AotTypeConfiguration forReflectiveAccess(MemberCategory... categories) { + this.categories.addAll(Arrays.asList(categories)); + return this; + } + + @Override + public AotTypeConfiguration generateEntityInstantiator() { + this.generateEntityInstantiator = true; + return this; + } + + @Override + public AotTypeConfiguration proxyInterface(List interfaces) { + this.proxies.add(interfaces); + return this; + } + + @Override + public AotTypeConfiguration forQuerydsl() { + this.forQuerydsl = true; + return this; + } + + @Override + public AotTypeConfiguration conditional(Predicate filter) { + + this.filter = filter; + return this; + } + + @Override + public void contribute(GenerationContext generationContext) { + + if (filter != null && !filter.test(this.type)) { + return; + } + + if (!this.categories.isEmpty()) { + generationContext.getRuntimeHints().reflection().registerType(this.type, + categories.toArray(MemberCategory[]::new)); + } + + if (generateEntityInstantiator) { + instantiationCreator(type).create(); + } + + if (forDataBinding) { + if (!doIfPresent(resolved -> TypeContributor.contribute(resolved, Set.of(TypeContributor.DATA_NAMESPACE), + generationContext))) { + generationContext.getRuntimeHints().reflection().registerType(type, + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); + } + } + + if (forQuerydsl) { + doIfPresent( + resolved -> QTypeContributor.contributeEntityPath(resolved, generationContext, resolved.getClassLoader())); + } + + if (!proxies.isEmpty()) { + for (List proxyInterfaces : proxies) { + generationContext.getRuntimeHints().proxies() + .registerJdkProxy(Stream.concat(Stream.of(type), proxyInterfaces.stream()).toArray(TypeReference[]::new)); + } + } + + } + + private boolean doIfPresent(Consumer> consumer) { + if (!ClassUtils.isPresent(type.getName(), type.getClass().getClassLoader())) { + return false; + } + try { + Class resolved = ClassUtils.forName(type.getName(), type.getClass().getClassLoader()); + consumer.accept(resolved); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + } } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index 0b865a8767..78833f7a75 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -25,7 +25,6 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; @@ -36,9 +35,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; import org.springframework.data.util.TypeUtils; import org.springframework.util.ClassUtils; @@ -57,7 +54,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(); + private AotContext aotContext; public void setModuleIdentifier(@Nullable String moduleIdentifier) { this.moduleIdentifier = moduleIdentifier; @@ -80,9 +77,8 @@ public void setEnvironment(Environment environment) { return null; } - BeanFactory beanFactory = registeredBean.getBeanFactory(); - return contribute(AotContext.from(beanFactory, this.environment.get()), resolveManagedTypes(registeredBean), - registeredBean); + this.aotContext = new DefaultAotContext(registeredBean.getBeanFactory(), environment.get()); + return contribute(this.aotContext, resolveManagedTypes(registeredBean), registeredBean); } private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { @@ -131,7 +127,7 @@ private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { */ protected BeanRegistrationAotContribution contribute(AotContext aotContext, ManagedTypes managedTypes, RegisteredBean registeredBean) { - return new ManagedTypesRegistrationAotContribution(managedTypes, registeredBean, this::contributeType); + return new ManagedTypesRegistrationAotContribution(aotContext, managedTypes, registeredBean, this::contributeType); } /** @@ -148,16 +144,12 @@ protected void contributeType(ResolvableType type, GenerationContext generationC Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE); - Class resolvedType = type.toClass(); - TypeContributor.contribute(resolvedType, annotationNamespaces, generationContext); - QTypeContributor.contributeEntityPath(resolvedType, generationContext, resolvedType.getClassLoader()); + aotContext.typeConfiguration(type).forDataBinding() // + .generateEntityInstantiator() // + .forQuerydsl() // + .contribute(generationContext); // - PersistentEntity entity = aotMappingContext.getPersistentEntity(resolvedType); - if (entity != null) { - aotMappingContext.contribute(entity); - } - - TypeUtils.resolveUsedAnnotations(resolvedType).forEach( + TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java index 1e59e7e852..ed7d32dc00 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java @@ -74,14 +74,16 @@ */ class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContribution { + private final AotContext aotContext; private final ManagedTypes managedTypes; private final Lazy>> sourceTypes; private final BiConsumer contributionAction; private final RegisteredBean source; - public ManagedTypesRegistrationAotContribution(ManagedTypes managedTypes, RegisteredBean registeredBean, + public ManagedTypesRegistrationAotContribution(AotContext aotContext, ManagedTypes managedTypes, RegisteredBean registeredBean, BiConsumer contributionAction) { + this.aotContext = aotContext; this.managedTypes = managedTypes; this.sourceTypes = Lazy.of(managedTypes::toList); this.contributionAction = contributionAction; 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 9a312ca380..feaedac470 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -243,6 +243,7 @@ public Collection getPersistentEntities() { @Override @Nullable public E getPersistentEntity(Class type) { + LOGGER.info("obtain persistent entity for type: " + type); return getPersistentEntity(TypeInformation.of(type)); } diff --git a/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java index 6640f925e3..e99edeacfe 100644 --- a/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java +++ b/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java @@ -36,9 +36,13 @@ class ReflectionFallbackPersistentPropertyAccessorFactory implements PersistentP public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { if (accessorFactory.isSupported(entity)) { - return accessorFactory.getPropertyAccessor(entity, bean); + PersistentPropertyAccessor propertyAccessor = accessorFactory.getPropertyAccessor(entity, bean); + System.out.println("Accessor Factory: " + propertyAccessor.getClass().getName()); + return propertyAccessor; + } + System.out.println("Fallback Accessor Factory :("); return BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(entity, bean); } diff --git a/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java index 0a89486886..c0eac7cf29 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java @@ -79,4 +79,6 @@ default Set getBasePackages() { */ Set> getResolvedTypes(); + Set> getUserDomainTypes(); + } diff --git a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java index ccf60a01e5..8541c5bb9b 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java @@ -19,17 +19,21 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.Environment; import org.springframework.data.aot.AotContext; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeCollector; +import org.springframework.data.util.TypeContributor; import org.springframework.data.util.TypeUtils; /** @@ -133,11 +137,34 @@ public Set> getResolvedTypes() { return managedTypes.get(); } + @Override + public Set> getUserDomainTypes() { + + return getResolvedTypes().stream() + .filter(it -> TypeContributor.isPartOf(it, Set.of(repositoryInformation.getDomainType().getPackageName()))) + .collect(Collectors.toSet()); + } + @Override public AotContext.TypeIntrospector introspectType(String typeName) { return aotContext.introspectType(typeName); } + @Override + public InstantiationCreator instantiationCreator(TypeReference typeReference) { + return aotContext.instantiationCreator(typeReference); + } + + @Override + public AotTypeConfiguration typeConfiguration(TypeReference typeReference) { + return aotContext.typeConfiguration(typeReference); + } + + @Override + public Collection typeConfigurations() { + return aotContext.typeConfigurations(); + } + @Override public AotContext.IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return aotContext.introspectBeanDefinition(beanName); 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 1a53247cce..d333cb350d 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Predicate; @@ -43,10 +44,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.aot.AotTypeConfiguration; import org.springframework.data.projection.EntityProjectionIntrospector; -import org.springframework.data.projection.TargetAware; import org.springframework.data.repository.Repository; import org.springframework.data.repository.aot.generate.AotRepositoryBeanDefinitionPropertiesDecorator; import org.springframework.data.repository.aot.generate.RepositoryContributor; @@ -57,7 +56,6 @@ import org.springframework.data.util.TypeContributor; import org.springframework.data.util.TypeUtils; import org.springframework.javapoet.CodeBlock; -import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -75,8 +73,6 @@ 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; @@ -259,62 +255,29 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener }; } - public Predicate> typeFilter() { // like only document ones. // TODO: As in MongoDB? - return Predicates.isTrue(); - } - private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, GenerationContext contribution) { RepositoryInformation repositoryInformation = getRepositoryInformation(); logTrace("Contributing repository information for [%s]", repositoryInformation.getRepositoryInterface()); - contribution.getRuntimeHints().reflection() - .registerType(repositoryInformation.getRepositoryInterface(), - hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)) - .registerType(repositoryInformation.getRepositoryBaseClass(), hint -> hint - .withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); - - TypeContributor.contribute(repositoryInformation.getDomainType(), contribution); - QTypeContributor.contributeEntityPath(repositoryInformation.getDomainType(), contribution, - repositoryContext.getClassLoader()); - - // 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); - } + repositoryContext.typeConfiguration(repositoryInformation.getRepositoryInterface()) + .forReflectiveAccess(MemberCategory.INVOKE_PUBLIC_METHODS) // + .repositoryProxy(); + + repositoryContext.typeConfiguration(repositoryInformation.getRepositoryBaseClass()) + .forReflectiveAccess(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); + + repositoryContext.typeConfiguration(repositoryInformation.getDomainType()).forDataBinding().forQuerydsl(); + + // TODO: purposeful api for uses cases to have some internal logic + repositoryContext.getUserDomainTypes().stream() // + .map(repositoryContext::typeConfiguration) // + .forEach(AotTypeConfiguration::generateEntityInstantiator); // // Repository Fragments contributeFragments(contribution); - // Repository Proxy - contribution.getRuntimeHints().proxies().registerJdkProxy(repositoryInformation.getRepositoryInterface(), - SpringProxy.class, Advised.class, DecoratingProxy.class); - - // Transactional Repository Proxy - // repositoryContext.ifTransactionManagerPresent(transactionManagerBeanNames -> { - - // TODO: Is the following double JDK Proxy registration above necessary or would a single JDK Proxy - // registration suffice? - // In other words, simply having a single JDK Proxy registration either with or without - // the additional Serializable TypeReference? - // NOTE: Using a single JDK Proxy registration causes the - // simpleRepositoryWithTxManagerNoKotlinNoReactiveButComponent() test case method to fail. - List transactionalRepositoryProxyTypeReferences = transactionalRepositoryProxyTypeReferences( - repositoryInformation); - - contribution.getRuntimeHints().proxies() - .registerJdkProxy(transactionalRepositoryProxyTypeReferences.toArray(new TypeReference[0])); - - if (isComponentAnnotatedRepository(repositoryInformation)) { - transactionalRepositoryProxyTypeReferences.add(TypeReference.of(Serializable.class)); - contribution.getRuntimeHints().proxies() - .registerJdkProxy(transactionalRepositoryProxyTypeReferences.toArray(new TypeReference[0])); - } - // }); - // Kotlin if (isKotlinCoroutineRepository(repositoryContext, repositoryInformation)) { contribution.getRuntimeHints().reflection().registerTypes(kotlinRepositoryReflectionTypeReferences(), hint -> {}); @@ -325,7 +288,7 @@ private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, Ge .filter(Class::isInterface).forEach(type -> { if (EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy().test(type, repositoryInformation.getDomainType())) { - contributeProjection(type, contribution); + repositoryContext.typeConfiguration(type).usedAsProjectionInterface(); } }); } @@ -358,10 +321,6 @@ private void contributeFragments(GenerationContext contribution) { } } - private boolean isComponentAnnotatedRepository(RepositoryInformation repositoryInformation) { - return AnnotationUtils.findAnnotation(repositoryInformation.getRepositoryInterface(), Component.class) != null; - } - private boolean isKotlinCoroutineRepository(AotRepositoryContext repositoryContext, RepositoryInformation repositoryInformation) { @@ -382,21 +341,6 @@ private List kotlinRepositoryReflectionTypeReferences() { TypeReference.of("kotlin.Boolean"))); } - private List transactionalRepositoryProxyTypeReferences(RepositoryInformation repositoryInformation) { - - return new ArrayList<>(Arrays.asList(TypeReference.of(repositoryInformation.getRepositoryInterface()), - TypeReference.of(Repository.class), // - TypeReference.of("org.springframework.transaction.interceptor.TransactionalProxy"), // - TypeReference.of("org.springframework.aop.framework.Advised"), // - TypeReference.of(DecoratingProxy.class))); - } - - private void contributeProjection(Class type, GenerationContext generationContext) { - - generationContext.getRuntimeHints().proxies().registerJdkProxy(type, TargetAware.class, SpringProxy.class, - DecoratingProxy.class); - } - static boolean isJavaOrPrimitiveType(Class type) { return TypeUtils.type(type).isPartOf("java") // || ClassUtils.isPrimitiveOrWrapper(type) // 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 535940d0d8..5a66753af9 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -28,6 +28,7 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.BeansException; @@ -44,8 +45,6 @@ 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; @@ -79,8 +78,6 @@ 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(); @@ -129,10 +126,7 @@ private void registerReflectiveForAggregateRoot(AotRepositoryContext repositoryC // arent we already registering the types in RepositoryRegistrationAotContribution#contributeRepositoryInfo? registrar.registerRuntimeHints(hints, it); - PersistentEntity persistentEntity = aotMappingContext.getPersistentEntity(it); - if (persistentEntity != null) { - aotMappingContext.contribute(persistentEntity); - } + repositoryContext.instantiationCreator(TypeReference.of(it)).create(); }); } diff --git a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java index 4c257440f1..635b3a814e 100644 --- a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java +++ b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java @@ -15,10 +15,14 @@ */ package org.springframework.data.aot; +import java.util.Collection; +import java.util.List; + import org.assertj.core.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mockito; +import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.env.Environment; import org.springframework.mock.env.MockEnvironment; @@ -83,6 +87,21 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return Mockito.mock(IntrospectedBeanDefinition.class); } + @Override + public InstantiationCreator instantiationCreator(TypeReference typeReference) { + return Mockito.mock(InstantiationCreator.class); + } + + @Override + public AotTypeConfiguration typeConfiguration(TypeReference typeReference) { + return null; + } + + @Override + public Collection typeConfigurations() { + return List.of(); + } + @Override public Environment getEnvironment() { return environment; diff --git a/src/test/java/org/springframework/data/aot/AotMappingContextUnitTests.java b/src/test/java/org/springframework/data/aot/AotMappingContextUnitTests.java new file mode 100644 index 0000000000..4a165555d7 --- /dev/null +++ b/src/test/java/org/springframework/data/aot/AotMappingContextUnitTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot; + +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Reference; +import org.springframework.data.util.TypeInformation; + +/** + * @author Christoph Strobl + */ +public class AotMappingContextUnitTests { + + @Test // GH-2595 + void obtainEntityWithReference() { + new AotMappingContext().getPersistentEntity(TypeInformation.of(DemoEntity.class)); + } + + static class DemoEntity { + + @Id String id; + String name; + + @Reference ReferencedEntity referencedEntity; + } + + static class ReferencedEntity { + @Id String id; + } +} diff --git a/src/test/java/org/springframework/data/repository/aot/AotUtil.java b/src/test/java/org/springframework/data/repository/aot/AotUtil.java index 0ce1763a40..042ada7c64 100644 --- a/src/test/java/org/springframework/data/repository/aot/AotUtil.java +++ b/src/test/java/org/springframework/data/repository/aot/AotUtil.java @@ -15,7 +15,7 @@ */ package org.springframework.data.repository.aot; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; @@ -42,35 +42,42 @@ static RepositoryRegistrationAotContributionBuilder contributionFor(Class con applicationContext.register(configuration); applicationContext.refreshForAotProcessing(new RuntimeHints()); - return repositoryType -> { + return repositoryTypes -> { - String[] repositoryBeanNames = applicationContext.getBeanNamesForType(repositoryType); + BeanRegistrationAotContribution beanContribution = null; - assertThat(repositoryBeanNames) - .describedAs("Unable to find repository [%s] in configuration [%s]", repositoryType, configuration) - .hasSize(1); + for (Class repositoryType : repositoryTypes) { - String repositoryBeanName = repositoryBeanNames[0]; + String[] repositoryBeanNames = applicationContext.getBeanNamesForType(repositoryType); - ConfigurableListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); + assertThat(repositoryBeanNames) + .describedAs("Unable to find repository [%s] in configuration [%s]", repositoryType, configuration) + .hasSize(1); - RepositoryRegistrationAotProcessor repositoryAotProcessor = applicationContext - .getBean(RepositoryRegistrationAotProcessor.class); + String repositoryBeanName = repositoryBeanNames[0]; - repositoryAotProcessor.setBeanFactory(beanFactory); + ConfigurableListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); - RegisteredBean bean = RegisteredBean.of(beanFactory, repositoryBeanName); + RepositoryRegistrationAotProcessor repositoryAotProcessor = applicationContext + .getBean(RepositoryRegistrationAotProcessor.class); - BeanRegistrationAotContribution beanContribution = repositoryAotProcessor.processAheadOfTime(bean); + repositoryAotProcessor.setBeanFactory(beanFactory); - assertThat(beanContribution).isInstanceOf(RepositoryRegistrationAotContribution.class); + RegisteredBean bean = RegisteredBean.of(beanFactory, repositoryBeanName); + beanContribution = repositoryAotProcessor.processAheadOfTime(bean); + } + assertThat(beanContribution).isInstanceOf(RepositoryRegistrationAotContribution.class); return (RepositoryRegistrationAotContribution) beanContribution; }; } @FunctionalInterface interface RepositoryRegistrationAotContributionBuilder { - RepositoryRegistrationAotContribution forRepository(Class repositoryInterface); + default RepositoryRegistrationAotContribution forRepository(Class repositoryInterface) { + return forRepositories(repositoryInterface); + } + + RepositoryRegistrationAotContribution forRepositories(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 index 39674dd06a..eeecdc5eb4 100644 --- a/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java @@ -17,22 +17,38 @@ import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.*; +import java.util.Map; + import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; 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.aot.types.BaseEntity; +import org.springframework.data.aot.types.CyclicPropertiesA; +import org.springframework.data.aot.types.CyclicPropertiesB; +import org.springframework.data.aot.types.EmptyType1; +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests.ConfigWithMultipleRepositories.Repo1; +import org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests.ConfigWithMultipleRepositories.Repo2; import org.springframework.data.repository.config.EnableRepositories; import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; +import org.springframework.data.util.TypeInformation; +import org.springframework.test.util.ReflectionTestUtils; /** * Integration Tests for {@link RepositoryRegistrationAotProcessor} to verify capturing generated instantiations and * property accessors. * * @author Mark Paluch + * @author Christoph Strobl */ public class GeneratedClassesCaptureIntegrationTests { @@ -44,39 +60,91 @@ void registersGeneratedPropertyAccessorsEntityInstantiators() { 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")); - */ + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.BaseEntity__Accessor_m5hoaa")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.BaseEntity__Instantiator_m5hoaa")); + + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.Address__Accessor_rf1iey")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.Address__Instantiator_rf1iey")); + }); + } + + @Test // GH-2595 + @Disabled("caching issue in ClassGeneratingEntityInstantiator") + void registersGeneratedPropertyAccessorsEntityInstantiatorsForCyclicProperties() { + + RepositoryRegistrationAotContribution repositoryBeanContribution = AotUtil + .contributionFor(ConfigWithCyclicReferences.class).forRepository(ConfigWithCyclicReferences.MyRepo.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesB__Accessor_o13htw")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesB__Instantiator_o13htw")); + + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesA__Accessor_o13htx")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesA__Instantiator_o13htx")); + }); + } + + @Test // GH-2595 + void registersGeneratedPropertyAccessorsEntityInstantiatorsForMultipleRepositoriesReferencingEachOther() { + + RepositoryRegistrationAotContribution repositoryBeanContribution = AotUtil + .contributionFor(ConfigWithMultipleRepositories.class) + .forRepositories(ConfigWithMultipleRepositories.Repo1.class, ConfigWithMultipleRepositories.Repo2.class); + + assertThatContribution(repositoryBeanContribution) // + .codeContributionSatisfies(contribution -> { + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesB__Accessor_o13htw")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesB__Instantiator_o13htw")); + + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesA__Accessor_o13htx")); + contribution.contributesReflectionFor( + TypeReference.of("org.springframework.data.aot.types.CyclicPropertiesA__Instantiator_o13htx")); }); } @EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = Config.MyRepo.class) }, basePackageClasses = Config.class, considerNestedRepositories = true) - public class Config { + public static class Config { - public interface MyRepo extends CrudRepository { + public interface MyRepo extends CrudRepository { } + } - public static class Person { + @EnableRepositories( + includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = ConfigWithCyclicReferences.MyRepo.class) }, + basePackageClasses = ConfigWithCyclicReferences.class, considerNestedRepositories = true) + public static class ConfigWithCyclicReferences { - @Nullable Address address; + public interface MyRepo extends CrudRepository { } + } + + @EnableRepositories( + includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = { Repo1.class, Repo2.class }) }, + basePackageClasses = ConfigWithCyclicReferences.class, considerNestedRepositories = true) + public static class ConfigWithMultipleRepositories { + + public interface Repo1 extends CrudRepository { - public static class Address { - String street; } + public interface Repo2 extends CrudRepository { + + } } } 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 8e73433867..b6afe7a3f0 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java @@ -116,7 +116,7 @@ public RepositoryRegistrationAotContributionAssert codeContributionSatisfies( BeanRegistrationCode mockBeanRegistrationCode = mock(BeanRegistrationCode.class); - GenerationContext generationContext = new TestGenerationContext(Object.class); + TestGenerationContext generationContext = new TestGenerationContext(Object.class); ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); @@ -138,7 +138,6 @@ public Object get() { 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 d71d0325fb..1226c1640c 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -78,9 +78,7 @@ void simpleRepositoryNoTxManagerNoKotlinNoReactiveNoComponent() { .contributesJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, SpringProxy.class, Advised.class, DecoratingProxy.class) // .contributesJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, Repository.class, - TransactionalProxy.class, Advised.class, DecoratingProxy.class) - .doesNotContributeJdkProxy(ConfigWithSimpleCrudRepository.MyRepo.class, Repository.class, - TransactionalProxy.class, Advised.class, DecoratingProxy.class, Serializable.class); + TransactionalProxy.class, Advised.class, DecoratingProxy.class); }); } @@ -103,9 +101,7 @@ void simpleRepositoryWithTxManagerNoKotlinNoReactiveNoComponent() { .contributesJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, SpringProxy.class, Advised.class, DecoratingProxy.class) .contributesJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, Repository.class, - TransactionalProxy.class, Advised.class, DecoratingProxy.class) - .doesNotContributeJdkProxy(ConfigWithTransactionManagerPresent.MyTxRepo.class, Repository.class, - TransactionalProxy.class, Advised.class, DecoratingProxy.class, Serializable.class); + TransactionalProxy.class, Advised.class, DecoratingProxy.class); }); } diff --git a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java index 3d3b3ffc64..d18f5e5d21 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java @@ -17,16 +17,19 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import java.util.Collection; import java.util.List; import java.util.Set; import org.jspecify.annotations.Nullable; +import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.test.tools.ClassFile; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; @@ -75,6 +78,21 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return null; } + @Override + public InstantiationCreator instantiationCreator(TypeReference typeReference) { + return null; + } + + @Override + public AotTypeConfiguration typeConfiguration(TypeReference typeReference) { + return null; + } + + @Override + public Collection typeConfigurations() { + return List.of(); + } + @Override public String getBeanName() { return "dummyRepository"; @@ -105,6 +123,11 @@ public Set> getResolvedTypes() { return Set.of(); } + @Override + public Set> getUserDomainTypes() { + return Set.of(); + } + public List getRequiredContextFiles() { return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); } From 274986b009053ab3a0ccc57172920ab9131ee81a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 3 Sep 2025 11:01:43 +0200 Subject: [PATCH 4/6] Polishing. Move off TypeReference for known and loaded classes for easier handling. Introduce configuration for enabled and include/exclude filters. Refactor configuration to functional style. # Conflicts: # src/main/java/org/springframework/data/aot/AotContext.java # src/main/java/org/springframework/data/aot/AotMappingContext.java # src/main/java/org/springframework/data/aot/DefaultAotContext.java # src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java # src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java # src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java # src/test/java/org/springframework/data/aot/AotContextUnitTests.java --- .../springframework/data/aot/AotContext.java | 37 +-- .../data/aot/AotMappingContext.java | 52 +++-- .../data/aot/AotTypeConfiguration.java | 36 +-- .../data/aot/DefaultAotContext.java | 127 ++++++----- ...agedTypesBeanRegistrationAotProcessor.java | 14 +- ...backPersistentPropertyAccessorFactory.java | 6 +- .../ClassGeneratingEntityInstantiator.java | 12 +- ...lassGeneratingPropertyAccessorFactory.java | 7 +- ...zer.java => EntityInstantiatorSource.java} | 14 +- .../mapping/model/EntityInstantiators.java | 3 +- .../config/DefaultAotRepositoryContext.java | 12 +- ...RepositoryRegistrationAotContribution.java | 43 ++-- .../RepositoryRegistrationAotProcessor.java | 4 +- .../data/aot/AotContextUnitTests.java | 214 ++++++++++++------ 14 files changed, 322 insertions(+), 259 deletions(-) rename src/main/java/org/springframework/data/mapping/model/{PersistentEntityClassInitializer.java => EntityInstantiatorSource.java} (65%) diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java index 7febea0da5..6683df6f27 100644 --- a/src/main/java/org/springframework/data/aot/AotContext.java +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -22,7 +22,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Predicate; import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.TypeReference; @@ -216,18 +215,31 @@ default IntrospectedBeanDefinition introspectBeanDefinition(BeanReference refere */ IntrospectedBeanDefinition introspectBeanDefinition(String beanName); - InstantiationCreator instantiationCreator(TypeReference typeReference); - - default AotTypeConfiguration typeConfiguration(ResolvableType resolvableType) { - return typeConfiguration(resolvableType.toClass()); - } - - default AotTypeConfiguration typeConfiguration(Class type) { - return typeConfiguration(TypeReference.of(type)); + /** + * Obtain a {@link AotTypeConfiguration} for the given {@link ResolvableType} to customize the AOT processing for the + * given type. + * + * @param resolvableType the resolvable type to configure. + * @param configurationConsumer configuration consumer function. + */ + default void typeConfiguration(ResolvableType resolvableType, Consumer configurationConsumer) { + typeConfiguration(resolvableType.toClass(), configurationConsumer); } - AotTypeConfiguration typeConfiguration(TypeReference typeReference); + /** + * Obtain a {@link AotTypeConfiguration} for the given {@link ResolvableType} to customize the AOT processing for the + * given type. + * + * @param type the type to configure. + * @param configurationConsumer configuration consumer function. + */ + void typeConfiguration(Class type, Consumer configurationConsumer); + /** + * Return all type configurations registered with this {@link AotContext}. + * + * @return all type configurations registered with this {@link AotContext}. + */ Collection typeConfigurations(); /** @@ -344,9 +356,4 @@ interface IntrospectedBeanDefinition { Class resolveType(); } - interface InstantiationCreator { - boolean isAvailable(); - void create(); - } - } diff --git a/src/main/java/org/springframework/data/aot/AotMappingContext.java b/src/main/java/org/springframework/data/aot/AotMappingContext.java index 4b18eb77d4..73d8c136d3 100644 --- a/src/main/java/org/springframework/data/aot/AotMappingContext.java +++ b/src/main/java/org/springframework/data/aot/AotMappingContext.java @@ -24,8 +24,8 @@ 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.EntityInstantiatorSource; 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.repository.aot.generate.RepositoryContributor; @@ -37,48 +37,53 @@ * @author Mark Paluch * @since 4.0 */ -class AotMappingContext extends // TODO: hide this one and delegate to other component - can we use the - // AotContext for it? - AbstractMappingContext, AotMappingContext.BasicPersistentProperty> { +class AotMappingContext extends + AbstractMappingContext, AotMappingContext.AotPersistentProperty> { private static final Log logger = LogFactory.getLog(AotMappingContext.class); private final EntityInstantiators instantiators = new EntityInstantiators(); - private final ClassGeneratingPropertyAccessorFactory propertyAccessorFactory = new ClassGeneratingPropertyAccessorFactory(); + private final AotAccessorFactory propertyAccessorFactory = new AotAccessorFactory(); /** * 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 + * @param entityType */ - public void contribute(PersistentEntity entity) { - EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); - if (instantiator instanceof PersistentEntityClassInitializer pec) { - pec.initialize(entity); + public void contribute(Class entityType) { + + BasicPersistentEntity entity = getPersistentEntity(entityType); + + if (entity != null) { + + EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); + if (instantiator instanceof EntityInstantiatorSource source) { + source.getInstantiatorFor(entity); + } + + propertyAccessorFactory.initialize(entity); } - propertyAccessorFactory.initialize(entity); } - // TODO: can we extract some util for this using only type @Override - protected BasicPersistentEntity createPersistentEntity( + protected BasicPersistentEntity createPersistentEntity( TypeInformation typeInformation) { logger.debug("I hate gradle: create persistent entity for type: " + typeInformation); return new BasicPersistentEntity<>(typeInformation); } @Override - protected BasicPersistentProperty createPersistentProperty(Property property, - BasicPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + protected AotPersistentProperty createPersistentProperty(Property property, + BasicPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { logger.info("creating property: " + property.getName()); - return new BasicPersistentProperty(property, owner, simpleTypeHolder); + return new AotPersistentProperty(property, owner, simpleTypeHolder); } - static class BasicPersistentProperty extends AnnotationBasedPersistentProperty { + static class AotPersistentProperty extends AnnotationBasedPersistentProperty { - public BasicPersistentProperty(Property property, PersistentEntity owner, + public AotPersistentProperty(Property property, PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { super(property, owner, simpleTypeHolder); } @@ -89,15 +94,22 @@ public boolean isAssociation() { } @Override - protected Association createAssociation() { + protected Association createAssociation() { return new Association<>(this, null); } @Override - public Association getRequiredAssociation() { + public Association getAssociation() { return new Association<>(this, null); } } + static class AotAccessorFactory extends ClassGeneratingPropertyAccessorFactory { + + public void initialize(PersistentEntity entity) { + potentiallyCreateAndRegisterPersistentPropertyAccessorClass(entity); + } + } + } diff --git a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java index 1dbc60a69f..5510df9246 100644 --- a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java +++ b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java @@ -39,10 +39,10 @@ public interface AotTypeConfiguration { AotTypeConfiguration forReflectiveAccess(MemberCategory... categories); - AotTypeConfiguration generateEntityInstantiator(); + AotTypeConfiguration contributeAccessors(); // TODO: ? should this be a global condition for the entire configuration or do we need it for certain aspects ? - AotTypeConfiguration conditional(Predicate filter); + AotTypeConfiguration filter(Predicate> filter); default AotTypeConfiguration usedAsProjectionInterface() { return proxyInterface(TargetAware.class, SpringProxy.class, DecoratingProxy.class); @@ -69,42 +69,12 @@ default AotTypeConfiguration repositoryProxy() { AotTypeConfiguration proxyInterface(List proxyInterfaces); - default AotTypeConfiguration proxyInterface(TypeReference... proxyInterfaces) { - return proxyInterface(List.of(proxyInterfaces)); - } - default AotTypeConfiguration proxyInterface(Class... proxyInterfaces) { return proxyInterface(Stream.of(proxyInterfaces).map(TypeReference::of).toList()); } AotTypeConfiguration forQuerydsl(); - void contribute(GenerationContext generationContext); - - static Predicate userConfiguredCondition(Environment environment) { - - return new Predicate() { - - private final List allowedAccessorTypes = environment.getProperty("spring.data.aot.generate.accessor", - List.class, List.of()); - - @Override - @SuppressWarnings("unchecked") - public boolean test(TypeReference typeReference) { - - if (!allowedAccessorTypes.isEmpty()) { - if (allowedAccessorTypes.contains("none") || allowedAccessorTypes.contains("false") - || allowedAccessorTypes.contains("off")) { - return false; - } - if (!allowedAccessorTypes.contains(typeReference.getName())) { - return false; - } - } - - return true; - } - }; - } + void contribute(Environment environment, GenerationContext generationContext); } diff --git a/src/main/java/org/springframework/data/aot/DefaultAotContext.java b/src/main/java/org/springframework/data/aot/DefaultAotContext.java index a60f87af4a..7d6bcc2f3b 100644 --- a/src/main/java/org/springframework/data/aot/DefaultAotContext.java +++ b/src/main/java/org/springframework/data/aot/DefaultAotContext.java @@ -40,13 +40,14 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.data.aot.AotMappingContext.BasicPersistentProperty; -import org.springframework.data.mapping.model.BasicPersistentEntity; -import org.springframework.data.util.Lazy; import org.springframework.core.env.Environment; +import org.springframework.core.env.Environment; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.QTypeContributor; import org.springframework.data.util.TypeContributor; +import org.springframework.util.AntPathMatcher; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * Default {@link AotContext} implementation. @@ -56,17 +57,16 @@ */ class DefaultAotContext implements AotContext { - private final AotMappingContext mappingContext = new AotMappingContext(); + private final AotMappingContext mappingContext = new AotMappingContext();; private final ConfigurableListableBeanFactory factory; // TODO: should we reuse the config or potentially have multiple ones with different settings - somehow targets the // filtering issue - private final Map typeConfigurations = new HashMap<>(); - - private final Environment environment; + private final Map, AotTypeConfiguration> typeConfigurations = new HashMap<>(); + private final Environment environment; public DefaultAotContext(BeanFactory beanFactory, Environment environment) { - factory = beanFactory instanceof ConfigurableListableBeanFactory cbf ? cbf + this.factory = beanFactory instanceof ConfigurableListableBeanFactory cbf ? cbf : new DefaultListableBeanFactory(beanFactory); this.environment = environment; } @@ -92,13 +92,8 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { } @Override - public InstantiationCreator instantiationCreator(TypeReference typeReference) { - return new DefaultInstantiationCreator(introspectType(typeReference.getName())); - } - - @Override - public AotTypeConfiguration typeConfiguration(TypeReference typeReference) { - return typeConfigurations.computeIfAbsent(typeReference, it -> new ContextualTypeConfiguration(typeReference)); + public void typeConfiguration(Class type, Consumer configurationConsumer) { + configurationConsumer.accept(typeConfigurations.computeIfAbsent(type, it -> new ContextualTypeConfiguration(type))); } @Override @@ -145,29 +140,6 @@ public List getBeanNames() { } } - class DefaultInstantiationCreator implements InstantiationCreator { - - Lazy> entity; - - public DefaultInstantiationCreator(TypeIntrospector typeIntrospector) { - this.entity = Lazy.of(() -> mappingContext.getPersistentEntity(typeIntrospector.resolveRequiredType())); - } - - @Override - public boolean isAvailable() { - return entity.getNullable() != null; - } - - @Override - public void create() { - - BasicPersistentEntity persistentEntity = entity.getNullable(); - if (persistentEntity != null) { - mappingContext.contribute(persistentEntity); - } - } - } - class DefaultIntrospectedBeanDefinition implements IntrospectedBeanDefinition { private final String beanName; @@ -210,15 +182,15 @@ public RootBeanDefinition getRootBeanDefinition() throws NoSuchBeanDefinitionExc class ContextualTypeConfiguration implements AotTypeConfiguration { - private final TypeReference type; + private final Class type; private boolean forDataBinding = false; private final Set categories = new HashSet<>(5); - private boolean generateEntityInstantiator = false; + private boolean contributeAccessors = false; private boolean forQuerydsl = false; private final List> proxies = new ArrayList<>(); - private Predicate filter; + private Predicate> filter; - ContextualTypeConfiguration(TypeReference type) { + ContextualTypeConfiguration(Class type) { this.type = type; } @@ -235,8 +207,8 @@ public AotTypeConfiguration forReflectiveAccess(MemberCategory... categories) { } @Override - public AotTypeConfiguration generateEntityInstantiator() { - this.generateEntityInstantiator = true; + public AotTypeConfiguration contributeAccessors() { + this.contributeAccessors = true; return this; } @@ -253,14 +225,14 @@ public AotTypeConfiguration forQuerydsl() { } @Override - public AotTypeConfiguration conditional(Predicate filter) { + public AotTypeConfiguration filter(Predicate> filter) { this.filter = filter; return this; } @Override - public void contribute(GenerationContext generationContext) { + public void contribute(Environment environment, GenerationContext generationContext) { if (filter != null && !filter.test(this.type)) { return; @@ -271,43 +243,70 @@ public void contribute(GenerationContext generationContext) { categories.toArray(MemberCategory[]::new)); } - if (generateEntityInstantiator) { - instantiationCreator(type).create(); + if (contributeAccessors) { + + boolean accessorsEnabled = environment.getProperty("spring.aot.data.accessors.enabled", Boolean.class, true); + String include = environment.getProperty("spring.aot.data.accessors.include", String.class, ""); + String exclude = environment.getProperty("spring.aot.data.accessors.exclude", String.class, ""); + + if (shouldContributeAccessors(type, accessorsEnabled, include, exclude)) { + mappingContext.contribute(type); + } } if (forDataBinding) { - if (!doIfPresent(resolved -> TypeContributor.contribute(resolved, Set.of(TypeContributor.DATA_NAMESPACE), - generationContext))) { - generationContext.getRuntimeHints().reflection().registerType(type, - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); - } + + TypeContributor.contribute(type, Set.of(TypeContributor.DATA_NAMESPACE), generationContext); + + generationContext.getRuntimeHints().reflection().registerType(type, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS); } if (forQuerydsl) { - doIfPresent( - resolved -> QTypeContributor.contributeEntityPath(resolved, generationContext, resolved.getClassLoader())); + QTypeContributor.contributeEntityPath(type, generationContext, factory.getBeanClassLoader()); } if (!proxies.isEmpty()) { for (List proxyInterfaces : proxies) { generationContext.getRuntimeHints().proxies() - .registerJdkProxy(Stream.concat(Stream.of(type), proxyInterfaces.stream()).toArray(TypeReference[]::new)); + .registerJdkProxy(Stream.concat(Stream.of(TypeReference.of(type)), proxyInterfaces.stream()) + .toArray(TypeReference[]::new)); } } } - private boolean doIfPresent(Consumer> consumer) { - if (!ClassUtils.isPresent(type.getName(), type.getClass().getClassLoader())) { + static boolean shouldContributeAccessors(Class type, boolean enabled, String include, String exclude) { + + if (!enabled) { return false; } - try { - Class resolved = ClassUtils.forName(type.getName(), type.getClass().getClassLoader()); - consumer.accept(resolved); - return true; - } catch (ClassNotFoundException e) { - return false; + + AntPathMatcher antPathMatcher = new AntPathMatcher("."); + + if (StringUtils.hasText(include)) { + + String[] includes = include.split(","); + + for (String includePattern : includes) { + if (antPathMatcher.match(includePattern.trim(), type.getName())) { + return true; + } + } } + + if (StringUtils.hasText(exclude)) { + + String[] excludes = exclude.split(","); + + for (String excludePattern : excludes) { + if (antPathMatcher.match(excludePattern.trim(), type.getName())) { + return false; + } + } + } + + return true; } } } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index 78833f7a75..518bf389bb 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -53,8 +53,8 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio private final Log logger = LogFactory.getLog(getClass()); private @Nullable String moduleIdentifier; - private Lazy environment = Lazy.of(StandardEnvironment::new); private AotContext aotContext; + private Lazy environment = Lazy.of(StandardEnvironment::new); public void setModuleIdentifier(@Nullable String moduleIdentifier) { this.moduleIdentifier = moduleIdentifier; @@ -65,6 +65,11 @@ public String getModuleIdentifier() { return this.moduleIdentifier; } + @Override + public void setEnvironment(Environment environment) { + this.environment = Lazy.of(environment); + } + @Override public void setEnvironment(Environment environment) { this.environment = Lazy.of(() -> environment); @@ -144,10 +149,9 @@ protected void contributeType(ResolvableType type, GenerationContext generationC Set annotationNamespaces = Collections.singleton(TypeContributor.DATA_NAMESPACE); - aotContext.typeConfiguration(type).forDataBinding() // - .generateEntityInstantiator() // - .forQuerydsl() // - .contribute(generationContext); // + aotContext.typeConfiguration(type, config -> config.forDataBinding() // + .contributeAccessors() // + .forQuerydsl().contribute(environment.get(), generationContext)); TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); diff --git a/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java index e99edeacfe..6640f925e3 100644 --- a/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java +++ b/src/main/java/org/springframework/data/mapping/context/ReflectionFallbackPersistentPropertyAccessorFactory.java @@ -36,13 +36,9 @@ class ReflectionFallbackPersistentPropertyAccessorFactory implements PersistentP public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { if (accessorFactory.isSupported(entity)) { - PersistentPropertyAccessor propertyAccessor = accessorFactory.getPropertyAccessor(entity, bean); - System.out.println("Accessor Factory: " + propertyAccessor.getClass().getName()); - return propertyAccessor; - + return accessorFactory.getPropertyAccessor(entity, bean); } - System.out.println("Fallback Accessor Factory :("); return BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(entity, bean); } 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 353336f816..589e225f74 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java @@ -60,7 +60,7 @@ * @author Mark Paluch * @since 1.11 */ -class ClassGeneratingEntityInstantiator implements EntityInstantiator, PersistentEntityClassInitializer { +class ClassGeneratingEntityInstantiator implements EntityInstantiator, EntityInstantiatorSource { private static final Log LOGGER = LogFactory.getLog(ClassGeneratingEntityInstantiator.class); @@ -87,11 +87,6 @@ public ClassGeneratingEntityInstantiator() { this.fallbackToReflectionOnError = fallbackToReflectionOnError; } - @Override - public void initialize(PersistentEntity entity) { - getEntityInstantiator(entity); - } - @Override public , P extends PersistentProperty

> T createInstance(E entity, ParameterValueProvider

provider) { @@ -100,6 +95,11 @@ public , P extends PersistentPrope return instantiator.createInstance(entity, provider); } + @Override + public EntityInstantiator getInstantiatorFor(PersistentEntity entity) { + return getEntityInstantiator(entity); + } + private , P extends PersistentProperty

> EntityInstantiator getEntityInstantiator( E entity) { 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 8c874d47f4..39f102a309 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java @@ -77,7 +77,7 @@ * @since 1.13 */ public class ClassGeneratingPropertyAccessorFactory - implements PersistentPropertyAccessorFactory, PersistentEntityClassInitializer { + implements PersistentPropertyAccessorFactory { // Pooling of parameter arrays to prevent excessive object allocation. private final ThreadLocal argumentCache = ThreadLocal.withInitial(() -> new Object[1]); @@ -89,11 +89,6 @@ public class ClassGeneratingPropertyAccessorFactory private final ConcurrentLruCache, Function<@Nullable Object, @Nullable Object>> wrapperCache = new ConcurrentLruCache<>( 256, KotlinValueBoxingAdapter::getWrapper); - @Override - public void initialize(PersistentEntity entity) { - getPropertyAccessorConstructor(entity); - } - @Override public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { diff --git a/src/main/java/org/springframework/data/mapping/model/PersistentEntityClassInitializer.java b/src/main/java/org/springframework/data/mapping/model/EntityInstantiatorSource.java similarity index 65% rename from src/main/java/org/springframework/data/mapping/model/PersistentEntityClassInitializer.java rename to src/main/java/org/springframework/data/mapping/model/EntityInstantiatorSource.java index cdf025bd73..b43afd2f8f 100644 --- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityClassInitializer.java +++ b/src/main/java/org/springframework/data/mapping/model/EntityInstantiatorSource.java @@ -18,9 +18,19 @@ import org.springframework.data.mapping.PersistentEntity; /** + * Interface declaring a source for {@link EntityInstantiator} objects. + * * @author Mark Paluch + * @since 4.0 */ -public interface PersistentEntityClassInitializer { +@FunctionalInterface +public interface EntityInstantiatorSource { + + /** + * Returns an {@link EntityInstantiator} for the given {@link PersistentEntity}. + * + * @return the {@link EntityInstantiator} for the given {@link PersistentEntity}. + */ + EntityInstantiator getInstantiatorFor(PersistentEntity entity); - void initialize(PersistentEntity entity); } diff --git a/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java b/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java index 4fd4f2cfad..71cfe042b6 100644 --- a/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java +++ b/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java @@ -31,7 +31,7 @@ * @author Mark Paluch * @since 2.3 */ -public class EntityInstantiators { +public class EntityInstantiators implements EntityInstantiatorSource { private final EntityInstantiator fallback; private final Map, EntityInstantiator> customInstantiators; @@ -84,6 +84,7 @@ public EntityInstantiators(EntityInstantiator defaultInstantiator, * @param entity must not be {@literal null}. * @return will never be {@literal null}. */ + @Override public EntityInstantiator getInstantiatorFor(PersistentEntity entity) { Assert.notNull(entity, "Entity must not be null"); diff --git a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java index 8541c5bb9b..4c1d0e5384 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultAotRepositoryContext.java @@ -19,11 +19,10 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; -import java.util.List; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; -import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.annotation.MergedAnnotation; @@ -151,13 +150,8 @@ public AotContext.TypeIntrospector introspectType(String typeName) { } @Override - public InstantiationCreator instantiationCreator(TypeReference typeReference) { - return aotContext.instantiationCreator(typeReference); - } - - @Override - public AotTypeConfiguration typeConfiguration(TypeReference typeReference) { - return aotContext.typeConfiguration(typeReference); + public void typeConfiguration(Class type, Consumer configurationConsumer) { + aotContext.typeConfiguration(type, configurationConsumer); } @Override 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 d333cb350d..9e5c12f1a3 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -39,6 +39,9 @@ import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.DecoratingProxy; import org.springframework.core.annotation.AnnotationUtils; @@ -51,6 +54,7 @@ import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Lazy; import org.springframework.data.util.Predicates; import org.springframework.data.util.QTypeContributor; import org.springframework.data.util.TypeContributor; @@ -67,17 +71,20 @@ * @author Mark Paluch * @since 3.0 */ -public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution { +public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution, EnvironmentAware { private static final Log logger = LogFactory.getLog(RepositoryRegistrationAotContribution.class); private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = "org.springframework.data.repository.kotlin.CoroutineCrudRepository"; + private final RepositoryRegistrationAotProcessor repositoryRegistrationAotProcessor; + private final RepositoryRegistrationAotProcessor aotProcessor; private final AotRepositoryContext repositoryContext; - private @Nullable RepositoryContributor repositoryContributor; + private @Nullable RepositoryContributor repositoryContributor; + private Lazy environment = Lazy.of(StandardEnvironment::new); private @Nullable BiFunction moduleContribution; @@ -158,10 +165,6 @@ protected RepositoryRegistrationAotContribution(RepositoryRegistrationAotProcess return new RepositoryRegistrationAotContribution(getRepositoryRegistrationAotProcessor(), repositoryContext); } - protected @Nullable BiFunction getModuleContribution() { - return this.moduleContribution; - } - protected AotRepositoryContext getRepositoryContext() { return this.repositoryContext; } @@ -213,6 +216,11 @@ public RepositoryRegistrationAotContribution withModuleContribution( return this; } + @Override + public void setEnvironment(Environment environment) { + this.environment = Lazy.of(environment); + } + @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { @@ -224,7 +232,7 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be this.repositoryContributor = moduleContribution.apply(getRepositoryContext(), generationContext); if (this.repositoryContributor != null) { - this.repositoryContributor.contribute(generationContext); + this.repositoryContributor.contribute(environment.get(), generationContext); } } } @@ -261,19 +269,18 @@ private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, Ge logTrace("Contributing repository information for [%s]", repositoryInformation.getRepositoryInterface()); - repositoryContext.typeConfiguration(repositoryInformation.getRepositoryInterface()) - .forReflectiveAccess(MemberCategory.INVOKE_PUBLIC_METHODS) // - .repositoryProxy(); + repositoryContext.typeConfiguration(repositoryInformation.getRepositoryInterface(), + config -> config.forReflectiveAccess(MemberCategory.INVOKE_PUBLIC_METHODS).repositoryProxy()); - repositoryContext.typeConfiguration(repositoryInformation.getRepositoryBaseClass()) - .forReflectiveAccess(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); + repositoryContext.typeConfiguration(repositoryInformation.getRepositoryBaseClass(), config -> config + .forReflectiveAccess(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); - repositoryContext.typeConfiguration(repositoryInformation.getDomainType()).forDataBinding().forQuerydsl(); + repositoryContext.typeConfiguration(repositoryInformation.getDomainType(), + config -> config.forDataBinding().forQuerydsl()); // TODO: purposeful api for uses cases to have some internal logic - repositoryContext.getUserDomainTypes().stream() // - .map(repositoryContext::typeConfiguration) // - .forEach(AotTypeConfiguration::generateEntityInstantiator); // + repositoryContext.getUserDomainTypes() // + .forEach(it -> repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors)); // Repository Fragments contributeFragments(contribution); @@ -288,7 +295,7 @@ private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, Ge .filter(Class::isInterface).forEach(type -> { if (EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy().test(type, repositoryInformation.getDomainType())) { - repositoryContext.typeConfiguration(type).usedAsProjectionInterface(); + repositoryContext.typeConfiguration(type, AotTypeConfiguration::usedAsProjectionInterface); } }); } @@ -297,7 +304,7 @@ private void contributeFragments(GenerationContext contribution) { for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { Class repositoryFragmentType = fragment.getSignatureContributor(); - Optional> implementation = fragment.getImplementationClass(); + Optional> implementation = fragment.getImplementationClass(); contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { 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 5a66753af9..108b44fd81 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -28,7 +28,6 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.BeansException; @@ -42,6 +41,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.EnvironmentAware; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; @@ -126,7 +126,7 @@ private void registerReflectiveForAggregateRoot(AotRepositoryContext repositoryC // arent we already registering the types in RepositoryRegistrationAotContribution#contributeRepositoryInfo? registrar.registerRuntimeHints(hints, it); - repositoryContext.instantiationCreator(TypeReference.of(it)).create(); + repositoryContext.typeConfiguration(it, AotTypeConfiguration::contributeAccessors); }); } diff --git a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java index 635b3a814e..2ece7e0204 100644 --- a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java +++ b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java @@ -15,96 +15,164 @@ */ package org.springframework.data.aot; +import static org.mockito.Mockito.*; + import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoSettings; + import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.env.Environment; +import org.springframework.data.aot.types.Address; +import org.springframework.data.aot.types.Customer; +import org.springframework.data.aot.types.EmptyType1; import org.springframework.mock.env.MockEnvironment; import org.springframework.util.StringUtils; /** - * Tests for {@link AotContext}. + * Unit tests for {@link AotContext}. + * * + * @author Mark Paluch * @author Christoph Strobl */ +@MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) class AotContextUnitTests { - @ParameterizedTest // GH-3322 - @CsvSource({ // - "'spring.aot.repositories.enabled', '', '', '', true", // - "'spring.aot.repositories.enabled', 'true', '', '', true", // - "'spring.aot.repositories.enabled', 'false', '', '', false", // - "'spring.aot.repositories.enabled', '', 'commons', 'true', true", // - "'spring.aot.repositories.enabled', 'true', 'commons', 'true', true", // - "'spring.aot.repositories.enabled', '', 'commons', 'false', false", // - "'spring.aot.repositories.enabled', 'false', 'commons', 'true', false" // - }) - void considersEnvironmentSettingsForGeneratedRepositories(String generalFlag, String generalValue, String storeName, - String storeValue, boolean enabled) { - - MockAotContext ctx = new MockAotContext(); - if (StringUtils.hasText(generalFlag) && StringUtils.hasText(generalValue)) { - ctx.withProperty(generalFlag, generalValue); - } - if (StringUtils.hasText(storeName) && StringUtils.hasText(storeValue)) { - ctx.withProperty("spring.aot.%s.repositories.enabled".formatted(storeName), storeValue); - } - - Assertions.assertThat(ctx.isGeneratedRepositoriesEnabled(storeName)).isEqualTo(enabled); - } - - static class MockAotContext implements AotContext { - - private final MockEnvironment environment; - - public MockAotContext() { - this.environment = new MockEnvironment(); - } - - MockAotContext withProperty(String key, String value) { - environment.setProperty(key, value); - return this; - } - - @Override - public ConfigurableListableBeanFactory getBeanFactory() { - return Mockito.mock(ConfigurableListableBeanFactory.class); - } - - @Override - public TypeIntrospector introspectType(String typeName) { - return Mockito.mock(TypeIntrospector.class); - } - - @Override - public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { - return Mockito.mock(IntrospectedBeanDefinition.class); - } - - @Override - public InstantiationCreator instantiationCreator(TypeReference typeReference) { - return Mockito.mock(InstantiationCreator.class); - } - - @Override - public AotTypeConfiguration typeConfiguration(TypeReference typeReference) { - return null; - } - - @Override - public Collection typeConfigurations() { - return List.of(); - } - - @Override - public Environment getEnvironment() { - return environment; - } - } + @Mock BeanFactory beanFactory; + + @Mock AotMappingContext mappingContext; + + MockEnvironment mockEnvironment = new MockEnvironment(); + + @Test // GH-2595 + void shouldContributeAccessorByDefault() { + + contributeAccessor(Address.class); + verify(mappingContext).contribute(Address.class); + } + + @Test // GH-2595 + void shouldConsiderDisabledAccessors() { + + mockEnvironment.setProperty("spring.aot.data.accessors.enabled", "false"); + + contributeAccessor(Address.class); + + verifyNoInteractions(mappingContext); + } + + @Test // GH-2595 + void shouldApplyExcludeFilters() { + + mockEnvironment.setProperty("spring.aot.data.accessors.exclude", + Customer.class.getName() + " , " + EmptyType1.class.getName()); + + contributeAccessor(Address.class, Customer.class, EmptyType1.class); + + verify(mappingContext).contribute(Address.class); + verifyNoMoreInteractions(mappingContext); + } + + @Test // GH-2595 + void shouldApplyIncludeExcludeFilters() { + + mockEnvironment.setProperty("spring.aot.data.accessors.include", Customer.class.getPackageName() + ".Add*"); + mockEnvironment.setProperty("spring.aot.data.accessors.exclude", Customer.class.getPackageName() + ".**"); + + contributeAccessor(Address.class, Customer.class, EmptyType1.class); + + verify(mappingContext).contribute(Address.class); + verifyNoMoreInteractions(mappingContext); + } + + private void contributeAccessor(Class... classes) { + + DefaultAotContext context = new DefaultAotContext(beanFactory, mockEnvironment); + + for (Class aClass : classes) { + context.typeConfiguration(aClass, AotTypeConfiguration::contributeAccessors); + } + + context.typeConfigurations().forEach(it -> it.contribute(mockEnvironment, new TestGenerationContext())); + } + + @ParameterizedTest // GH-3322 + @CsvSource({ // + "'spring.aot.repositories.enabled', '', '', '', true", // + "'spring.aot.repositories.enabled', 'true', '', '', true", // + "'spring.aot.repositories.enabled', 'false', '', '', false", // + "'spring.aot.repositories.enabled', '', 'commons', 'true', true", // + "'spring.aot.repositories.enabled', 'true', 'commons', 'true', true", // + "'spring.aot.repositories.enabled', '', 'commons', 'false', false", // + "'spring.aot.repositories.enabled', 'false', 'commons', 'true', false" // + }) + void considersEnvironmentSettingsForGeneratedRepositories(String generalFlag, String generalValue, String storeName, + String storeValue, boolean enabled) { + + MockAotContext ctx = new MockAotContext(); + if (StringUtils.hasText(generalFlag) && StringUtils.hasText(generalValue)) { + ctx.withProperty(generalFlag, generalValue); + } + if (StringUtils.hasText(storeName) && StringUtils.hasText(storeValue)) { + ctx.withProperty("spring.aot.%s.repositories.enabled".formatted(storeName), storeValue); + } + + Assertions.assertThat(ctx.isGeneratedRepositoriesEnabled(storeName)).isEqualTo(enabled); + } + + static class MockAotContext implements AotContext { + + private final MockEnvironment environment; + + public MockAotContext() { + this.environment = new MockEnvironment(); + } + + MockAotContext withProperty(String key, String value) { + environment.setProperty(key, value); + return this; + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return Mockito.mock(ConfigurableListableBeanFactory.class); + } + + @Override + public TypeIntrospector introspectType(String typeName) { + return Mockito.mock(TypeIntrospector.class); + } + + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return Mockito.mock(IntrospectedBeanDefinition.class); + } + + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { + + } + + @Override + public Collection typeConfigurations() { + return List.of(); + } + + @Override + public Environment getEnvironment() { + return environment; + } + } } From 4a9b9ab146a6c97b4d5d73bd031653838c139c21 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 4 Sep 2025 10:58:25 +0200 Subject: [PATCH 5/6] Add documentation and fix issues after rebase --- src/main/antora/modules/ROOT/nav.adoc | 1 + src/main/antora/modules/ROOT/pages/aot.adoc | 71 +++++++ .../repositories/custom-implementations.adoc | 1 - .../springframework/data/aot/AotContext.java | 1 - .../data/aot/AotMappingContext.java | 1 - .../data/aot/AotTypeConfiguration.java | 78 +++++++- .../data/aot/DefaultAotContext.java | 38 +--- ...agedTypesBeanRegistrationAotProcessor.java | 17 +- ...nagedTypesRegistrationAotContribution.java | 16 +- ...toryBeanDefinitionPropertiesDecorator.java | 4 +- .../RepositoryConstructorBuilder.java | 3 + ...RepositoryRegistrationAotContribution.java | 26 +-- .../RepositoryRegistrationAotProcessor.java | 10 +- .../data/aot/AotContextUnitTests.java | 189 +++++++++--------- .../AbstractMappingContextUnitTests.java | 23 ++- .../ProjectionIntegrationTests.java | 26 ++- ...neratedClassesCaptureIntegrationTests.java | 13 +- ...toryRegistrationAotContributionAssert.java | 6 +- ...istrationAotProcessorIntegrationTests.java | 23 +-- .../DummyModuleAotRepositoryContext.java | 10 +- 20 files changed, 341 insertions(+), 216 deletions(-) create mode 100644 src/main/antora/modules/ROOT/pages/aot.adoc diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 9a7ac3241f..92d163d147 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -21,6 +21,7 @@ * xref:custom-conversions.adoc[] * xref:entity-callbacks.adoc[] * xref:is-new-state-detection.adoc[] +* xref:aot.adoc[] * xref:kotlin.adoc[] ** xref:kotlin/requirements.adoc[] ** xref:kotlin/null-safety.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/aot.adoc b/src/main/antora/modules/ROOT/pages/aot.adoc new file mode 100644 index 0000000000..60d02d77cd --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/aot.adoc @@ -0,0 +1,71 @@ += Ahead of Time Optimizations + +This chapter covers Spring Data's Ahead of Time (AOT) optimizations that build upon {spring-framework-docs}/core/aot.html[Spring's Ahead of Time Optimizations]. + +[[aot.bestpractices]] +== Best Practices + +=== Annotate your Domain Types + +During application startup, Spring scans the classpath for domain classes for early processing of entities. +By annotating your domain types with Spring Data Store specific `@Table`, `@Document` or `@Entity` annotations you can aid initial entity scanning and ensure that those types are registered with `ManagedTypes` for Runtime Hints. +Classpath scanning is not possible in native image arrangements and so Spring has to use `ManagedTypes` for the initial entity set. + +[[aot.code-gen]] +== Ahead of Time Code Generation + +Ahead of time code generation is not limited to usage with GraalVM Native Image but also offers benefits when working with regular deployments and can help optimize startup performance on the jvm. + +If Ahead of Time compilation is enabled Spring Data can (depending on the actual Module in use) contribute several components during the AOT phase of your build. + +* Bytecode for generated Type/Property Accessors +* Sourcecode for the defined Repository Interfaces +* Repository Metadata in JSON format + +Each of the above is enabled by default. +However there users may fine tune the configuration with following options. + +[options = "autowidth",cols="1,1"] +|=== +|spring.aot.data.accessors.enabled +|boolean flag to control contribution of Bytecode for generated Type/Property Accessors + +|spring.aot.data.accessors.exclude +|comma separated list of FQCN for which to skip contribution of Bytecode for generated Type/Property Accessors + +|spring.aot.data.accessors.include +|comma separated list of FQCN for which to contribute Bytecode for generated Type/Property Accessors + +|spring.aot.repositories.enabled +|boolean flag to control contribution of Source Code for Repository Interfaces + +|spring.aot.[module-name].repositories.enabled +|boolean flag to control contribution of Source Code for Repository Interfaces for a certain module (eg. jdbc) +|=== + +[[aot.repositories]] +== Ahead of Time Repositories + +AOT Repositories are an extension to AOT processing by pre-generating eligible query method implementations. +Query methods are opaque to developers regarding their underlying queries being executed in a query method call. +AOT repositories contribute query method implementations based on derived, annotated, and named queries that are known at build-time. +This optimization moves query method processing from runtime to build-time, which can lead to a significant performance improvement as query methods do not need to be analyzed reflectively upon each application start. + +The resulting AOT repository fragment follows the naming scheme of `Impl_AotRepository` and is placed in the same package as the repository interface. + +[[aot.hints]] +== Native Image Runtime Hints + +Running an application as a native image requires additional information compared to a regular JVM runtime. +Spring Data contributes {spring-framework-docs}/core/aot.html#aot.hints[Runtime Hints] during AOT processing for native image usage. +These are in particular hints for: + +* Auditing +* `ManagedTypes` to capture the outcome of class-path scans +* Repositories +** Reflection hints for entities, return types, and Spring Data annotations +** Repository fragments +** Querydsl `Q` classes +** Kotlin Coroutine support +* Web support (Jackson Hints for `PagedModel`) + diff --git a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc index 3407e07545..b3d204e393 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -362,7 +362,6 @@ The `exposeMetadata` flag can be set directly on the repository factory bean via import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Configuration; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; -import org.springframework.lang.Nullable; @Configuration class MyConfiguration { diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java index 6683df6f27..537f801354 100644 --- a/src/main/java/org/springframework/data/aot/AotContext.java +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -24,7 +24,6 @@ import java.util.function.Consumer; import org.jspecify.annotations.Nullable; -import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; diff --git a/src/main/java/org/springframework/data/aot/AotMappingContext.java b/src/main/java/org/springframework/data/aot/AotMappingContext.java index 73d8c136d3..01d023ca9f 100644 --- a/src/main/java/org/springframework/data/aot/AotMappingContext.java +++ b/src/main/java/org/springframework/data/aot/AotMappingContext.java @@ -28,7 +28,6 @@ import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.util.TypeInformation; /** diff --git a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java index 5510df9246..e9953e9b43 100644 --- a/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java +++ b/src/main/java/org/springframework/data/aot/AotTypeConfiguration.java @@ -1,11 +1,11 @@ /* - * Copyright 2025. the original author or authors. + * Copyright 2025-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,7 +18,6 @@ import java.io.Serializable; import java.util.List; -import java.util.function.Predicate; import java.util.stream.Stream; import org.springframework.aop.SpringProxy; @@ -31,27 +30,70 @@ import org.springframework.data.projection.TargetAware; /** + * Configuration object that captures various AOT configuration aspects of types within the data context by offering + * predefined methods to register native configuration necessary for data binding, projection proxy definitions, AOT + * cglib bytecode generation and other common tasks. + *

+ * On {@link #contribute(Environment, GenerationContext)} the configuration is added to the {@link GenerationContext}. + * * @author Christoph Strobl + * @since 4.0 */ public interface AotTypeConfiguration { + /** + * Configure the referenced type for data binding. In case of {@link java.lang.annotation.Annotation} only data ones + * are considered. For more fine grained control use {@link #forReflectiveAccess(MemberCategory...)}. + * + * @return this. + */ AotTypeConfiguration forDataBinding(); + /** + * Configure the referenced type for reflective access by providing at least one {@link MemberCategory}. + * + * @param categories must not contain {@literal null}. + * @return this. + */ AotTypeConfiguration forReflectiveAccess(MemberCategory... categories); + /** + * Contribute generated cglib accessors for the referenced type. + *

+ * Can be disabled by user configuration ({@code spring.aot.data.accessors.enabled}). Honors in/exclusions set by user + * configuration {@code spring.aot.data.accessors.include} / {@code spring.aot.data.accessors.exclude} + * + * @return this. + */ AotTypeConfiguration contributeAccessors(); - // TODO: ? should this be a global condition for the entire configuration or do we need it for certain aspects ? - AotTypeConfiguration filter(Predicate> filter); - + /** + * Configure the referenced type as a projection interface returned by eg. a query method. + *

+ * Shortcut for {@link #proxyInterface(Class[]) proxyInterface(TargetAware, SpringProxy, DecoratingProxy)} + * + * @return this. + */ default AotTypeConfiguration usedAsProjectionInterface() { return proxyInterface(TargetAware.class, SpringProxy.class, DecoratingProxy.class); } + /** + * Configure the referenced type as a spring proxy interface. + *

+ * Shortcut for {@link #proxyInterface(Class[]) proxyInterface(SpringProxy, Advised, DecoratingProxy)} + * + * @return this. + */ default AotTypeConfiguration springProxy() { return proxyInterface(SpringProxy.class, Advised.class, DecoratingProxy.class); } + /** + * Configure the referenced type as a repository proxy. + * + * @return this. + */ default AotTypeConfiguration repositoryProxy() { springProxy(); @@ -67,14 +109,36 @@ default AotTypeConfiguration repositoryProxy() { return this; } + /** + * Register a proxy for the referenced type that also implements the given proxyInterfaces. + * + * @param proxyInterfaces additional interfaces the proxy implements. Order matters! + * @return this. + */ AotTypeConfiguration proxyInterface(List proxyInterfaces); + /** + * Register a proxy for the referenced type that also implements the given proxyInterfaces. + * + * @param proxyInterfaces additional interfaces the proxy implements. Order matters! + * @return this. + */ default AotTypeConfiguration proxyInterface(Class... proxyInterfaces) { return proxyInterface(Stream.of(proxyInterfaces).map(TypeReference::of).toList()); } + /** + * Configure the referenced type for usage with Querydsl by registering hints for potential {@code Q} types. + * + * @return this. + */ AotTypeConfiguration forQuerydsl(); + /** + * Write the configuration to the given {@link GenerationContext}. + * + * @param environment must not be {@literal null}. + * @param generationContext must not be {@literal null}. + */ void contribute(Environment environment, GenerationContext generationContext); - } diff --git a/src/main/java/org/springframework/data/aot/DefaultAotContext.java b/src/main/java/org/springframework/data/aot/DefaultAotContext.java index 7d6bcc2f3b..f2e6bb26dd 100644 --- a/src/main/java/org/springframework/data/aot/DefaultAotContext.java +++ b/src/main/java/org/springframework/data/aot/DefaultAotContext.java @@ -26,11 +26,9 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; @@ -41,8 +39,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.env.Environment; -import org.springframework.core.env.Environment; -import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.QTypeContributor; import org.springframework.data.util.TypeContributor; import org.springframework.util.AntPathMatcher; @@ -53,22 +49,27 @@ * Default {@link AotContext} implementation. * * @author Mark Paluch + * @author Christoph Strobl * @since 3.0 */ class DefaultAotContext implements AotContext { - private final AotMappingContext mappingContext = new AotMappingContext();; + private final AotMappingContext mappingContext; private final ConfigurableListableBeanFactory factory; - // TODO: should we reuse the config or potentially have multiple ones with different settings - somehow targets the - // filtering issue + // TODO: should we reuse the config or potentially have multiple ones with different settings for the same type private final Map, AotTypeConfiguration> typeConfigurations = new HashMap<>(); - private final Environment environment; + private final Environment environment; public DefaultAotContext(BeanFactory beanFactory, Environment environment) { + this(beanFactory, environment, new AotMappingContext()); + } + + DefaultAotContext(BeanFactory beanFactory, Environment environment, AotMappingContext mappingContext) { this.factory = beanFactory instanceof ConfigurableListableBeanFactory cbf ? cbf : new DefaultListableBeanFactory(beanFactory); this.environment = environment; + this.mappingContext = mappingContext; } @Override @@ -188,7 +189,6 @@ class ContextualTypeConfiguration implements AotTypeConfiguration { private boolean contributeAccessors = false; private boolean forQuerydsl = false; private final List> proxies = new ArrayList<>(); - private Predicate> filter; ContextualTypeConfiguration(Class type) { this.type = type; @@ -224,20 +224,9 @@ public AotTypeConfiguration forQuerydsl() { return this; } - @Override - public AotTypeConfiguration filter(Predicate> filter) { - - this.filter = filter; - return this; - } - @Override public void contribute(Environment environment, GenerationContext generationContext) { - if (filter != null && !filter.test(this.type)) { - return; - } - if (!this.categories.isEmpty()) { generationContext.getRuntimeHints().reflection().registerType(this.type, categories.toArray(MemberCategory[]::new)); @@ -255,11 +244,7 @@ public void contribute(Environment environment, GenerationContext generationCont } if (forDataBinding) { - TypeContributor.contribute(type, Set.of(TypeContributor.DATA_NAMESPACE), generationContext); - - generationContext.getRuntimeHints().reflection().registerType(type, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_DECLARED_METHODS); } if (forQuerydsl) { @@ -268,9 +253,8 @@ public void contribute(Environment environment, GenerationContext generationCont if (!proxies.isEmpty()) { for (List proxyInterfaces : proxies) { - generationContext.getRuntimeHints().proxies() - .registerJdkProxy(Stream.concat(Stream.of(TypeReference.of(type)), proxyInterfaces.stream()) - .toArray(TypeReference[]::new)); + generationContext.getRuntimeHints().proxies().registerJdkProxy( + Stream.concat(Stream.of(TypeReference.of(type)), proxyInterfaces.stream()).toArray(TypeReference[]::new)); } } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index 518bf389bb..e6935c5bb4 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -22,7 +22,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; @@ -53,7 +52,6 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio private final Log logger = LogFactory.getLog(getClass()); private @Nullable String moduleIdentifier; - private AotContext aotContext; private Lazy environment = Lazy.of(StandardEnvironment::new); public void setModuleIdentifier(@Nullable String moduleIdentifier) { @@ -70,11 +68,6 @@ public void setEnvironment(Environment environment) { this.environment = Lazy.of(environment); } - @Override - public void setEnvironment(Environment environment) { - this.environment = Lazy.of(() -> environment); - } - @Override public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { @@ -82,8 +75,8 @@ public void setEnvironment(Environment environment) { return null; } - this.aotContext = new DefaultAotContext(registeredBean.getBeanFactory(), environment.get()); - return contribute(this.aotContext, resolveManagedTypes(registeredBean), registeredBean); + DefaultAotContext aotContext = new DefaultAotContext(registeredBean.getBeanFactory(), environment.get()); + return contribute(aotContext, resolveManagedTypes(registeredBean), registeredBean); } private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { @@ -124,7 +117,7 @@ private ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { /** * Hook to provide a customized flavor of {@link BeanRegistrationAotContribution}. By overriding this method calls to - * {@link #contributeType(ResolvableType, GenerationContext)} might no longer be issued. + * {@link #contributeType(ResolvableType, GenerationContext, AotContext)} might no longer be issued. * * @param aotContext never {@literal null}. * @param managedTypes never {@literal null}. @@ -141,7 +134,7 @@ protected BeanRegistrationAotContribution contribute(AotContext aotContext, Mana * @param type never {@literal null}. * @param generationContext never {@literal null}. */ - protected void contributeType(ResolvableType type, GenerationContext generationContext) { + protected void contributeType(ResolvableType type, GenerationContext generationContext, AotContext aotContext) { if (logger.isDebugEnabled()) { logger.debug(String.format("Contributing type information for [%s]", type.getType())); @@ -153,7 +146,7 @@ protected void contributeType(ResolvableType type, GenerationContext generationC .contributeAccessors() // .forQuerydsl().contribute(environment.get(), generationContext)); - TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( + TypeUtils.resolveUsedAnnotations(type.toClass()).forEach( annotation -> TypeContributor.contribute(annotation.getType(), annotationNamespaces, generationContext)); } diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java index ed7d32dc00..e86f8c161f 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java @@ -23,7 +23,6 @@ import javax.lang.model.element.Modifier; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GenerationContext; @@ -77,11 +76,11 @@ class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContri private final AotContext aotContext; private final ManagedTypes managedTypes; private final Lazy>> sourceTypes; - private final BiConsumer contributionAction; + private final TypeRegistration contributionAction; private final RegisteredBean source; - public ManagedTypesRegistrationAotContribution(AotContext aotContext, ManagedTypes managedTypes, RegisteredBean registeredBean, - BiConsumer contributionAction) { + public ManagedTypesRegistrationAotContribution(AotContext aotContext, ManagedTypes managedTypes, + RegisteredBean registeredBean, TypeRegistration contributionAction) { this.aotContext = aotContext; this.managedTypes = managedTypes; @@ -96,7 +95,7 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be List> types = sourceTypes.get(); if (!types.isEmpty()) { - TypeCollector.inspect(types).forEach(type -> contributionAction.accept(type, generationContext)); + TypeCollector.inspect(types).forEach(type -> contributionAction.register(type, generationContext, aotContext)); } } @@ -118,6 +117,10 @@ public RegisteredBean getSource() { return source; } + interface TypeRegistration { + void register(ResolvableType type, GenerationContext generationContext, AotContext aotContext); + } + /** * Class used to generate the fragment of code needed to define a {@link ManagedTypes} bean from previously discovered * managed types. @@ -145,7 +148,8 @@ protected ManagedTypesInstanceCodeFragment(List> sourceTypes, Registere } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("Instance", this::generateInstanceFactory); diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java index e39c9ffd4a..367162f879 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBeanDefinitionPropertiesDecorator.java @@ -123,6 +123,8 @@ public CodeBlock decorate() { private CodeBlock buildCallbackBody() { + Assert.state(repositoryContributor.getContributedTypeName() != null, "ContributedTypeName must not be null"); + CodeBlock.Builder callback = CodeBlock.builder(); List arguments = new ArrayList<>(); @@ -146,7 +148,7 @@ private CodeBlock buildCallbackBody() { List args = new ArrayList<>(); args.add(RepositoryComposition.RepositoryFragments.class); - args.add(repositoryContributor.getContributedTypeName().getCanonicalName()); + args.add(repositoryContributor.getContributedTypeName().getName()); args.addAll(arguments); callback.addStatement("return $T.just(new $L(%s%s))".formatted("$L".repeat(arguments.isEmpty() ? 0 : 1), diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java index 3193aa5563..8f28e0b506 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java @@ -48,8 +48,11 @@ */ class RepositoryConstructorBuilder implements AotRepositoryConstructorBuilder { + @SuppressWarnings("NullAway") private final String beanFactory = AotRepositoryBeanDefinitionPropertiesDecorator.RESERVED_TYPES .get(ResolvableType.forClass(BeanFactory.class)); + + @SuppressWarnings("NullAway") private final String fragmentCreationContext = AotRepositoryBeanDefinitionPropertiesDecorator.RESERVED_TYPES .get(ResolvableType.forClass(RepositoryFactoryBeanSupport.FragmentCreationContext.class)); 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 9e5c12f1a3..0d4ebbd053 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotContribution.java @@ -15,12 +15,10 @@ */ package org.springframework.data.repository.config; -import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Predicate; @@ -29,8 +27,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; -import org.springframework.aop.SpringProxy; -import org.springframework.aop.framework.Advised; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; @@ -39,13 +35,10 @@ import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.core.DecoratingProxy; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.env.Environment; import org.springframework.data.aot.AotContext; import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.projection.EntityProjectionIntrospector; @@ -55,9 +48,6 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.util.Lazy; -import org.springframework.data.util.Predicates; -import org.springframework.data.util.QTypeContributor; -import org.springframework.data.util.TypeContributor; import org.springframework.data.util.TypeUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.util.Assert; @@ -77,13 +67,11 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = "org.springframework.data.repository.kotlin.CoroutineCrudRepository"; - private final RepositoryRegistrationAotProcessor repositoryRegistrationAotProcessor; - private final RepositoryRegistrationAotProcessor aotProcessor; private final AotRepositoryContext repositoryContext; - private @Nullable RepositoryContributor repositoryContributor; + private @Nullable RepositoryContributor repositoryContributor; private Lazy environment = Lazy.of(StandardEnvironment::new); private @Nullable BiFunction moduleContribution; @@ -165,6 +153,10 @@ protected RepositoryRegistrationAotContribution(RepositoryRegistrationAotProcess return new RepositoryRegistrationAotContribution(getRepositoryRegistrationAotProcessor(), repositoryContext); } + protected @Nullable BiFunction getModuleContribution() { + return this.moduleContribution; + } + protected AotRepositoryContext getRepositoryContext() { return this.repositoryContext; } @@ -232,9 +224,11 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be this.repositoryContributor = moduleContribution.apply(getRepositoryContext(), generationContext); if (this.repositoryContributor != null) { - this.repositoryContributor.contribute(environment.get(), generationContext); + this.repositoryContributor.contribute(generationContext); } } + getRepositoryContext().typeConfigurations() + .forEach(typeConfiguration -> typeConfiguration.contribute(environment.get(), generationContext)); } @Override @@ -304,7 +298,7 @@ private void contributeFragments(GenerationContext contribution) { for (RepositoryFragment fragment : getRepositoryInformation().getFragments()) { Class repositoryFragmentType = fragment.getSignatureContributor(); - Optional> implementation = fragment.getImplementationClass(); + Optional> implementation = fragment.getImplementationClass(); contribution.getRuntimeHints().reflection().registerType(repositoryFragmentType, hint -> { 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 108b44fd81..c94b648522 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -25,7 +25,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.annotation.Reflective; @@ -41,10 +40,10 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.EnvironmentAware; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -89,9 +88,9 @@ public class RepositoryRegistrationAotProcessor return isRepositoryBean(bean) ? newRepositoryRegistrationAotContribution(bean) : null; } - @Nullable - protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { repositoryContext.getResolvedTypes().stream() .filter(it -> !RepositoryRegistrationAotContribution.isJavaOrPrimitiveType(it)) @@ -145,7 +144,7 @@ private boolean isRepositoryBean(RegisteredBean bean) { return null; } - //TODO: add the hook for customizing bean initialization code here! + // TODO: add the hook for customizing bean initialization code here! return contribution.withModuleContribution((repositoryContext, generationContext) -> { registerReflectiveForAggregateRoot(repositoryContext, generationContext); @@ -196,7 +195,6 @@ 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/AotContextUnitTests.java b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java index 2ece7e0204..a943aded02 100644 --- a/src/test/java/org/springframework/data/aot/AotContextUnitTests.java +++ b/src/test/java/org/springframework/data/aot/AotContextUnitTests.java @@ -15,7 +15,9 @@ */ package org.springframework.data.aot; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import java.util.Collection; import java.util.List; @@ -28,8 +30,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoSettings; - -import org.springframework.aot.hint.TypeReference; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -43,136 +43,135 @@ /** * Unit tests for {@link AotContext}. * - * * @author Mark Paluch * @author Christoph Strobl */ @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) class AotContextUnitTests { - @Mock BeanFactory beanFactory; + @Mock BeanFactory beanFactory; - @Mock AotMappingContext mappingContext; + @Mock AotMappingContext mappingContext; - MockEnvironment mockEnvironment = new MockEnvironment(); + MockEnvironment mockEnvironment = new MockEnvironment(); - @Test // GH-2595 - void shouldContributeAccessorByDefault() { + @Test // GH-2595 + void shouldContributeAccessorByDefault() { - contributeAccessor(Address.class); - verify(mappingContext).contribute(Address.class); - } + contributeAccessor(Address.class); + verify(mappingContext).contribute(Address.class); + } - @Test // GH-2595 - void shouldConsiderDisabledAccessors() { + @Test // GH-2595 + void shouldConsiderDisabledAccessors() { - mockEnvironment.setProperty("spring.aot.data.accessors.enabled", "false"); + mockEnvironment.setProperty("spring.aot.data.accessors.enabled", "false"); - contributeAccessor(Address.class); + contributeAccessor(Address.class); - verifyNoInteractions(mappingContext); - } + verifyNoInteractions(mappingContext); + } - @Test // GH-2595 - void shouldApplyExcludeFilters() { + @Test // GH-2595 + void shouldApplyExcludeFilters() { - mockEnvironment.setProperty("spring.aot.data.accessors.exclude", - Customer.class.getName() + " , " + EmptyType1.class.getName()); + mockEnvironment.setProperty("spring.aot.data.accessors.exclude", + Customer.class.getName() + " , " + EmptyType1.class.getName()); - contributeAccessor(Address.class, Customer.class, EmptyType1.class); + contributeAccessor(Address.class, Customer.class, EmptyType1.class); - verify(mappingContext).contribute(Address.class); - verifyNoMoreInteractions(mappingContext); - } + verify(mappingContext).contribute(Address.class); + verifyNoMoreInteractions(mappingContext); + } - @Test // GH-2595 - void shouldApplyIncludeExcludeFilters() { + @Test // GH-2595 + void shouldApplyIncludeExcludeFilters() { - mockEnvironment.setProperty("spring.aot.data.accessors.include", Customer.class.getPackageName() + ".Add*"); - mockEnvironment.setProperty("spring.aot.data.accessors.exclude", Customer.class.getPackageName() + ".**"); + mockEnvironment.setProperty("spring.aot.data.accessors.include", Customer.class.getPackageName() + ".Add*"); + mockEnvironment.setProperty("spring.aot.data.accessors.exclude", Customer.class.getPackageName() + ".**"); - contributeAccessor(Address.class, Customer.class, EmptyType1.class); + contributeAccessor(Address.class, Customer.class, EmptyType1.class); - verify(mappingContext).contribute(Address.class); - verifyNoMoreInteractions(mappingContext); - } + verify(mappingContext).contribute(Address.class); + verifyNoMoreInteractions(mappingContext); + } - private void contributeAccessor(Class... classes) { + private void contributeAccessor(Class... classes) { - DefaultAotContext context = new DefaultAotContext(beanFactory, mockEnvironment); + DefaultAotContext context = new DefaultAotContext(beanFactory, mockEnvironment, mappingContext); - for (Class aClass : classes) { - context.typeConfiguration(aClass, AotTypeConfiguration::contributeAccessors); - } + for (Class aClass : classes) { + context.typeConfiguration(aClass, AotTypeConfiguration::contributeAccessors); + } - context.typeConfigurations().forEach(it -> it.contribute(mockEnvironment, new TestGenerationContext())); - } + context.typeConfigurations().forEach(it -> it.contribute(mockEnvironment, new TestGenerationContext())); + } - @ParameterizedTest // GH-3322 - @CsvSource({ // - "'spring.aot.repositories.enabled', '', '', '', true", // - "'spring.aot.repositories.enabled', 'true', '', '', true", // - "'spring.aot.repositories.enabled', 'false', '', '', false", // - "'spring.aot.repositories.enabled', '', 'commons', 'true', true", // - "'spring.aot.repositories.enabled', 'true', 'commons', 'true', true", // - "'spring.aot.repositories.enabled', '', 'commons', 'false', false", // - "'spring.aot.repositories.enabled', 'false', 'commons', 'true', false" // - }) - void considersEnvironmentSettingsForGeneratedRepositories(String generalFlag, String generalValue, String storeName, - String storeValue, boolean enabled) { + @ParameterizedTest // GH-3322 + @CsvSource({ // + "'spring.aot.repositories.enabled', '', '', '', true", // + "'spring.aot.repositories.enabled', 'true', '', '', true", // + "'spring.aot.repositories.enabled', 'false', '', '', false", // + "'spring.aot.repositories.enabled', '', 'commons', 'true', true", // + "'spring.aot.repositories.enabled', 'true', 'commons', 'true', true", // + "'spring.aot.repositories.enabled', '', 'commons', 'false', false", // + "'spring.aot.repositories.enabled', 'false', 'commons', 'true', false" // + }) + void considersEnvironmentSettingsForGeneratedRepositories(String generalFlag, String generalValue, String storeName, + String storeValue, boolean enabled) { - MockAotContext ctx = new MockAotContext(); - if (StringUtils.hasText(generalFlag) && StringUtils.hasText(generalValue)) { - ctx.withProperty(generalFlag, generalValue); - } - if (StringUtils.hasText(storeName) && StringUtils.hasText(storeValue)) { - ctx.withProperty("spring.aot.%s.repositories.enabled".formatted(storeName), storeValue); - } + MockAotContext ctx = new MockAotContext(); + if (StringUtils.hasText(generalFlag) && StringUtils.hasText(generalValue)) { + ctx.withProperty(generalFlag, generalValue); + } + if (StringUtils.hasText(storeName) && StringUtils.hasText(storeValue)) { + ctx.withProperty("spring.aot.%s.repositories.enabled".formatted(storeName), storeValue); + } - Assertions.assertThat(ctx.isGeneratedRepositoriesEnabled(storeName)).isEqualTo(enabled); - } + Assertions.assertThat(ctx.isGeneratedRepositoriesEnabled(storeName)).isEqualTo(enabled); + } - static class MockAotContext implements AotContext { + static class MockAotContext implements AotContext { - private final MockEnvironment environment; + private final MockEnvironment environment; - public MockAotContext() { - this.environment = new MockEnvironment(); - } + public MockAotContext() { + this.environment = new MockEnvironment(); + } - MockAotContext withProperty(String key, String value) { - environment.setProperty(key, value); - return this; - } + MockAotContext withProperty(String key, String value) { + environment.setProperty(key, value); + return this; + } - @Override - public ConfigurableListableBeanFactory getBeanFactory() { - return Mockito.mock(ConfigurableListableBeanFactory.class); - } + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return Mockito.mock(ConfigurableListableBeanFactory.class); + } - @Override - public TypeIntrospector introspectType(String typeName) { - return Mockito.mock(TypeIntrospector.class); - } + @Override + public TypeIntrospector introspectType(String typeName) { + return Mockito.mock(TypeIntrospector.class); + } - @Override - public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { - return Mockito.mock(IntrospectedBeanDefinition.class); - } + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return Mockito.mock(IntrospectedBeanDefinition.class); + } - @Override - public void typeConfiguration(Class type, Consumer configurationConsumer) { + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { - } + } - @Override - public Collection typeConfigurations() { - return List.of(); - } + @Override + public Collection typeConfigurations() { + return List.of(); + } - @Override - public Environment getEnvironment() { - return environment; - } - } + @Override + public Environment getEnvironment() { + return environment; + } + } } diff --git a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java index 708139c04a..896abf21b5 100755 --- a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java @@ -15,14 +15,29 @@ */ package org.springframework.data.mapping.context; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import groovy.lang.MetaClass; import java.time.LocalDateTime; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; import java.util.function.Supplier; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java b/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java index 5ffa62b5e8..e531c3404b 100755 --- a/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java @@ -15,20 +15,21 @@ */ package org.springframework.data.projection; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; +import org.springframework.lang.Nullable; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.Configuration.ConfigurationBuilder; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Option; -import org.springframework.lang.Nullable; /** * Integration tests for projections. * * @author Oliver Gierke + * @author Christoph Strobl */ class ProjectionIntegrationTests { @@ -44,7 +45,26 @@ void jacksonSerializationDoesNotExposeDecoratedClass() throws Exception { assertThat(json.read("$.decoratedClass", String.class)).isNull(); } + @Test // GH-3170 + void jacksonSerializationConsidersJspecifyNullableAnnotations() throws Exception { + + var factory = new ProxyProjectionFactory(); + var projection = factory.createProjection(SampleProjectionJSpecify.class); + + var context = JsonPath.using(new ConfigurationBuilder().options(Option.SUPPRESS_EXCEPTIONS).build()); + var json = context.parse(new ObjectMapper().writeValueAsString(projection)); + + assertThat(json.read("$.decoratedClass", String.class)).isNull(); + } + + @SuppressWarnings("deprecation") interface SampleProjection { - @Nullable String getName(); + @Nullable + String getName(); + } + + interface SampleProjectionJSpecify { + @org.jspecify.annotations.Nullable + String getName(); } } diff --git a/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java b/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java index eeecdc5eb4..7d7c0aa670 100644 --- a/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/GeneratedClassesCaptureIntegrationTests.java @@ -15,33 +15,22 @@ */ package org.springframework.data.repository.aot; -import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.*; +import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.assertThatContribution; -import java.util.Map; - -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; 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.aot.types.BaseEntity; import org.springframework.data.aot.types.CyclicPropertiesA; import org.springframework.data.aot.types.CyclicPropertiesB; -import org.springframework.data.aot.types.EmptyType1; -import org.springframework.data.mapping.model.BasicPersistentEntity; -import org.springframework.data.mapping.model.EntityInstantiator; -import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests.ConfigWithMultipleRepositories.Repo1; import org.springframework.data.repository.aot.GeneratedClassesCaptureIntegrationTests.ConfigWithMultipleRepositories.Repo2; import org.springframework.data.repository.config.EnableRepositories; import org.springframework.data.repository.config.RepositoryRegistrationAotContribution; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; -import org.springframework.data.util.TypeInformation; -import org.springframework.test.util.ReflectionTestUtils; /** * Integration Tests for {@link RepositoryRegistrationAotProcessor} to verify capturing generated instantiations and 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 b6afe7a3f0..43b5ad85f7 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotContributionAssert.java @@ -15,8 +15,9 @@ */ package org.springframework.data.repository.aot; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -27,7 +28,6 @@ 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; 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 1226c1640c..c01d50310f 100644 --- a/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java +++ b/src/test/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessorIntegrationTests.java @@ -15,12 +15,11 @@ */ package org.springframework.data.repository.aot; -import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.*; +import static org.springframework.data.repository.aot.RepositoryRegistrationAotContributionAssert.assertThatContribution; import java.io.Serializable; import org.junit.jupiter.api.Test; - import org.springframework.aop.SpringProxy; import org.springframework.aop.framework.Advised; import org.springframework.context.annotation.ComponentScan.Filter; @@ -109,8 +108,8 @@ void simpleRepositoryWithTxManagerNoKotlinNoReactiveNoComponent() { void simpleRepositoryWithTxManagerNoKotlinNoReactiveButComponent() { RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( - ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.class).forRepository( - ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.MyComponentTxRepo.class); + ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.class) + .forRepository(ConfigWithTransactionManagerPresentAndAtComponentAnnotatedRepository.MyComponentTxRepo.class); assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs( @@ -176,7 +175,7 @@ void contributesCustomImplementationCorrectly() { RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( ConfigWithCustomImplementation.class) - .forRepository(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class); + .forRepository(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class); assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs(ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class) // @@ -225,15 +224,15 @@ void contributesRepositoryBaseClassCorrectly() { RepositoryRegistrationAotContribution repositoryBeanContribution = computeAotConfiguration( ConfigWithCustomRepositoryBaseClass.class) - .forRepository(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class); + .forRepository(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class); assertThatContribution(repositoryBeanContribution) // .targetRepositoryTypeIs(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // .hasFragments() // .codeContributionSatisfies(contribution -> { // // interface - contribution - .contributesReflectionFor(SampleRepositoryFragmentsContributor.class) // repository structural fragment + contribution.contributesReflectionFor(SampleRepositoryFragmentsContributor.class) // repository structural + // fragment .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo.class) // repository .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class) // base repo class .contributesReflectionFor(ConfigWithCustomRepositoryBaseClass.Person.class); // repository domain type @@ -308,7 +307,7 @@ void registersReflectionForInheritedDomainPublicationAnnotations() { RepositoryRegistrationAotContribution contribution = computeAotConfiguration( InheritedEventPublicationConfiguration.class) - .forRepository(InheritedEventPublicationConfiguration.SampleRepository.class); + .forRepository(InheritedEventPublicationConfiguration.SampleRepository.class); assertThatContribution(contribution).codeContributionSatisfies(it -> { it.contributesReflectionFor(AbstractAggregateRoot.class); @@ -335,10 +334,8 @@ void cleanup() {} interface SampleRepository extends Repository {} } - @EnableRepositories( - includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, - value = InheritedEventPublicationConfiguration.SampleRepository.class) }, - considerNestedRepositories = true) + @EnableRepositories(includeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, + value = InheritedEventPublicationConfiguration.SampleRepository.class) }, considerNestedRepositories = true) public class InheritedEventPublicationConfiguration { static class Sample extends AbstractAggregateRoot {} diff --git a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java index d18f5e5d21..8cb172f1c4 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java @@ -20,10 +20,9 @@ import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import org.jspecify.annotations.Nullable; - -import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.Environment; @@ -79,13 +78,8 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { } @Override - public InstantiationCreator instantiationCreator(TypeReference typeReference) { - return null; - } + public void typeConfiguration(Class type, Consumer configurationConsumer) { - @Override - public AotTypeConfiguration typeConfiguration(TypeReference typeReference) { - return null; } @Override From 7e754ce7f196bca6aaccfc9baaa367de4250981d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 8 Sep 2025 10:34:16 +0200 Subject: [PATCH 6/6] Polishing. Encapsulate config, add details about ant-path-style matching. --- src/main/antora/modules/ROOT/pages/aot.adoc | 26 ++++---- .../data/aot/DefaultAotContext.java | 60 +++++++++++++++---- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/aot.adoc b/src/main/antora/modules/ROOT/pages/aot.adoc index 60d02d77cd..f60d10ece0 100644 --- a/src/main/antora/modules/ROOT/pages/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/aot.adoc @@ -23,24 +23,28 @@ If Ahead of Time compilation is enabled Spring Data can (depending on the actual * Repository Metadata in JSON format Each of the above is enabled by default. -However there users may fine tune the configuration with following options. +However, there users may fine tune the configuration with following options. [options = "autowidth",cols="1,1"] |=== -|spring.aot.data.accessors.enabled -|boolean flag to control contribution of Bytecode for generated Type/Property Accessors +|`spring.aot.data.accessors.enabled` +|Boolean flag to control contribution of Bytecode for generated Type/Property Accessors -|spring.aot.data.accessors.exclude -|comma separated list of FQCN for which to skip contribution of Bytecode for generated Type/Property Accessors +|`spring.aot.data.accessors.include` +|Comma separated list of FQCN for which to contribute Bytecode for generated Type/Property Accessors. +Ant-style include patterns matching package names (e.g. `com.acme.**`) or type names inclusion. +Inclusion pattern matches are evaluated before exclusions for broad exclusion and selective inclusion. -|spring.aot.data.accessors.include -|comma separated list of FQCN for which to contribute Bytecode for generated Type/Property Accessors +|`spring.aot.data.accessors.exclude` +|Comma separated list of FQCN for which to skip contribution of Bytecode for generated Type/Property Accessors. +Ant-style exclude patterns matching package names (e.g. `com.acme.**`) or type names exclusion. +Exclusion pattern matches are evaluated after inclusions for broad exclusion and selective inclusion. -|spring.aot.repositories.enabled -|boolean flag to control contribution of Source Code for Repository Interfaces +|`spring.aot.repositories.enabled` +|Boolean flag to control contribution of Source Code for Repository Interfaces -|spring.aot.[module-name].repositories.enabled -|boolean flag to control contribution of Source Code for Repository Interfaces for a certain module (eg. jdbc) +|`spring.aot.[module-name].repositories.enabled` +|Boolean flag to control contribution of Source Code for Repository Interfaces for a certain module (eg. `jdbc`, `jpa`, `mongodb`, `cassandra`) |=== [[aot.repositories]] diff --git a/src/main/java/org/springframework/data/aot/DefaultAotContext.java b/src/main/java/org/springframework/data/aot/DefaultAotContext.java index f2e6bb26dd..fcffdaa0ae 100644 --- a/src/main/java/org/springframework/data/aot/DefaultAotContext.java +++ b/src/main/java/org/springframework/data/aot/DefaultAotContext.java @@ -26,6 +26,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; @@ -39,6 +40,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.env.Environment; +import org.springframework.data.util.Lazy; import org.springframework.data.util.QTypeContributor; import org.springframework.data.util.TypeContributor; import org.springframework.util.AntPathMatcher; @@ -234,11 +236,8 @@ public void contribute(Environment environment, GenerationContext generationCont if (contributeAccessors) { - boolean accessorsEnabled = environment.getProperty("spring.aot.data.accessors.enabled", Boolean.class, true); - String include = environment.getProperty("spring.aot.data.accessors.include", String.class, ""); - String exclude = environment.getProperty("spring.aot.data.accessors.exclude", String.class, ""); - - if (shouldContributeAccessors(type, accessorsEnabled, include, exclude)) { + AccessorContributionConfiguration configuration = AccessorContributionConfiguration.of(environment); + if (configuration.shouldContributeAccessors(type)) { mappingContext.contribute(type); } } @@ -257,20 +256,55 @@ public void contribute(Environment environment, GenerationContext generationCont Stream.concat(Stream.of(TypeReference.of(type)), proxyInterfaces.stream()).toArray(TypeReference[]::new)); } } + } + } + + /** + * Configuration for accessor to determine whether accessors should be contributed for a given type. + */ + private record AccessorContributionConfiguration(boolean enabled, Lazy include, Lazy exclude) { + + /** + * {@code boolean }Environment property to enable/disable accessor contribution. Enabled by default. + */ + public static final String ACCESSORS_ENABLED = "spring.aot.data.accessors.enabled"; + + /** + * {@code String} Environment property to define Ant-style include patterns (comma-separated) matching package names + * (e.g. {@code com.acme.**}) or type names inclusion. Inclusion pattern matches are evaluated before exclusions for + * broad exclusion and selective inclusion. + */ + public static final String INCLUDE_PATTERNS = "spring.aot.data.accessors.include"; + + /** + * {@code String} Environment property to define Ant-style exclude patterns (comma-separated) matching package names + * (e.g. {@code com.acme.**}) or type names exclusion. Exclusion pattern matches are evaluated after inclusions for + * broad exclusion and selective inclusion. + */ + public static final String EXCLUDE_PATTERNS = "spring.aot.data.accessors.exclude"; + + private static final AntPathMatcher antPathMatcher = new AntPathMatcher("."); + + private AccessorContributionConfiguration(boolean enabled, Supplier include, Supplier exclude) { + this(enabled, Lazy.of(include), Lazy.of(exclude)); } - static boolean shouldContributeAccessors(Class type, boolean enabled, String include, String exclude) { + public static AccessorContributionConfiguration of(Environment environment) { + return new AccessorContributionConfiguration(environment.getProperty(ACCESSORS_ENABLED, Boolean.class, true), + () -> environment.getProperty(INCLUDE_PATTERNS, String.class, ""), + () -> environment.getProperty(EXCLUDE_PATTERNS, String.class, "")); + } + + boolean shouldContributeAccessors(Class type) { if (!enabled) { return false; } - AntPathMatcher antPathMatcher = new AntPathMatcher("."); - - if (StringUtils.hasText(include)) { + if (StringUtils.hasText(include.get())) { - String[] includes = include.split(","); + String[] includes = include.get().split(","); for (String includePattern : includes) { if (antPathMatcher.match(includePattern.trim(), type.getName())) { @@ -279,9 +313,9 @@ static boolean shouldContributeAccessors(Class type, boolean enabled, String } } - if (StringUtils.hasText(exclude)) { + if (StringUtils.hasText(exclude.get())) { - String[] excludes = exclude.split(","); + String[] excludes = exclude.get().split(","); for (String excludePattern : excludes) { if (antPathMatcher.match(excludePattern.trim(), type.getName())) { @@ -292,5 +326,7 @@ static boolean shouldContributeAccessors(Class type, boolean enabled, String return true; } + } + }