From b8d212e455f73993a3d576d45e18314f85243952 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 8 Aug 2025 10:33:23 +0200 Subject: [PATCH 1/4] Prepare issue branch --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b1d17888e9..063e5eca96 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 4.0.0-SNAPSHOT + 4.0.x-GH-3339-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From e2327a6e7aa609631a3f89c7fc75a5467e541242 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 8 Aug 2025 10:33:30 +0200 Subject: [PATCH 2/4] Use generated classname for writing aot repository content. This change makes sure register AOT repository code with a generated typename. This is necessary to allow recreation for repository code with different configuration/context settings during tests. --- .../aot/generate/AotRepositoryBuilder.java | 69 ++++++++------ .../AotRepositoryFragmentMetadata.java | 8 +- .../aot/generate/RepositoryContributor.java | 92 ++++++++++--------- ...toryBeanDefinitionPropertiesDecorator.java | 5 +- .../AotRepositoryBuilderUnitTests.java | 31 ++++++- .../RepositoryContributorUnitTests.java | 21 +++-- 6 files changed, 140 insertions(+), 86 deletions(-) diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java index d7c0c9dd96..e979b3542a 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java @@ -29,9 +29,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - -import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.Generated; +import org.springframework.aot.generate.GeneratedTypeReference; +import org.springframework.aot.hint.TypeReference; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata.ConstructorArgument; import org.springframework.data.repository.core.RepositoryInformation; @@ -39,7 +39,6 @@ import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryMethod; import org.springframework.javapoet.ClassName; -import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeName; @@ -64,6 +63,8 @@ class AotRepositoryBuilder { private @Nullable Consumer constructorCustomizer; private @Nullable MethodContributorFactory methodContributorFactory; private Consumer classCustomizer; + private @Nullable TypeReference targetClassName; + private RepositoryConstructorBuilder constructorBuilder; private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String moduleName, ProjectionFactory projectionFactory) { @@ -72,13 +73,9 @@ private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String this.moduleName = moduleName; this.projectionFactory = projectionFactory; - this.generationMetadata = new AotRepositoryFragmentMetadata(className()); - this.generationMetadata.addField(FieldSpec - .builder(TypeName.get(Log.class), "logger", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) - .initializer("$T.getLog($T.class)", TypeName.get(LogFactory.class), this.generationMetadata.getTargetTypeName()) - .build()); - + this.generationMetadata = new AotRepositoryFragmentMetadata(); this.classCustomizer = (builder) -> {}; + this.constructorBuilder = new RepositoryConstructorBuilder(generationMetadata); } /** @@ -131,15 +128,24 @@ public AotRepositoryBuilder withQueryMethodContributor(MethodContributorFactory return this; } - public AotBundle build() { + public AotRepositoryBuilder prepare(@Nullable ClassName targetClassName) { + if (targetClassName == null) { + withTargetClassName(null); + } else { + withTargetClassName(GeneratedTypeReference.of(targetClassName)); + } + if (constructorCustomizer != null) { + constructorCustomizer.accept(constructorBuilder); + } + return this; + } + + public AotBundle build(TypeSpec.Builder builder) { List methodMetadata = new ArrayList<>(); RepositoryComposition repositoryComposition = repositoryInformation.getRepositoryComposition(); - // start creating the type - TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) // - .addModifiers(Modifier.PUBLIC) // - .addAnnotation(Generated.class) // + builder.addModifiers(Modifier.PUBLIC) // .addJavadoc("AOT generated $L repository implementation for {@link $T}.\n", moduleName, repositoryInformation.getRepositoryInterface()); @@ -177,15 +183,31 @@ public AotBundle build() { return new AotBundle(javaFile, metadata); } - private MethodSpec buildConstructor() { + public AotBundle build() { + + ClassName className = ClassName + .bestGuess((targetClassName != null ? targetClassName : intendedTargetClassName()).getCanonicalName()); + return build(TypeSpec.classBuilder(className).addAnnotation(Generated.class)); + } - RepositoryConstructorBuilder constructorBuilder = new RepositoryConstructorBuilder( - generationMetadata); + public TypeReference intendedTargetClassName() { + return TypeReference.of("%s.%s".formatted(packageName(), typeName())); + } - if (constructorCustomizer != null) { - constructorCustomizer.accept(constructorBuilder); + public @Nullable TypeReference actualTargetClassName() { + + if (targetClassName == null) { + return null; } + return targetClassName; + } + AotRepositoryBuilder withTargetClassName(@Nullable TypeReference targetClassName) { + this.targetClassName = targetClassName; + return this; + } + + private MethodSpec buildConstructor() { return constructorBuilder.buildConstructor(); } @@ -252,15 +274,11 @@ public AotRepositoryFragmentMetadata getGenerationMetadata() { return generationMetadata; } - private ClassName className() { - return new ClassNameGenerator(ClassName.get(packageName(), typeName())).generateClassName("Aot", null); - } - - private String packageName() { + public String packageName() { return repositoryInformation.getRepositoryInterface().getPackageName(); } - private String typeName() { + public String typeName() { return "%sImpl".formatted(repositoryInformation.getRepositoryInterface().getSimpleName()); } @@ -280,7 +298,6 @@ public ProjectionFactory getProjectionFactory() { return projectionFactory; } - /** * Customizer interface to customize the AOT repository fragment constructor through * {@link AotRepositoryConstructorBuilder}. diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java index c8df5e6c01..f6423463c7 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryFragmentMetadata.java @@ -37,12 +37,10 @@ */ public class AotRepositoryFragmentMetadata { - private final ClassName className; private final Map fields = new HashMap<>(3); private final Map constructorArguments = new LinkedHashMap<>(3); - public AotRepositoryFragmentMetadata(ClassName className) { - this.className = className; + public AotRepositoryFragmentMetadata() { } /** @@ -65,10 +63,6 @@ public String fieldNameOf(Class type) { return null; } - public ClassName getTargetTypeName() { - return className; - } - /** * Add a field to the repository fragment. * diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java index 7a27ef2018..68e812312f 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java @@ -20,16 +20,18 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedTypeReference; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.AotBundle; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.javapoet.JavaFile; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.TypeName; /** @@ -42,8 +44,11 @@ public class RepositoryContributor { private static final Log logger = LogFactory.getLog(RepositoryContributor.class); + private static final String FEATURE_NAME = "AotRepository"; - private final AotRepositoryBuilder builder; + private AotRepositoryBuilder builder; + private final AotRepositoryContext repositoryContext; + private @Nullable TypeReference contributedTypeName; /** * Create a new {@code RepositoryContributor} for the given {@link AotRepositoryContext}. @@ -51,7 +56,9 @@ public class RepositoryContributor { * @param repositoryContext */ public RepositoryContributor(AotRepositoryContext repositoryContext) { - this.builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation(), + + this.repositoryContext = repositoryContext; + builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation(), repositoryContext.getModuleName(), createProjectionFactory()); } @@ -77,8 +84,8 @@ protected RepositoryInformation getRepositoryInformation() { return builder.getRepositoryInformation(); } - public String getContributedTypeName() { - return builder.getGenerationMetadata().getTargetTypeName().toString(); + public @Nullable TypeReference getContributedTypeName() { + return this.contributedTypeName; } public java.util.Map requiredArgs() { @@ -87,44 +94,47 @@ public java.util.Map requiredArgs() { public void contribute(GenerationContext generationContext) { - AotRepositoryBuilder.AotBundle aotBundle = builder.withClassCustomizer(this::customizeClass) // + builder.withClassCustomizer(this::customizeClass) // .withConstructorCustomizer(this::customizeConstructor) // - .withQueryMethodContributor(this::contributeQueryMethod) // - .build(); - - Class repositoryInterface = getRepositoryInformation().getRepositoryInterface(); - String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface); - - JavaFile javaFile = aotBundle.javaFile(); - String typeName = "%s.%s".formatted(javaFile.packageName(), javaFile.typeSpec().name()); - String repositoryJson; - - try { - repositoryJson = aotBundle.metadata().toJson().toString(2); - } catch (JSONException e) { - throw new RuntimeException(e); - } - - if (logger.isTraceEnabled()) { - logger.trace(""" - ------ AOT Repository.json: %s ------ - %s - ------------------- - """.formatted(repositoryJsonFileName, repositoryJson)); - - logger.trace(""" - ------ AOT Generated Repository: %s ------ - %s - ------------------- - """.formatted(typeName, javaFile)); - } - - // generate the files - generationContext.getGeneratedFiles().addSourceFile(javaFile); - generationContext.getGeneratedFiles().addResourceFile(repositoryJsonFileName, repositoryJson); + .withQueryMethodContributor(this::contributeQueryMethod); // + + GeneratedClass generatedClass = generationContext.getGeneratedClasses().getOrAddForFeatureComponent(FEATURE_NAME, + ClassName.bestGuess(builder.intendedTargetClassName().getCanonicalName()), targetTypeSpec -> { + + AotBundle aotBundle = builder.build(targetTypeSpec); + { + Class repositoryInterface = getRepositoryInformation().getRepositoryInterface(); + String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface); + String repositoryJson; + try { + repositoryJson = aotBundle.metadata().toJson().toString(2); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + if (logger.isTraceEnabled()) { + logger.trace(""" + ------ AOT Repository.json: %s ------ + %s + ------------------- + """.formatted(repositoryJsonFileName, repositoryJson)); + + logger.trace(""" + ------ AOT Generated Repository: %s ------ + %s + ------------------- + """.formatted(null, aotBundle.javaFile())); + } + + generationContext.getGeneratedFiles().addResourceFile(repositoryJsonFileName, repositoryJson); + } + }); + + builder.prepare(generatedClass.getName()); // initialize ctor argument resolution and set type name to target + this.contributedTypeName = GeneratedTypeReference.of(generatedClass.getName()); // generate native runtime hints - needed cause we're using the repository proxy - generationContext.getRuntimeHints().reflection().registerType(TypeReference.of(typeName), + generationContext.getRuntimeHints().reflection().registerType(this.contributedTypeName, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); } diff --git a/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java index d25e0f1cb3..c1b7029002 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java @@ -24,6 +24,7 @@ import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -51,6 +52,8 @@ public AotRepositoryBeanDefinitionPropertiesDecorator(Supplier inheri */ public CodeBlock decorate() { + Assert.notNull(repositoryContributor.getContributedTypeName(), "contributed type name must not be null"); + CodeBlock.Builder builder = CodeBlock.builder(); // bring in properties as usual builder.add(inheritedProperties.get()); @@ -78,7 +81,7 @@ public CodeBlock decorate() { } builder.addStatement("return RepositoryComposition.RepositoryFragments.just(new $L($L))", - repositoryContributor.getContributedTypeName(), + repositoryContributor.getContributedTypeName().getCanonicalName(), StringUtils.collectionToDelimitedString(repositoryContributor.requiredArgs().keySet(), ", ")); builder.unindent(); builder.add("}\n"); diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java index 1bb20248f3..cf9107378a 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.TypeReference; import org.springframework.data.geo.Metric; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; @@ -37,6 +38,7 @@ import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeName; import org.springframework.stereotype.Repository; @@ -66,8 +68,8 @@ void writesClassSkeleton() { assertThat(repoBuilder.build().javaFile().toString()) .contains("package %s;".formatted(UserRepository.class.getPackageName())) // same package as source repo .contains("@Generated") // marked as generated source - .contains("public class %sImpl__Aot".formatted(UserRepository.class.getSimpleName())) // target name - .contains("public UserRepositoryImpl__Aot()"); // default constructor if not arguments to wire + .contains("public class %sImpl".formatted(UserRepository.class.getSimpleName())) // target name + .contains("public UserRepositoryImpl"); // default constructor if not arguments to wire } @Test // GH-3279 @@ -80,11 +82,11 @@ void appliesCtorArguments() { ctor.addParameter("param2", String.class); ctor.addParameter("ctorScoped", TypeName.get(Object.class), false); }); - assertThat(repoBuilder.build().javaFile().toString()) // + assertThat(repoBuilder.prepare(null).build().javaFile().toString()) // .contains("private final Metric param1;") // .contains("private final String param2;") // .doesNotContain("private final Object ctorScoped;") // - .contains("public UserRepositoryImpl__Aot(Metric param1, String param2, Object ctorScoped)") // + .contains("public UserRepositoryImpl(Metric param1, String param2, Object ctorScoped)") // .contains("this.param1 = param1") // .contains("this.param2 = param2") // .doesNotContain("this.ctorScoped = ctorScoped"); @@ -100,8 +102,9 @@ void appliesCtorCodeBlock() { code.addStatement("throw new $T($S)", IllegalStateException.class, "initialization error"); }); }); + repoBuilder.prepare(null); assertThat(repoBuilder.build().javaFile().toString()).containsIgnoringWhitespaces( - "UserRepositoryImpl__Aot() { throw new IllegalStateException(\"initialization error\"); }"); + "UserRepositoryImpl() { throw new IllegalStateException(\"initialization error\"); }"); } @Test // GH-3279 @@ -180,6 +183,24 @@ void shouldContributeFragmentImplementationMetadata() { assertThat(method.fragment().implementation()).isEqualTo(DummyQuerydslPredicateExecutor.class.getName()); } + @Test // GH-3339 + void usesTargetTypeName() { + + AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", + new SpelAwareProxyProjectionFactory()); + repoBuilder.withConstructorCustomizer(ctor -> { + ctor.addParameter("param1", Metric.class); + ctor.addParameter("param2", String.class); + ctor.addParameter("ctorScoped", TypeName.get(Object.class), false); + }); + + TypeReference targetType = TypeReference.of("%s__AotPostfix".formatted(UserRepository.class.getCanonicalName())); + + assertThat(repoBuilder.prepare(ClassName.bestGuess(targetType.getCanonicalName())).build().javaFile().toString()) // + .contains("class %s".formatted(targetType.getSimpleName())) // + .contains("public %s(Metric param1, String param2, Object ctorScoped)".formatted(targetType.getSimpleName())); + } + interface UserRepository extends org.springframework.data.repository.Repository { String someMethod(); diff --git a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java index 8640c1eada..402898784d 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java @@ -87,7 +87,7 @@ public Map serialize() { repositoryContributor.contribute(generationContext); generationContext.writeGeneratedContent(); - String expectedTypeName = "example.UserRepositoryImpl__Aot"; + String expectedTypeName = "example.UserRepositoryImpl__AotRepository"; TestCompiler.forSystem().with(generationContext).compile(compiled -> { assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains(expectedTypeName); @@ -132,7 +132,7 @@ public Map serialize() { repositoryContributor.contribute(generationContext); generationContext.writeGeneratedContent(); - String expectedTypeName = "example.UserRepositoryImpl__Aot"; + String expectedTypeName = "example.UserRepositoryImpl__AotRepository"; TestCompiler.forSystem().with(generationContext).compile(compiled -> { String content = compiled.getResourceFile().getContent(); @@ -154,7 +154,9 @@ void callsMethodContributionForQueryMethod() { when(repositoryInformation.isQueryMethod(argThat(it -> it.getName().equals("findByFirstname")))).thenReturn(true); MethodCapturingRepositoryContributor contributor = new MethodCapturingRepositoryContributor(repositoryContext); - contributor.contribute(new TestGenerationContext(UserRepository.class)); + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + contributor.contribute(generationContext); + generationContext.writeGeneratedContent(); contributor.verifyContributionFor("findByFirstname"); } @@ -174,8 +176,11 @@ void doesNotContributeBaseClassMethods() { .thenReturn(true); when(repositoryInformation.isQueryMethod(argThat(it -> !it.getName().equals("findByFirstname")))).thenReturn(true); + TestGenerationContext testGenerationContext = new TestGenerationContext(UserRepository.class); MethodCapturingRepositoryContributor contributor = new MethodCapturingRepositoryContributor(repositoryContext); - contributor.contribute(new TestGenerationContext(UserRepository.class)); + contributor.contribute(testGenerationContext); + testGenerationContext.writeGeneratedContent(); + contributor.verifyContributedMethods().isNotEmpty().doesNotContainKey("findByFirstname"); } @@ -200,7 +205,9 @@ void doesNotContributeFragmentMethod() { when(repositoryInformation.isQueryMethod(argThat(it -> it.getName().equals("findByFirstname")))).thenReturn(true); MethodCapturingRepositoryContributor contributor = new MethodCapturingRepositoryContributor(repositoryContext); - contributor.contribute(new TestGenerationContext(UserRepository.class)); + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + contributor.contribute(generationContext); + generationContext.writeGeneratedContent(); contributor.verifyContributedMethods().isNotEmpty().doesNotContainKey("findUserByExtensionMethod"); } @@ -221,7 +228,9 @@ void contributesBaseClassMethodIfQueryMethod() { when(repositoryInformation.isQueryMethod(any())).thenReturn(true); MethodCapturingRepositoryContributor contributor = new MethodCapturingRepositoryContributor(repositoryContext); - contributor.contribute(new TestGenerationContext(UserRepository.class)); + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + contributor.contribute(generationContext); + generationContext.writeGeneratedContent(); contributor.verifyContributedMethods().containsKey("findByFirstname").hasSizeGreaterThan(1); } From 9fc4240b12c830cb19f5ef073073627f9ffded9a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Aug 2025 12:42:59 +0200 Subject: [PATCH 3/4] Polishing. Capture class name from within the builder callback. Simplify name handling. --- .../aot/generate/AotRepositoryBuilder.java | 48 ++++++--------- .../aot/generate/RepositoryContributor.java | 60 +++++++++---------- ...toryBeanDefinitionPropertiesDecorator.java | 2 +- .../AotRepositoryBuilderUnitTests.java | 7 +-- 4 files changed, 51 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java index e979b3542a..b9f80b2202 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java @@ -29,9 +29,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.Generated; -import org.springframework.aot.generate.GeneratedTypeReference; -import org.springframework.aot.hint.TypeReference; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata.ConstructorArgument; import org.springframework.data.repository.core.RepositoryInformation; @@ -62,9 +61,9 @@ class AotRepositoryBuilder { private @Nullable Consumer constructorCustomizer; private @Nullable MethodContributorFactory methodContributorFactory; + private @Nullable String targetClassName; private Consumer classCustomizer; - private @Nullable TypeReference targetClassName; - private RepositoryConstructorBuilder constructorBuilder; + private final RepositoryConstructorBuilder constructorBuilder; private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String moduleName, ProjectionFactory projectionFactory) { @@ -128,15 +127,15 @@ public AotRepositoryBuilder withQueryMethodContributor(MethodContributorFactory return this; } - public AotRepositoryBuilder prepare(@Nullable ClassName targetClassName) { - if (targetClassName == null) { - withTargetClassName(null); - } else { - withTargetClassName(GeneratedTypeReference.of(targetClassName)); - } - if (constructorCustomizer != null) { - constructorCustomizer.accept(constructorBuilder); - } + /** + * Configure the {@link Class#getSimpleName() simple class name} of the generated repository implementation. + * + * @param className the class name to use for the generated repository implementation. Defaults to the simple + * {@link RepositoryInformation#getRepositoryInterface()} class name suffixed with {@code Impl} + * @return {@code this}. + */ + public AotRepositoryBuilder withClassName(@Nullable String className) { + this.targetClassName = className; return this; } @@ -184,30 +183,19 @@ public AotBundle build(TypeSpec.Builder builder) { } public AotBundle build() { - - ClassName className = ClassName - .bestGuess((targetClassName != null ? targetClassName : intendedTargetClassName()).getCanonicalName()); - return build(TypeSpec.classBuilder(className).addAnnotation(Generated.class)); + return build(TypeSpec.classBuilder(getClassName()).addAnnotation(Generated.class)); } - public TypeReference intendedTargetClassName() { - return TypeReference.of("%s.%s".formatted(packageName(), typeName())); + public ClassName getClassName() { + return ClassName.get(packageName(), targetClassName != null ? targetClassName : typeName()); } - public @Nullable TypeReference actualTargetClassName() { + private MethodSpec buildConstructor() { - if (targetClassName == null) { - return null; + if (constructorCustomizer != null) { + constructorCustomizer.accept(constructorBuilder); } - return targetClassName; - } - AotRepositoryBuilder withTargetClassName(@Nullable TypeReference targetClassName) { - this.targetClassName = targetClassName; - return this; - } - - private MethodSpec buildConstructor() { return constructorBuilder.buildConstructor(); } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java index 68e812312f..6c50a51b55 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GeneratedTypeReference; import org.springframework.aot.generate.GenerationContext; @@ -31,7 +32,6 @@ import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.javapoet.ClassName; import org.springframework.javapoet.TypeName; /** @@ -46,8 +46,7 @@ public class RepositoryContributor { private static final Log logger = LogFactory.getLog(RepositoryContributor.class); private static final String FEATURE_NAME = "AotRepository"; - private AotRepositoryBuilder builder; - private final AotRepositoryContext repositoryContext; + private final AotRepositoryBuilder builder; private @Nullable TypeReference contributedTypeName; /** @@ -57,7 +56,6 @@ public class RepositoryContributor { */ public RepositoryContributor(AotRepositoryContext repositoryContext) { - this.repositoryContext = repositoryContext; builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation(), repositoryContext.getModuleName(), createProjectionFactory()); } @@ -99,38 +97,38 @@ public void contribute(GenerationContext generationContext) { .withQueryMethodContributor(this::contributeQueryMethod); // GeneratedClass generatedClass = generationContext.getGeneratedClasses().getOrAddForFeatureComponent(FEATURE_NAME, - ClassName.bestGuess(builder.intendedTargetClassName().getCanonicalName()), targetTypeSpec -> { + builder.getClassName(), targetTypeSpec -> { + + // capture the actual type name early on so that we can use it in the constructor. + builder.withClassName(targetTypeSpec.build().name()); AotBundle aotBundle = builder.build(targetTypeSpec); - { - Class repositoryInterface = getRepositoryInformation().getRepositoryInterface(); - String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface); - String repositoryJson; - try { - repositoryJson = aotBundle.metadata().toJson().toString(2); - } catch (JSONException e) { - throw new RuntimeException(e); - } - - if (logger.isTraceEnabled()) { - logger.trace(""" - ------ AOT Repository.json: %s ------ - %s - ------------------- - """.formatted(repositoryJsonFileName, repositoryJson)); - - logger.trace(""" - ------ AOT Generated Repository: %s ------ - %s - ------------------- - """.formatted(null, aotBundle.javaFile())); - } - - generationContext.getGeneratedFiles().addResourceFile(repositoryJsonFileName, repositoryJson); + Class repositoryInterface = getRepositoryInformation().getRepositoryInterface(); + String repositoryJsonFileName = getRepositoryJsonFileName(repositoryInterface); + String repositoryJson; + try { + repositoryJson = aotBundle.metadata().toJson().toString(2); + } catch (JSONException e) { + throw new RuntimeException(e); } + + if (logger.isTraceEnabled()) { + logger.trace(""" + ------ AOT Repository.json: %s ------ + %s + ------------------- + """.formatted(repositoryJsonFileName, repositoryJson)); + + logger.trace(""" + ------ AOT Generated Repository: %s ------ + %s + ------------------- + """.formatted(null, aotBundle.javaFile())); + } + + generationContext.getGeneratedFiles().addResourceFile(repositoryJsonFileName, repositoryJson); }); - builder.prepare(generatedClass.getName()); // initialize ctor argument resolution and set type name to target this.contributedTypeName = GeneratedTypeReference.of(generatedClass.getName()); // generate native runtime hints - needed cause we're using the repository proxy diff --git a/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java index c1b7029002..23e90fe744 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryBeanDefinitionPropertiesDecorator.java @@ -52,7 +52,7 @@ public AotRepositoryBeanDefinitionPropertiesDecorator(Supplier inheri */ public CodeBlock decorate() { - Assert.notNull(repositoryContributor.getContributedTypeName(), "contributed type name must not be null"); + Assert.notNull(repositoryContributor.getContributedTypeName(), "Contributed type name must not be null"); CodeBlock.Builder builder = CodeBlock.builder(); // bring in properties as usual diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java index cf9107378a..f184620889 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java @@ -38,7 +38,6 @@ import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.javapoet.ClassName; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeName; import org.springframework.stereotype.Repository; @@ -82,7 +81,7 @@ void appliesCtorArguments() { ctor.addParameter("param2", String.class); ctor.addParameter("ctorScoped", TypeName.get(Object.class), false); }); - assertThat(repoBuilder.prepare(null).build().javaFile().toString()) // + assertThat(repoBuilder.withClassName(null).build().javaFile().toString()) // .contains("private final Metric param1;") // .contains("private final String param2;") // .doesNotContain("private final Object ctorScoped;") // @@ -102,7 +101,7 @@ void appliesCtorCodeBlock() { code.addStatement("throw new $T($S)", IllegalStateException.class, "initialization error"); }); }); - repoBuilder.prepare(null); + repoBuilder.withClassName(null); assertThat(repoBuilder.build().javaFile().toString()).containsIgnoringWhitespaces( "UserRepositoryImpl() { throw new IllegalStateException(\"initialization error\"); }"); } @@ -196,7 +195,7 @@ void usesTargetTypeName() { TypeReference targetType = TypeReference.of("%s__AotPostfix".formatted(UserRepository.class.getCanonicalName())); - assertThat(repoBuilder.prepare(ClassName.bestGuess(targetType.getCanonicalName())).build().javaFile().toString()) // + assertThat(repoBuilder.withClassName(targetType.getSimpleName()).build().javaFile().toString()) // .contains("class %s".formatted(targetType.getSimpleName())) // .contains("public %s(Metric param1, String param2, Object ctorScoped)".formatted(targetType.getSimpleName())); } From 72f94b3238bb66f1be556b62a94d685d9b5916e9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Aug 2025 13:10:56 +0200 Subject: [PATCH 4/4] Add workaround for deferred constructor argument discovery. --- .../data/repository/aot/generate/RepositoryContributor.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java index 6c50a51b55..7b0399b0f7 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java @@ -96,6 +96,12 @@ public void contribute(GenerationContext generationContext) { .withConstructorCustomizer(this::customizeConstructor) // .withQueryMethodContributor(this::contributeQueryMethod); // + // TODO: temporary fix until we have a better representation of constructor arguments + // decouple the description of arguments from the actual code used in the constructor initialization, super calls, + // etc. + RepositoryConstructorBuilder constructorBuilder = new RepositoryConstructorBuilder(builder.getGenerationMetadata()); + customizeConstructor(constructorBuilder); + GeneratedClass generatedClass = generationContext.getGeneratedClasses().getOrAddForFeatureComponent(FEATURE_NAME, builder.getClassName(), targetTypeSpec -> {