From c9178431327356aaedc22b0e94097154e4578bc7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Fri, 13 Sep 2024 11:47:41 +0200 Subject: [PATCH 01/14] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cdd68fe2d4..0d10b48560 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> - <version>3.5.0-SNAPSHOT</version> + <version>3.5.x-GENERATED-REPOSITORIES-SNAPSHOT</version> <name>Spring Data Core</name> <description>Core Spring concepts underpinning every Spring Data module.</description> From 0d6521c1908a847fcc8029b0ea1d8e2616b31b5e Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Fri, 20 Sep 2024 11:36:54 +0200 Subject: [PATCH 02/14] Hacking - add code contributor for derived repository methods --- .../aot/generate/AotCodeContributor.java | 25 ++ .../aot/generate/AotRepositoryBuilder.java | 175 +++++++++++++ .../AotRepositoryConstructorBuilder.java | 99 +++++++ .../AotRepositoryDerivedMethodBuilder.java | 74 ++++++ .../generate/AotRepositoryMethodBuilder.java | 181 +++++++++++++ .../aot/generate/RepositoryContributor.java | 69 +++++ src/test/java/example/UserRepository.java | 46 ++++ .../generate/RepositoryBuilderUnitTests.java | 246 ++++++++++++++++++ 8 files changed, 915 insertions(+) create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotCodeContributor.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java create mode 100644 src/test/java/example/UserRepository.java create mode 100644 src/test/java/org/springframework/data/repository/aot/generate/RepositoryBuilderUnitTests.java diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotCodeContributor.java b/src/main/java/org/springframework/data/repository/aot/generate/AotCodeContributor.java new file mode 100644 index 0000000000..350afa7681 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotCodeContributor.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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.generate; + +import org.springframework.aot.generate.GenerationContext; + +/** + * @author Christoph Strobl + */ +public interface AotCodeContributor { + void contribute(GenerationContext generationContext); +} 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 new file mode 100644 index 0000000000..26e112f432 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilder.java @@ -0,0 +1,175 @@ +/* + * Copyright 2024 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.generate; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Consumer; + +import javax.lang.model.element.Modifier; + +import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.FieldSpec; +import org.springframework.javapoet.JavaFile; +import org.springframework.javapoet.TypeName; +import org.springframework.javapoet.TypeSpec; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * @author Christoph Strobl + */ +public class AotRepositoryBuilder { + + private final RepositoryInformation repositoryInformation; + private final GenerationMetadata generationMetadata; + + private Consumer<AotRepositoryConstructorBuilder> constructorBuilderCustomizer; + private Consumer<AotRepositoryMethodBuilder> derivedMethodBuilderCustomizer; + private RepositoryCustomizer customizer; + + public static AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation) { + return new AotRepositoryBuilder(repositoryInformation); + } + + AotRepositoryBuilder(RepositoryInformation repositoryInformation) { + + this.repositoryInformation = repositoryInformation; + this.generationMetadata = new GenerationMetadata(className()); + this.customizer = (info, metadata, builder) -> {}; + } + + public JavaFile javaFile() { + + // start creating the type + TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) + .addModifiers(Modifier.PUBLIC); + // TODO: we do not need that here + // .addSuperinterface(repositoryInformation.getRepositoryInterface()); + + // create the constructor + AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation, + generationMetadata); + constructorBuilderCustomizer.accept(constructorBuilder); + builder.addMethod(constructorBuilder.buildConstructor()); + + // write methods + // start with the derived ones + ReflectionUtils.doWithMethods(repositoryInformation.getRepositoryInterface(), method -> { + + AotRepositoryDerivedMethodBuilder derivedMethodBuilder = new AotRepositoryDerivedMethodBuilder(method, + repositoryInformation, generationMetadata); + derivedMethodBuilderCustomizer.accept(derivedMethodBuilder); + builder.addMethod(derivedMethodBuilder.buildMethod()); + }, it -> { + return !repositoryInformation.isBaseClassMethod(it) && !repositoryInformation.isCustomMethod(it) && !it.isDefault(); + }); + + // write fields at the end so we make sure to capture things added by methods + generationMetadata.getFields().values().forEach(builder::addField); + + // finally customize the file itself + this.customizer.customize(repositoryInformation, generationMetadata, builder); + return JavaFile.builder(packageName(), builder.build()).build(); + } + + AotRepositoryBuilder withConstructorCustomizer(Consumer<AotRepositoryConstructorBuilder> constuctorBuilder) { + + this.constructorBuilderCustomizer = constuctorBuilder; + return this; + } + + AotRepositoryBuilder withDerivedMethodCustomizer(Consumer<AotRepositoryMethodBuilder> methodBuilder) { + this.derivedMethodBuilderCustomizer = methodBuilder; + return this; + } + + AotRepositoryBuilder withFileCustomizer(RepositoryCustomizer repositoryCustomizer) { + + this.customizer = repositoryCustomizer; + return this; + } + + GenerationMetadata getGenerationMetadata() { + return generationMetadata; + } + + private ClassName className() { + return new ClassNameGenerator(ClassName.get(packageName(), typeName())).generateClassName("Aot", null); + } + + private String packageName() { + return repositoryInformation.getRepositoryInterface().getPackageName(); + } + + private String typeName() { + return "%sImpl".formatted(repositoryInformation.getRepositoryInterface().getSimpleName()); + } + + public interface RepositoryCustomizer { + + void customize(RepositoryInformation repositoryInformation, GenerationMetadata metadata, TypeSpec.Builder builder); + } + + public class GenerationMetadata { + + private ClassName className; + private Map<String, FieldSpec> fields = new HashMap<>(); + + public GenerationMetadata(ClassName className) { + this.className = className; + } + + @Nullable + public String fieldNameOf(Class<?> type) { + + // TODO: might be more than one match + return fields.entrySet().stream().filter(entry -> entry.getValue().name.equals(type.getName())).map(Entry::getKey) + .findFirst().orElse(null); + } + + public ClassName getTargetTypeName() { + return className; + } + + public String getTargetTypeSimpleName() { + return className.simpleName(); + } + + public String getTargetTypePackageName() { + return className.packageName(); + } + + public boolean hasField(String fieldName) { + return fields.containsKey(fieldName); + } + + public void addField(String fieldName, TypeName type, Modifier... modifiers) { + fields.put(fieldName, FieldSpec.builder(type, fieldName, modifiers).build()); + } + + public void addField(FieldSpec fieldSpec) { + fields.put(fieldSpec.name, fieldSpec); + } + + Map<String, FieldSpec> getFields() { + return fields; + } + } +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java new file mode 100644 index 0000000000..91bd3fedb4 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java @@ -0,0 +1,99 @@ +/* + * Copyright 2024 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.generate; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.lang.model.element.Modifier; + +import org.springframework.core.ResolvableType; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.javapoet.TypeName; + +/** + * @author Christoph Strobl + */ +public class AotRepositoryConstructorBuilder { + + private final RepositoryInformation repositoryInformation; + private final AotRepositoryBuilder.GenerationMetadata metadata; + private final Map<String, TypeName> constructorArguments; + + private ConstructorCustomizer customizer = (info, builder) -> {}; + + public AotRepositoryConstructorBuilder(RepositoryInformation repositoryInformation, + AotRepositoryBuilder.GenerationMetadata metadata) { + + this.repositoryInformation = repositoryInformation; + this.metadata = metadata; + this.constructorArguments = new LinkedHashMap<>(3); + addParameter("delegate", getDefaultStoreRepositoryImplementationType(repositoryInformation)); + } + + public void addParameter(String parameterName, Class<?> type) { + + ResolvableType resolvableType = ResolvableType.forClass(type); + if (!resolvableType.hasGenerics() || !resolvableType.hasResolvableGenerics()) { + addParameter(parameterName, TypeName.get(type)); + return; + } + addParameter(parameterName, ParameterizedTypeName.get(type, resolvableType.resolveGenerics())); + } + + public void addParameter(String parameterName, TypeName type) { + + this.constructorArguments.put(parameterName, type); + this.metadata.addField(parameterName, type, Modifier.PRIVATE, Modifier.FINAL); + } + + public void customize(ConstructorCustomizer customizer) { + this.customizer = customizer; + } + + MethodSpec buildConstructor() { + + MethodSpec.Builder builder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); + for (Entry<String, TypeName> parameter : constructorArguments.entrySet()) { + builder.addParameter(parameter.getValue(), parameter.getKey()).addStatement("this.$N = $N", parameter.getKey(), + parameter.getKey()); + } + customizer.customize(repositoryInformation, builder); + return builder.build(); + } + + private static TypeName getDefaultStoreRepositoryImplementationType(RepositoryInformation repositoryInformation) { + + ResolvableType resolvableType = ResolvableType.forClass(repositoryInformation.getRepositoryBaseClass()); + if (resolvableType.hasGenerics()) { + List<Class<?>> generics = List.of(); + if (resolvableType.getGenerics().length == 2) { // TODO: Find some other way to resolve generics + generics = List.of(repositoryInformation.getDomainType(), repositoryInformation.getIdType()); + } + return ParameterizedTypeName.get(repositoryInformation.getRepositoryBaseClass(), generics.toArray(Class[]::new)); + } + return TypeName.get(repositoryInformation.getRepositoryBaseClass()); + } + + public interface ConstructorCustomizer { + + void customize(RepositoryInformation repositoryInformation, MethodSpec.Builder builder); + } +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java new file mode 100644 index 0000000000..187b931a08 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 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.generate; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.GenerationMetadata; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.javapoet.TypeName; + +/** + * @author Christoph Strobl + */ +public class AotRepositoryDerivedMethodBuilder extends AotRepositoryMethodBuilder { + + public AotRepositoryDerivedMethodBuilder(Method method, RepositoryInformation repositoryInformation, + GenerationMetadata metadata) { + + super(method, repositoryInformation, metadata); + + initReturnType(method, repositoryInformation); + initParameters(method, repositoryInformation); + } + + private void initParameters(Method method, RepositoryInformation repositoryInformation) { + + ResolvableType repositoryInterface = ResolvableType.forClass(repositoryInformation.getRepositoryInterface()); + if (method.getParameterCount() > 0) { + int index = 0; + for (Parameter parameter : method.getParameters()) { + + ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(new MethodParameter(method, index), + repositoryInterface); + + TypeName parameterType = TypeName.get(resolvableParameterType.resolve()); + if (resolvableParameterType.hasGenerics()) { + parameterType = ParameterizedTypeName.get(resolvableParameterType.resolve(), + resolvableParameterType.resolveGenerics()); + } + addParameter(parameter.getName(), parameterType); + } + } + } + + private void initReturnType(Method method, RepositoryInformation repositoryInformation) { + + ResolvableType returnType = ResolvableType.forMethodReturnType(method, + repositoryInformation.getRepositoryInterface()); + + TypeName returnTypeName = TypeName.get(returnType.resolve()); + if (returnType.hasGenerics()) { + returnTypeName = ParameterizedTypeName.get(returnType.resolve(), returnType.resolveGenerics()); + } + + setReturnType(returnTypeName); + } +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java new file mode 100644 index 0000000000..90ef94bebc --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java @@ -0,0 +1,181 @@ +/* + * Copyright 2024 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.generate; + +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import javax.lang.model.element.Modifier; + +import org.springframework.core.ResolvableType; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.javapoet.FieldSpec; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterSpec; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.javapoet.TypeName; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + */ +public class AotRepositoryMethodBuilder { + + private final Method method; + private final RepositoryInformation repositoryInformation; + private final MethodGenerationMetadata metadata; + + private RepositoryMethodCustomizer customizer = (info, md, builder) -> {}; + + public AotRepositoryMethodBuilder(Method method, RepositoryInformation repositoryInformation, + AotRepositoryBuilder.GenerationMetadata metadata) { + + this.method = method; + this.repositoryInformation = repositoryInformation; + this.metadata = new MethodGenerationMetadata(metadata, method); + } + + public void addParameter(String parameterName, Class<?> type) { + + ResolvableType resolvableType = ResolvableType.forClass(type); + if (!resolvableType.hasGenerics() || !resolvableType.hasResolvableGenerics()) { + addParameter(parameterName, TypeName.get(type)); + return; + } + addParameter(parameterName, ParameterizedTypeName.get(type, resolvableType.resolveGenerics())); + } + + public void addParameter(String parameterName, TypeName type) { + addParameter(ParameterSpec.builder(type, parameterName).build()); + } + + public void addParameter(ParameterSpec parameter) { + this.metadata.methodArguments.put(parameter.name, parameter); + } + + public void setReturnType(@Nullable TypeName returnType) { + this.metadata.returnType = returnType; + } + + public void customize(RepositoryMethodCustomizer customizer) { + this.customizer = customizer; + } + + MethodSpec buildMethod() { + + MethodSpec.Builder builder = MethodSpec.methodBuilder(method.getName()).addModifiers(Modifier.PUBLIC); + if (!metadata.returnsVoid()) { + builder.returns(metadata.getReturnType()); + } + builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", method.getDeclaringClass(), + method.getName(), StringUtils.collectionToCommaDelimitedString( + metadata.methodArguments.values().stream().map(it -> it.type.toString()).collect(Collectors.toList()))); + metadata.methodArguments.forEach((name, spec) -> builder.addParameter(spec)); + customizer.customize(repositoryInformation, metadata, builder); + return builder.build(); + } + + public interface RepositoryMethodCustomizer { + + void customize(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, + MethodSpec.Builder builder); + } + + public static class MethodGenerationMetadata { + + private final AotRepositoryBuilder.GenerationMetadata generationMetadata; + private final Method repositoryMethod; + private final Map<String, ParameterSpec> methodArguments; + @Nullable private TypeName returnType; + + public MethodGenerationMetadata(AotRepositoryBuilder.GenerationMetadata generationMetadata, + Method repositoryMethod) { + this.generationMetadata = generationMetadata; + this.repositoryMethod = repositoryMethod; + this.methodArguments = new LinkedHashMap<>(); + } + + public Method getRepositoryMethod() { + return repositoryMethod; + } + + @Nullable + public String getParameterNameOf(Class<?> type) { + for (Entry<String, ParameterSpec> entry : methodArguments.entrySet()) { + if (entry.getValue().type.equals(TypeName.get(type))) { + return entry.getKey(); + } + } + return null; + } + + public boolean returnsVoid() { + return repositoryMethod.getReturnType().equals(Void.TYPE); + } + + @Nullable + public TypeName getReturnType() { + return returnType; + } + + @Nullable + public String getSortParameterName() { + return getParameterNameOf(Sort.class); + } + + @Nullable + public String getPageableParameterName() { + return getParameterNameOf(Pageable.class); + } + + @Nullable + public String getLimitParameterName() { + return getParameterNameOf(Limit.class); + } + + public void addParameter(ParameterSpec parameterSpec) { + this.methodArguments.put(parameterSpec.name, parameterSpec); + } + + @Nullable + public String fieldNameOf(Class<?> type) { + return generationMetadata.fieldNameOf(type); + } + + public boolean hasField(String fieldName) { + return generationMetadata.hasField(fieldName); + } + + public void addField(String fieldName, TypeName type, Modifier... modifiers) { + generationMetadata.addField(fieldName, type, modifiers); + } + + public void addField(FieldSpec fieldSpec) { + generationMetadata.addField(fieldSpec); + } + + public Map<String, FieldSpec> getFields() { + return generationMetadata.getFields(); + } + } +} 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 new file mode 100644 index 0000000000..1fffb7ffcb --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryContributor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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.generate; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.javapoet.JavaFile; +import org.springframework.javapoet.TypeSpec; + +/** + * @author Christoph Strobl + */ +public class RepositoryContributor implements AotCodeContributor { + + private AotRepositoryContext repositoryContext; + + public RepositoryContributor(AotRepositoryContext repositoryContext) { + this.repositoryContext = repositoryContext; + } + + @Override + public void contribute(GenerationContext generationContext) { + + RepositoryInformation repositoryInformation = repositoryContext.getRepositoryInformation(); + + AotRepositoryBuilder builder = AotRepositoryBuilder.forRepository(repositoryInformation); + builder.withFileCustomizer(this::customizeFile); + builder.withConstructorCustomizer(this::customizeConstructor); + builder.withDerivedMethodCustomizer(this::customizeDerivedMethod); + + JavaFile file = builder.javaFile(); + + System.out.printf("------ %s.%s ------\n", file.packageName, file.typeSpec.name); + System.out.println(file); + System.out.println("-------------------"); + + generationContext.getGeneratedFiles().addSourceFile(file); + } + + /** + * Customization Hook for Store implementations + */ + protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + + } + + protected void customizeFile(RepositoryInformation information, AotRepositoryBuilder.GenerationMetadata metadata, + TypeSpec.Builder builder) { + + } + + protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + + } +} diff --git a/src/test/java/example/UserRepository.java b/src/test/java/example/UserRepository.java new file mode 100644 index 0000000000..d87b9237ad --- /dev/null +++ b/src/test/java/example/UserRepository.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 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 example; + +import example.UserRepository.User; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + */ +public interface UserRepository extends CrudRepository<User, Long> { + + User findByFirstname(String firstname); + + List<User> findByFirstnameIn(List<String> firstnames); + + Long countAllByLastname(String lastname); + + Long countAll(); + + void doSomething(); + + default Long theDefaultMethod() { + return countAll(); + } + + class User { + String firstname; + } +} diff --git a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryBuilderUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryBuilderUnitTests.java new file mode 100644 index 0000000000..426944c700 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryBuilderUnitTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2024 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.generate; + +import static org.assertj.core.api.Assertions.assertThat; + +import example.UserRepository; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.test.tools.ClassFile; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2024/09 + */ +// testclass needs to be public otherwise we cannot reference the repository within +public class RepositoryBuilderUnitTests { + + @Test + void compileInstance() { + + DummyAotRepoContext aotContext = new DummyAotRepoContext(UserRepository.class, null); + RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { + + @Override + protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + methodBuilder.customize(((repositoryInformation, metadata, builder) -> { + if (!metadata.returnsVoid()) { + builder.addStatement("return null"); + } + })); + } + }; + + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + repositoryContributor.contribute(generationContext); + generationContext.writeGeneratedContent(); + + TestCompiler.forSystem().with(generationContext).withClasses(aotContext.getRequiredContextFiles()) + .compile(compiled -> { + assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains("example.UserRepositoryImpl__Aot"); + }); + } + + public static abstract class SimpleDummyRepository<T, ID> implements CrudRepository<T, ID> {} + + public static class DummyAotRepoContext implements AotRepositoryContext { + + private final StubRepositoryInformation repositoryInformation; + + public DummyAotRepoContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { + this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return null; + } + + @Override + public TypeIntrospector introspectType(String typeName) { + return null; + } + + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return null; + } + + @Override + public String getBeanName() { + return "dummyRepository"; + } + + @Override + public Set<String> getBasePackages() { + return Set.of("org.springframework.data.dummy.repository.aot"); + } + + @Override + public Set<Class<? extends Annotation>> getIdentifyingAnnotations() { + return Set.of(); + } + + @Override + public RepositoryInformation getRepositoryInformation() { + return repositoryInformation; + } + + @Override + public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() { + return Set.of(); + } + + @Override + public Set<Class<?>> getResolvedTypes() { + return Set.of(); + } + + public List<ClassFile> getRequiredContextFiles() { + return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); + } + + static ClassFile classFileForType(Class<?> type) { + + String name = type.getName(); + ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); + + try { + return ClassFile.of(name, cpr.getContentAsByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); + } + } + } + + public static class StubRepositoryInformation implements RepositoryInformation { + + private final RepositoryMetadata metadata; + private final RepositoryComposition baseComposition; + + public StubRepositoryInformation(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { + + this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface); + this.baseComposition = composition != null ? composition + : RepositoryComposition.of(RepositoryFragment.structural(SimpleDummyRepository.class)); + } + + @Override + public TypeInformation<?> getIdTypeInformation() { + return metadata.getIdTypeInformation(); + } + + @Override + public TypeInformation<?> getDomainTypeInformation() { + return metadata.getDomainTypeInformation(); + } + + @Override + public Class<?> getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + @Override + public TypeInformation<?> getReturnType(Method method) { + return metadata.getReturnType(method); + } + + @Override + public Class<?> getReturnedDomainClass(Method method) { + return metadata.getReturnedDomainClass(method); + } + + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return false; + } + + @Override + public Set<Class<?>> getAlternativeDomainTypes() { + return null; + } + + @Override + public boolean isReactiveRepository() { + return false; + } + + @Override + public Set<RepositoryFragment<?>> getFragments() { + return null; + } + + @Override + public boolean isBaseClassMethod(Method method) { + + return baseComposition.findMethod(method).isPresent(); + } + + @Override + public boolean isCustomMethod(Method method) { + return false; + } + + @Override + public boolean isQueryMethod(Method method) { + return false; + } + + @Override + public Streamable<Method> getQueryMethods() { + return null; + } + + @Override + public Class<?> getRepositoryBaseClass() { + return SimpleDummyRepository.class; + } + + @Override + public Method getTargetClassMethod(Method method) { + return null; + } + + } +} From 88cf81ef4e10a036e6d9fd2866ee843939002541 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Mon, 23 Sep 2024 08:20:47 +0200 Subject: [PATCH 03/14] fix field name lookup for type --- .../repository/aot/generate/AotRepositoryBuilder.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 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 26e112f432..07d2065ce9 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 @@ -139,9 +139,14 @@ public GenerationMetadata(ClassName className) { @Nullable public String fieldNameOf(Class<?> type) { - // TODO: might be more than one match - return fields.entrySet().stream().filter(entry -> entry.getValue().name.equals(type.getName())).map(Entry::getKey) - .findFirst().orElse(null); + TypeName lookup = TypeName.get(type).withoutAnnotations(); + for(Entry<String, FieldSpec> field : fields.entrySet()) { + if(field.getValue().type.withoutAnnotations().equals(lookup)) { + return field.getKey(); + } + } + + return null; } public ClassName getTargetTypeName() { From 04125c16b77bee0fda72ceb5b1db9209dc2889a1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Mon, 23 Sep 2024 10:53:40 +0200 Subject: [PATCH 04/14] Add logging and feature flag --- .../aot/generate/RepositoryContributor.java | 20 +- .../config/AotRepositoryContext.java | 6 + .../DummyModuleAotRepositoryContext.java | 105 +++++++++ ...ModuleDefaultRepositoryImplementation.java | 28 +++ .../generate/RepositoryBuilderUnitTests.java | 213 +----------------- .../RepositoryContributorUnitTests.java | 64 ++++++ .../generate/StubRepositoryInformation.java | 127 +++++++++++ 7 files changed, 349 insertions(+), 214 deletions(-) create mode 100644 src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java create mode 100644 src/test/java/org/springframework/data/repository/aot/generate/DummyModuleDefaultRepositoryImplementation.java create mode 100644 src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java create mode 100644 src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java 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 1fffb7ffcb..3cdcaeca34 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 @@ -15,6 +15,8 @@ */ package org.springframework.data.repository.aot.generate; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.aot.generate.GenerationContext; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; @@ -26,6 +28,8 @@ */ public class RepositoryContributor implements AotCodeContributor { + private static final Log logger = LogFactory.getLog(RepositoryContributor.class); + private AotRepositoryContext repositoryContext; public RepositoryContributor(AotRepositoryContext repositoryContext) { @@ -44,11 +48,21 @@ public void contribute(GenerationContext generationContext) { JavaFile file = builder.javaFile(); - System.out.printf("------ %s.%s ------\n", file.packageName, file.typeSpec.name); - System.out.println(file); - System.out.println("-------------------"); + if (logger.isTraceEnabled()) { + logger.trace(""" + ------ AOT Generated Repository: %s.%s ------ + %s + ------------------- + """.formatted(file.packageName, file.typeSpec.name, file)); + } + + // generate the file itself generationContext.getGeneratedFiles().addSourceFile(file); + + // register it in spring.factories + String registration = "%s=%s.%s".formatted(repositoryInformation.getRepositoryInterface().getName(), file.packageName, file.typeSpec.name); + generationContext.getGeneratedFiles().addResourceFile("META-INF/spring.factories", registration); } /** 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 6e18dc727b..e38c1e5a58 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java @@ -18,6 +18,7 @@ import java.lang.annotation.Annotation; import java.util.Set; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.aot.AotContext; import org.springframework.data.repository.core.RepositoryInformation; @@ -32,6 +33,8 @@ */ public interface AotRepositoryContext extends AotContext { + String GENERATED_REPOSITORIES_ENABLED = "spring.aot.repositories.enabled"; + /** * @return the {@link String bean name} of the repository / factory bean. */ @@ -64,4 +67,7 @@ public interface AotRepositoryContext extends AotContext { */ Set<Class<?>> getResolvedTypes(); + default boolean aotGeneratedRepositoriesEnabled() { + return SpringProperties.getFlag(GENERATED_REPOSITORIES_ENABLED); + } } 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 new file mode 100644 index 0000000000..b873f8efe7 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleAotRepositoryContext.java @@ -0,0 +1,105 @@ +/* + * Copyright 2024 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.generate; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.test.tools.ClassFile; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.lang.Nullable; + +/** + * Dummy {@link AotRepositoryContext} used to simulate module specific repository implementation. + * + * @author Christoph Strobl + */ +class DummyModuleAotRepositoryContext implements AotRepositoryContext { + + private final StubRepositoryInformation repositoryInformation; + + public DummyModuleAotRepositoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { + this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return null; + } + + @Override + public TypeIntrospector introspectType(String typeName) { + return null; + } + + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return null; + } + + @Override + public String getBeanName() { + return "dummyRepository"; + } + + @Override + public Set<String> getBasePackages() { + return Set.of("org.springframework.data.dummy.repository.aot"); + } + + @Override + public Set<Class<? extends Annotation>> getIdentifyingAnnotations() { + return Set.of(); + } + + @Override + public RepositoryInformation getRepositoryInformation() { + return repositoryInformation; + } + + @Override + public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() { + return Set.of(); + } + + @Override + public Set<Class<?>> getResolvedTypes() { + return Set.of(); + } + + public List<ClassFile> getRequiredContextFiles() { + return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); + } + + static ClassFile classFileForType(Class<?> type) { + + String name = type.getName(); + ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); + + try { + return ClassFile.of(name, cpr.getContentAsByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); + } + } +} diff --git a/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleDefaultRepositoryImplementation.java b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleDefaultRepositoryImplementation.java new file mode 100644 index 0000000000..f666b24e1a --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/generate/DummyModuleDefaultRepositoryImplementation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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.generate; + +import org.springframework.data.repository.CrudRepository; + +/** + * Dummy base class to simulate module specific repository implementation. <br> + * NOTE: needs to be {@literal public} to be referenced in generated sources. + * + * @author Christoph Strobl + */ +public abstract class DummyModuleDefaultRepositoryImplementation<T, ID> implements CrudRepository<T, ID> { + +} diff --git a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryBuilderUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryBuilderUnitTests.java index 426944c700..5bf04cd6e7 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/RepositoryBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryBuilderUnitTests.java @@ -19,228 +19,19 @@ import example.UserRepository; -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.List; -import java.util.Set; - import org.junit.jupiter.api.Test; import org.springframework.aot.test.generate.TestGenerationContext; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.test.tools.ClassFile; import org.springframework.core.test.tools.TestCompiler; -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.config.AotRepositoryContext; -import org.springframework.data.repository.core.CrudMethods; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; -import org.springframework.data.repository.core.support.RepositoryComposition; -import org.springframework.data.repository.core.support.RepositoryFragment; -import org.springframework.data.util.Streamable; -import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl - * @since 2024/09 */ // testclass needs to be public otherwise we cannot reference the repository within -public class RepositoryBuilderUnitTests { +class RepositoryBuilderUnitTests { @Test void compileInstance() { - DummyAotRepoContext aotContext = new DummyAotRepoContext(UserRepository.class, null); - RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { - - @Override - protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { - methodBuilder.customize(((repositoryInformation, metadata, builder) -> { - if (!metadata.returnsVoid()) { - builder.addStatement("return null"); - } - })); - } - }; - - TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); - repositoryContributor.contribute(generationContext); - generationContext.writeGeneratedContent(); - - TestCompiler.forSystem().with(generationContext).withClasses(aotContext.getRequiredContextFiles()) - .compile(compiled -> { - assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains("example.UserRepositoryImpl__Aot"); - }); - } - - public static abstract class SimpleDummyRepository<T, ID> implements CrudRepository<T, ID> {} - - public static class DummyAotRepoContext implements AotRepositoryContext { - - private final StubRepositoryInformation repositoryInformation; - - public DummyAotRepoContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { - this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); - } - - @Override - public ConfigurableListableBeanFactory getBeanFactory() { - return null; - } - - @Override - public TypeIntrospector introspectType(String typeName) { - return null; - } - - @Override - public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { - return null; - } - - @Override - public String getBeanName() { - return "dummyRepository"; - } - - @Override - public Set<String> getBasePackages() { - return Set.of("org.springframework.data.dummy.repository.aot"); - } - - @Override - public Set<Class<? extends Annotation>> getIdentifyingAnnotations() { - return Set.of(); - } - - @Override - public RepositoryInformation getRepositoryInformation() { - return repositoryInformation; - } - - @Override - public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() { - return Set.of(); - } - - @Override - public Set<Class<?>> getResolvedTypes() { - return Set.of(); - } - - public List<ClassFile> getRequiredContextFiles() { - return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); - } - - static ClassFile classFileForType(Class<?> type) { - - String name = type.getName(); - ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); - - try { - return ClassFile.of(name, cpr.getContentAsByteArray()); - } catch (IOException e) { - throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); - } - } - } - - public static class StubRepositoryInformation implements RepositoryInformation { - - private final RepositoryMetadata metadata; - private final RepositoryComposition baseComposition; - - public StubRepositoryInformation(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { - - this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface); - this.baseComposition = composition != null ? composition - : RepositoryComposition.of(RepositoryFragment.structural(SimpleDummyRepository.class)); - } - - @Override - public TypeInformation<?> getIdTypeInformation() { - return metadata.getIdTypeInformation(); - } - - @Override - public TypeInformation<?> getDomainTypeInformation() { - return metadata.getDomainTypeInformation(); - } - - @Override - public Class<?> getRepositoryInterface() { - return metadata.getRepositoryInterface(); - } - - @Override - public TypeInformation<?> getReturnType(Method method) { - return metadata.getReturnType(method); - } - - @Override - public Class<?> getReturnedDomainClass(Method method) { - return metadata.getReturnedDomainClass(method); - } - - @Override - public CrudMethods getCrudMethods() { - return metadata.getCrudMethods(); - } - - @Override - public boolean isPagingRepository() { - return false; - } - - @Override - public Set<Class<?>> getAlternativeDomainTypes() { - return null; - } - - @Override - public boolean isReactiveRepository() { - return false; - } - - @Override - public Set<RepositoryFragment<?>> getFragments() { - return null; - } - - @Override - public boolean isBaseClassMethod(Method method) { - - return baseComposition.findMethod(method).isPresent(); - } - - @Override - public boolean isCustomMethod(Method method) { - return false; - } - - @Override - public boolean isQueryMethod(Method method) { - return false; - } - - @Override - public Streamable<Method> getQueryMethods() { - return null; - } - - @Override - public Class<?> getRepositoryBaseClass() { - return SimpleDummyRepository.class; - } - - @Override - public Method getTargetClassMethod(Method method) { - return null; - } - + // moved to contributor } } 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 new file mode 100644 index 0000000000..bf562a6d8e --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/generate/RepositoryContributorUnitTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 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.generate; + +import static org.assertj.core.api.Assertions.assertThat; + +import example.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.core.test.tools.ResourceFile; +import org.springframework.core.test.tools.ResourceFileAssert; +import org.springframework.core.test.tools.ResourceFiles; +import org.springframework.core.test.tools.TestCompiler; + +/** + * @author Christoph Strobl + */ +class RepositoryContributorUnitTests { + + @Test + void testCompile() { + + DummyModuleAotRepositoryContext aotContext = new DummyModuleAotRepositoryContext(UserRepository.class, null); + RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { + + @Override + protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + methodBuilder.customize(((repositoryInformation, metadata, builder) -> { + if (!metadata.returnsVoid()) { + builder.addStatement("return null"); + } + })); + } + }; + + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + repositoryContributor.contribute(generationContext); + generationContext.writeGeneratedContent(); + + TestCompiler.forSystem().with(generationContext).compile(compiled -> { + assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains("example.UserRepositoryImpl__Aot"); + + ResourceFile springFactories = compiled.getResourceFiles().get("META-INF/spring.factories"); + assertThat(springFactories).isNotNull(); + springFactories.assertThat().contains("example.UserRepository=example.UserRepositoryImpl__Aot"); + }); + + + } + +} diff --git a/src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java b/src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java new file mode 100644 index 0000000000..e526a21279 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/aot/generate/StubRepositoryInformation.java @@ -0,0 +1,127 @@ +/* + * Copyright 2024 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.generate; + +import java.lang.reflect.Method; +import java.util.Set; + +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * Stub {@link RepositoryInformation} used for testing. + * + * @author Christoph Strobl + */ +class StubRepositoryInformation implements RepositoryInformation { + + private final RepositoryMetadata metadata; + private final RepositoryComposition baseComposition; + + public StubRepositoryInformation(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { + + this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface); + this.baseComposition = composition != null ? composition + : RepositoryComposition.of(RepositoryFragment.structural(DummyModuleDefaultRepositoryImplementation.class)); + } + + @Override + public TypeInformation<?> getIdTypeInformation() { + return metadata.getIdTypeInformation(); + } + + @Override + public TypeInformation<?> getDomainTypeInformation() { + return metadata.getDomainTypeInformation(); + } + + @Override + public Class<?> getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + @Override + public TypeInformation<?> getReturnType(Method method) { + return metadata.getReturnType(method); + } + + @Override + public Class<?> getReturnedDomainClass(Method method) { + return metadata.getReturnedDomainClass(method); + } + + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return false; + } + + @Override + public Set<Class<?>> getAlternativeDomainTypes() { + return null; + } + + @Override + public boolean isReactiveRepository() { + return false; + } + + @Override + public Set<RepositoryFragment<?>> getFragments() { + return null; + } + + @Override + public boolean isBaseClassMethod(Method method) { + return baseComposition.findMethod(method).isPresent(); + } + + @Override + public boolean isCustomMethod(Method method) { + return false; + } + + @Override + public boolean isQueryMethod(Method method) { + return false; + } + + @Override + public Streamable<Method> getQueryMethods() { + return null; + } + + @Override + public Class<?> getRepositoryBaseClass() { + return DummyModuleDefaultRepositoryImplementation.class; + } + + @Override + public Method getTargetClassMethod(Method method) { + return null; + } +} From 5a5047ac017da69b07191b4f84309f1f56555639 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Mon, 23 Sep 2024 16:13:47 +0200 Subject: [PATCH 05/14] Add logging and pass and delegate find method to repository composition --- .../aot/generate/AotRepositoryBuilder.java | 31 +++++++++-- .../AotRepositoryConstructorBuilder.java | 2 +- .../aot/generate/RepositoryContributor.java | 14 +++-- .../config/AotRepositoryInformation.java | 9 +++- .../data/aot/CodeContributionAssert.java | 13 ++++- .../RepositoryContributorUnitTests.java | 52 ++++++++++--------- 6 files changed, 83 insertions(+), 38 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 07d2065ce9..71bad0e214 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 @@ -15,6 +15,9 @@ */ package org.springframework.data.repository.aot.generate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.temporal.ChronoField; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; @@ -22,6 +25,8 @@ import javax.lang.model.element.Modifier; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.ClassName; @@ -30,6 +35,7 @@ import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; /** @@ -52,14 +58,28 @@ public static AotRepositoryBuilder forRepository(RepositoryInformation repositor this.repositoryInformation = repositoryInformation; this.generationMetadata = new GenerationMetadata(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.customizer = (info, metadata, builder) -> {}; } public JavaFile javaFile() { + YearMonth creationDate = YearMonth.now(ZoneId.of("UTC")); + // start creating the type - TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) - .addModifiers(Modifier.PUBLIC); + TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) // + .addModifiers(Modifier.PUBLIC) // + .addAnnotation(Component.class) // + .addJavadoc("AOT generated repository implementation for {@link $T}.\n", + repositoryInformation.getRepositoryInterface()) // + .addJavadoc("\n") // + .addJavadoc("@since $L/$L\n", creationDate.get(ChronoField.YEAR), creationDate.get(ChronoField.MONTH_OF_YEAR)) // + .addJavadoc("@author $L", "Spring Data"); // TODO: does System.getProperty("user.name") make sense here? + // TODO: we do not need that here // .addSuperinterface(repositoryInformation.getRepositoryInterface()); @@ -78,7 +98,8 @@ public JavaFile javaFile() { derivedMethodBuilderCustomizer.accept(derivedMethodBuilder); builder.addMethod(derivedMethodBuilder.buildMethod()); }, it -> { - return !repositoryInformation.isBaseClassMethod(it) && !repositoryInformation.isCustomMethod(it) && !it.isDefault(); + return !repositoryInformation.isBaseClassMethod(it) && !repositoryInformation.isCustomMethod(it) + && !it.isDefault(); }); // write fields at the end so we make sure to capture things added by methods @@ -140,8 +161,8 @@ public GenerationMetadata(ClassName className) { public String fieldNameOf(Class<?> type) { TypeName lookup = TypeName.get(type).withoutAnnotations(); - for(Entry<String, FieldSpec> field : fields.entrySet()) { - if(field.getValue().type.withoutAnnotations().equals(lookup)) { + for (Entry<String, FieldSpec> field : fields.entrySet()) { + if (field.getValue().type.withoutAnnotations().equals(lookup)) { return field.getKey(); } } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java index 91bd3fedb4..8f89708088 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java @@ -45,7 +45,7 @@ public AotRepositoryConstructorBuilder(RepositoryInformation repositoryInformati this.repositoryInformation = repositoryInformation; this.metadata = metadata; this.constructorArguments = new LinkedHashMap<>(3); - addParameter("delegate", getDefaultStoreRepositoryImplementationType(repositoryInformation)); + // addParameter("delegate", getDefaultStoreRepositoryImplementationType(repositoryInformation)); } public void addParameter(String parameterName, Class<?> type) { 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 3cdcaeca34..65e9220c05 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 @@ -18,6 +18,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.TypeReference; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.JavaFile; @@ -39,6 +41,7 @@ public RepositoryContributor(AotRepositoryContext repositoryContext) { @Override public void contribute(GenerationContext generationContext) { + //TODO: do we need - generationContext.withName("spring-data"); RepositoryInformation repositoryInformation = repositoryContext.getRepositoryInformation(); AotRepositoryBuilder builder = AotRepositoryBuilder.forRepository(repositoryInformation); @@ -47,21 +50,24 @@ public void contribute(GenerationContext generationContext) { builder.withDerivedMethodCustomizer(this::customizeDerivedMethod); JavaFile file = builder.javaFile(); + String typeName = "%s.%s".formatted(file.packageName, file.typeSpec.name); if (logger.isTraceEnabled()) { logger.trace(""" - ------ AOT Generated Repository: %s.%s ------ + ------ AOT Generated Repository: %s ------ %s ------------------- - """.formatted(file.packageName, file.typeSpec.name, file)); + """.formatted(typeName, file)); } - // generate the file itself generationContext.getGeneratedFiles().addSourceFile(file); + // generate native runtime hints - needed cause we're using the repository proxy + generationContext.getRuntimeHints().reflection().registerType(TypeReference.of(typeName), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); + // register it in spring.factories - String registration = "%s=%s.%s".formatted(repositoryInformation.getRepositoryInterface().getName(), file.packageName, file.typeSpec.name); + String registration = "%s=%s".formatted(repositoryInformation.getRepositoryInterface().getName(), typeName); generationContext.getGeneratedFiles().addResourceFile("META-INF/spring.factories", registration); } diff --git a/src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java b/src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java index b2faf62e4c..ee0ab7cfb2 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryInformation.java @@ -24,7 +24,9 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformationSupport; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Lazy; /** * {@link RepositoryInformation} based on {@link RepositoryMetadata} collected at build time. @@ -35,6 +37,9 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements RepositoryInformation { private final Supplier<Collection<RepositoryFragment<?>>> fragments; + private Lazy<RepositoryComposition> baseComposition = Lazy.of(() -> { + return RepositoryComposition.of(RepositoryFragment.structural(getRepositoryBaseClass())); + }); AotRepositoryInformation(Supplier<RepositoryMetadata> repositoryMetadata, Supplier<Class<?>> repositoryBaseClass, Supplier<Collection<RepositoryFragment<?>>> fragments) { @@ -60,12 +65,12 @@ public boolean isCustomMethod(Method method) { @Override public boolean isBaseClassMethod(Method method) { - return false; + return baseComposition.get().findMethod(method).isPresent(); } @Override public Method getTargetClassMethod(Method method) { - return method; + return baseComposition.get().findMethod(method).orElse(method); } } diff --git a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java index a7f7cd4a34..1bf8817bb8 100644 --- a/src/test/java/org/springframework/data/aot/CodeContributionAssert.java +++ b/src/test/java/org/springframework/data/aot/CodeContributionAssert.java @@ -15,7 +15,7 @@ */ package org.springframework.data.aot; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Method; import java.util.Arrays; @@ -24,6 +24,7 @@ import org.assertj.core.api.AbstractAssert; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.JdkProxyHint; +import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; /** @@ -51,6 +52,16 @@ public CodeContributionAssert contributesReflectionFor(Class<?>... types) { return this; } + public CodeContributionAssert contributesReflectionFor(String... types) { + + for (String type : types) { + assertThat(this.actual.getRuntimeHints()).describedAs("No reflection entry found for [%s]", type) + .matches(RuntimeHintsPredicates.reflection().onType(TypeReference.of(type))); + } + + return this; + } + public CodeContributionAssert contributesReflectionFor(Method... methods) { for (Method method : methods) { 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 bf562a6d8e..db00b9cd21 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 @@ -18,47 +18,49 @@ import static org.assertj.core.api.Assertions.assertThat; import example.UserRepository; + import org.junit.jupiter.api.Test; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.core.test.tools.ResourceFile; -import org.springframework.core.test.tools.ResourceFileAssert; -import org.springframework.core.test.tools.ResourceFiles; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.aot.CodeContributionAssert; /** * @author Christoph Strobl */ class RepositoryContributorUnitTests { - @Test - void testCompile() { + @Test + void testCompile() { - DummyModuleAotRepositoryContext aotContext = new DummyModuleAotRepositoryContext(UserRepository.class, null); - RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { + DummyModuleAotRepositoryContext aotContext = new DummyModuleAotRepositoryContext(UserRepository.class, null); + RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { - @Override - protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { - methodBuilder.customize(((repositoryInformation, metadata, builder) -> { - if (!metadata.returnsVoid()) { - builder.addStatement("return null"); - } - })); - } - }; + @Override + protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + methodBuilder.customize(((repositoryInformation, metadata, builder) -> { + if (!metadata.returnsVoid()) { + builder.addStatement("return null"); + } + })); + } + }; - TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); - repositoryContributor.contribute(generationContext); - generationContext.writeGeneratedContent(); + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + repositoryContributor.contribute(generationContext); + generationContext.writeGeneratedContent(); - TestCompiler.forSystem().with(generationContext).compile(compiled -> { - assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains("example.UserRepositoryImpl__Aot"); + String expectedTypeName = "example.UserRepositoryImpl__Aot"; - ResourceFile springFactories = compiled.getResourceFiles().get("META-INF/spring.factories"); - assertThat(springFactories).isNotNull(); - springFactories.assertThat().contains("example.UserRepository=example.UserRepositoryImpl__Aot"); - }); + TestCompiler.forSystem().with(generationContext).compile(compiled -> { + assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains(expectedTypeName); + ResourceFile springFactories = compiled.getResourceFiles().get("META-INF/spring.factories"); + assertThat(springFactories).isNotNull(); + springFactories.assertThat().contains("example.UserRepository=%s".formatted(expectedTypeName)); + }); - } + new CodeContributionAssert(generationContext).contributesReflectionFor(expectedTypeName); + } } From 23758ca1000c5350f6bfa55ec5ef346488a89739 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Tue, 24 Sep 2024 10:17:11 +0200 Subject: [PATCH 06/14] move flag lookup --- src/main/java/org/springframework/data/aot/AotContext.java | 7 +++++++ .../data/repository/config/AotRepositoryContext.java | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/springframework/data/aot/AotContext.java b/src/main/java/org/springframework/data/aot/AotContext.java index f6df3d1dc3..5d8073edd2 100644 --- a/src/main/java/org/springframework/data/aot/AotContext.java +++ b/src/main/java/org/springframework/data/aot/AotContext.java @@ -28,6 +28,7 @@ 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.SpringProperties; import org.springframework.data.util.TypeScanner; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -48,6 +49,12 @@ */ public interface AotContext { + String GENERATED_REPOSITORIES_ENABLED = "spring.aot.repositories.enabled"; + + static boolean aotGeneratedRepositoriesEnabled() { + return SpringProperties.getFlag(GENERATED_REPOSITORIES_ENABLED); + } + /** * Create an {@link AotContext} backed by the given {@link BeanFactory}. * 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 e38c1e5a58..995aa04084 100644 --- a/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java +++ b/src/main/java/org/springframework/data/repository/config/AotRepositoryContext.java @@ -33,8 +33,6 @@ */ public interface AotRepositoryContext extends AotContext { - String GENERATED_REPOSITORIES_ENABLED = "spring.aot.repositories.enabled"; - /** * @return the {@link String bean name} of the repository / factory bean. */ @@ -66,8 +64,4 @@ public interface AotRepositoryContext extends AotContext { * @return all {@link Class types} reachable from the repository. */ Set<Class<?>> getResolvedTypes(); - - default boolean aotGeneratedRepositoriesEnabled() { - return SpringProperties.getFlag(GENERATED_REPOSITORIES_ENABLED); - } } From 0b67b0fe97004553e66816585386346d99a3edd6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Tue, 24 Sep 2024 14:20:34 +0200 Subject: [PATCH 07/14] fixme: fragment method lookuop --- .../support/DefaultRepositoryInformation.java | 23 +++++++++++++++++++ ...DefaultRepositoryInformationUnitTests.java | 16 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java index f033a2023b..111a4c4177 100644 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java @@ -18,6 +18,7 @@ import static org.springframework.util.ReflectionUtils.*; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -25,7 +26,10 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryInformationSupport; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * Default implementation of {@link RepositoryInformation}. @@ -100,6 +104,25 @@ public boolean isBaseClassMethod(Method method) { return baseComposition.getMethod(method) != null; } + + protected boolean isQueryMethodCandidate(Method method) { + + // FIXME - that should be simplified + boolean queryMethodCandidate = super.isQueryMethodCandidate(method); + if(!isQueryAnnotationPresentOn(method)) { + return queryMethodCandidate; + } + + return queryMethodCandidate && !getFragments().stream().anyMatch(fragment -> { + if(fragment.getImplementation().isPresent()) { + if(ClassUtils.hasMethod(fragment.getImplementation().get().getClass(), method.getName(), method.getParameterTypes())) { + return true; + } + } + return false; + }); + } + @Override public Set<RepositoryFragment<?>> getFragments() { return composition.getFragments().toSet(); diff --git a/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java index 7ec9a2deda..e91ce12301 100755 --- a/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java @@ -198,6 +198,16 @@ void getQueryShouldNotReturnAnyBridgeMethods() { assertThat(information.getQueryMethods()).allMatch(method -> !method.isBridge()); } + @Test // GH-??? + void annotatedQueryMethodWithFragmentImplementationIsNotConsideredForQueryMethods() { + + RepositoryMetadata metadata = new DefaultRepositoryMetadata(CustomDefaultRepositoryMethodsRepository.class); + RepositoryInformation information = new DefaultRepositoryInformation(metadata, CrudRepository.class, + RepositoryComposition.of(RepositoryFragment.implemented(new FragmentThatImplementsFinderWithQueryAnnotation()))); + + assertThat(information.getQueryMethods()).allMatch(it -> !it.getName().equals("findAll")); + } + @Test // DATACMNS-854 void discoversCustomlyImplementedCrudMethodWithGenerics() throws SecurityException, NoSuchMethodException { @@ -377,6 +387,12 @@ interface CustomDefaultRepositoryMethodsRepository extends CrudRepository<User, List<User> findAll(); } + static class FragmentThatImplementsFinderWithQueryAnnotation { + public List<User> findAll() { + return null; + } + } + // DATACMNS-854, DATACMNS-912 interface GenericsSaveRepository extends CrudRepository<Sample, Long> {} From f3e4fb7a7552bba7b43ccdd54cd830ebe805f038 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Tue, 24 Sep 2024 14:38:45 +0200 Subject: [PATCH 08/14] keep not where we could plug some bean registration code --- .../repository/config/RepositoryRegistrationAotProcessor.java | 1 + 1 file changed, 1 insertion(+) 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 42e57e7d26..b61d08ab8d 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryRegistrationAotProcessor.java @@ -126,6 +126,7 @@ protected RepositoryRegistrationAotContribution newRepositoryRegistrationAotCont .forBean(repositoryBean); BiConsumer<AotRepositoryContext, GenerationContext> moduleContribution = this::registerReflectiveForAggregateRoot; + //TODO: add the hook for customizing bean initialization code here! return contribution.withModuleContribution(moduleContribution.andThen(this::contribute)); } From ff735e6f1bdc1d2cb4ed78e48b7e67296422d61c Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Thu, 9 Jan 2025 13:21:00 +0100 Subject: [PATCH 09/14] Add actual return type to metadata to get the User out of the List<User> for example --- .../aot/generate/AotRepositoryBuilder.java | 11 +++++++++++ .../generate/AotRepositoryDerivedMethodBuilder.java | 12 ++++++++++-- .../aot/generate/AotRepositoryMethodBuilder.java | 9 ++++++++- 3 files changed, 29 insertions(+), 3 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 71bad0e214..f7172d63f9 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 @@ -28,6 +28,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.FieldSpec; @@ -98,6 +99,16 @@ public JavaFile javaFile() { derivedMethodBuilderCustomizer.accept(derivedMethodBuilder); builder.addMethod(derivedMethodBuilder.buildMethod()); }, it -> { + + /* + the isBaseClassMethod(it) check seems to have some issues. + need to hard code it here + */ + + if(ReflectionUtils.findMethod(CrudRepository.class, it.getName(), it.getParameterTypes()) != null) { + return false; + } + return !repositoryInformation.isBaseClassMethod(it) && !repositoryInformation.isCustomMethod(it) && !it.isDefault(); }); diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java index 187b931a08..5cb2bbd19d 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java @@ -65,10 +65,18 @@ private void initReturnType(Method method, RepositoryInformation repositoryInfor repositoryInformation.getRepositoryInterface()); TypeName returnTypeName = TypeName.get(returnType.resolve()); + TypeName actualReturnTypeName = null; if (returnType.hasGenerics()) { - returnTypeName = ParameterizedTypeName.get(returnType.resolve(), returnType.resolveGenerics()); + Class<?>[] generics = returnType.resolveGenerics(); + returnTypeName = ParameterizedTypeName.get(returnType.resolve(), generics); + + if(generics.length == 1) { + actualReturnTypeName = TypeName.get(generics[0]); + } } - setReturnType(returnTypeName); + setReturnType(returnTypeName, actualReturnTypeName); } + + } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java index 90ef94bebc..ab46ee6d92 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java @@ -73,8 +73,9 @@ public void addParameter(ParameterSpec parameter) { this.metadata.methodArguments.put(parameter.name, parameter); } - public void setReturnType(@Nullable TypeName returnType) { + public void setReturnType(@Nullable TypeName returnType, @Nullable TypeName actualReturnType) { this.metadata.returnType = returnType; + this.metadata.actualReturnType = actualReturnType; } public void customize(RepositoryMethodCustomizer customizer) { @@ -106,6 +107,7 @@ public static class MethodGenerationMetadata { private final AotRepositoryBuilder.GenerationMetadata generationMetadata; private final Method repositoryMethod; private final Map<String, ParameterSpec> methodArguments; + @Nullable public TypeName actualReturnType; @Nullable private TypeName returnType; public MethodGenerationMetadata(AotRepositoryBuilder.GenerationMetadata generationMetadata, @@ -138,6 +140,11 @@ public TypeName getReturnType() { return returnType; } + @Nullable + public TypeName getActualReturnType() { + return actualReturnType; + } + @Nullable public String getSortParameterName() { return getParameterNameOf(Sort.class); From 11fba5e85257e0115dcce620e55b8851fb56a705 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Mon, 13 Jan 2025 12:00:32 +0100 Subject: [PATCH 10/14] Modules should be able to report back if code could be generated for certain method --- .../aot/generate/AotRepositoryBuilder.java | 11 ++-- .../generate/AotRepositoryMethodBuilder.java | 6 ++ .../repository/aot/generate/CodeBlocks.java | 64 +++++++++++++++++++ .../repository/aot/generate/Contribution.java | 40 ++++++++++++ .../aot/generate/RepositoryContributor.java | 4 +- .../RepositoryContributorUnitTests.java | 3 +- 6 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/Contribution.java 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 f7172d63f9..5899d000e1 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 @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.function.Consumer; +import java.util.function.Function; import javax.lang.model.element.Modifier; @@ -48,7 +49,7 @@ public class AotRepositoryBuilder { private final GenerationMetadata generationMetadata; private Consumer<AotRepositoryConstructorBuilder> constructorBuilderCustomizer; - private Consumer<AotRepositoryMethodBuilder> derivedMethodBuilderCustomizer; + private Function<AotRepositoryMethodBuilder, Contribution> derivedMethodBuilderCustomizer; private RepositoryCustomizer customizer; public static AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation) { @@ -96,8 +97,10 @@ public JavaFile javaFile() { AotRepositoryDerivedMethodBuilder derivedMethodBuilder = new AotRepositoryDerivedMethodBuilder(method, repositoryInformation, generationMetadata); - derivedMethodBuilderCustomizer.accept(derivedMethodBuilder); - builder.addMethod(derivedMethodBuilder.buildMethod()); + switch (derivedMethodBuilderCustomizer.apply(derivedMethodBuilder)) { + case CODE -> builder.addMethod(derivedMethodBuilder.buildMethod()); + // todo other cases, not sure which ones right now + } }, it -> { /* @@ -127,7 +130,7 @@ AotRepositoryBuilder withConstructorCustomizer(Consumer<AotRepositoryConstructor return this; } - AotRepositoryBuilder withDerivedMethodCustomizer(Consumer<AotRepositoryMethodBuilder> methodBuilder) { + AotRepositoryBuilder withDerivedMethodCustomizer(Function<AotRepositoryMethodBuilder, Contribution> methodBuilder) { this.derivedMethodBuilderCustomizer = methodBuilder; return this; } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java index ab46ee6d92..edcfaa25f1 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java @@ -46,6 +46,7 @@ public class AotRepositoryMethodBuilder { private final MethodGenerationMetadata metadata; private RepositoryMethodCustomizer customizer = (info, md, builder) -> {}; + private CodeBlocks codeBlocks; public AotRepositoryMethodBuilder(Method method, RepositoryInformation repositoryInformation, AotRepositoryBuilder.GenerationMetadata metadata) { @@ -53,6 +54,7 @@ public AotRepositoryMethodBuilder(Method method, RepositoryInformation repositor this.method = method; this.repositoryInformation = repositoryInformation; this.metadata = new MethodGenerationMetadata(metadata, method); + this.codeBlocks = new CodeBlocks(metadata); } public void addParameter(String parameterName, Class<?> type) { @@ -96,6 +98,10 @@ MethodSpec buildMethod() { return builder.build(); } + public CodeBlocks codeBlocks() { + return codeBlocks; + } + public interface RepositoryMethodCustomizer { void customize(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, diff --git a/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java b/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java new file mode 100644 index 0000000000..680cf7c2db --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java @@ -0,0 +1,64 @@ +/* + * 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. + */ + +/* + * 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.repository.aot.generate; + +import org.apache.commons.logging.Log; +import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.GenerationMetadata; +import org.springframework.javapoet.CodeBlock; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class CodeBlocks { + + private final GenerationMetadata metadata; + + public CodeBlocks(GenerationMetadata metadata) { + this.metadata = metadata; + } + + private CodeBlock log(String level, String message) { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.beginControlFlow("if($L.is$LEnabled())", metadata.fieldNameOf(Log.class), StringUtils.capitalize(level)); + builder.addStatement("$L.$L($S)", metadata.fieldNameOf(Log.class), level, message); + builder.endControlFlow(); + return builder.build(); + } + + public CodeBlock logDebug(String message) { + return log("debug", message); + } + +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/Contribution.java b/src/main/java/org/springframework/data/repository/aot/generate/Contribution.java new file mode 100644 index 0000000000..acf39b525d --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/Contribution.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + +/* + * 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.repository.aot.generate; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public enum Contribution { + CODE, SKIP +} 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 65e9220c05..0cd80f119e 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 @@ -83,7 +83,7 @@ protected void customizeFile(RepositoryInformation information, AotRepositoryBui } - protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { - + protected Contribution customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + return Contribution.SKIP; } } 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 db00b9cd21..6889e5ed97 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 @@ -37,12 +37,13 @@ void testCompile() { RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { @Override - protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + protected Contribution customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { methodBuilder.customize(((repositoryInformation, metadata, builder) -> { if (!metadata.returnsVoid()) { builder.addStatement("return null"); } })); + return Contribution.CODE; } }; From 0fa4ed17ffdf420a79d161a1e2abdb237428bc6c Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Mon, 13 Jan 2025 15:36:22 +0100 Subject: [PATCH 11/14] add some return type checks for generating page and slice code stuff --- .../AotRepositoryDerivedMethodBuilder.java | 2 + .../generate/AotRepositoryMethodBuilder.java | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java index 5cb2bbd19d..e08b60beb8 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java @@ -55,7 +55,9 @@ private void initParameters(Method method, RepositoryInformation repositoryInfor resolvableParameterType.resolveGenerics()); } addParameter(parameter.getName(), parameterType); + index++; } + } } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java index edcfaa25f1..dd28b16ae5 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java @@ -15,25 +15,37 @@ */ package org.springframework.data.repository.aot.generate; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.stream.Collectors; import javax.lang.model.element.Modifier; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterSpec; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -141,6 +153,26 @@ public boolean returnsVoid() { return repositoryMethod.getReturnType().equals(Void.TYPE); } + public boolean returnsPage() { + return ClassUtils.isAssignable(Page.class, repositoryMethod.getReturnType()); + } + + public boolean returnsSlice() { + return ClassUtils.isAssignable(Slice.class, repositoryMethod.getReturnType()); + } + + public boolean returnsCollection() { + return ClassUtils.isAssignable(Collection.class, repositoryMethod.getReturnType()); + } + + public boolean returnsSingleValue() { + return !returnsPage() && !returnsSlice() && !returnsCollection(); + } + + public boolean returnsOptionalValue() { + return ClassUtils.isAssignable(Optional.class, repositoryMethod.getReturnType()); + } + @Nullable public TypeName getReturnType() { return returnType; @@ -190,5 +222,11 @@ public void addField(FieldSpec fieldSpec) { public Map<String, FieldSpec> getFields() { return generationMetadata.getFields(); } + + @Nullable + public <T> T annotationValue(Class<? extends Annotation> annotation, String attribute) { + AnnotationAttributes values = AnnotatedElementUtils.getMergedAnnotationAttributes(this.repositoryMethod, annotation); + return values != null ? (T) values.get(attribute) : null; + } } } From 8c76b40985954ddad36804738598b3c6a760c90b Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Tue, 14 Jan 2025 12:27:22 +0100 Subject: [PATCH 12/14] Reduce number of callbacks and merge context with metadata --- .../aot/generate/AotRepositoryBuilder.java | 30 +-- .../AotRepositoryConstructorBuilder.java | 5 +- .../AotRepositoryDerivedMethodBuilder.java | 84 -------- .../generate/AotRepositoryMethodBuilder.java | 184 ++++++------------ .../AotRepositoryMethodGenerationContext.java | 178 +++++++++++++++++ .../repository/aot/generate/CodeBlocks.java | 52 ++--- .../aot/generate/RepositoryContributor.java | 9 +- .../RepositoryContributorUnitTests.java | 9 +- 8 files changed, 297 insertions(+), 254 deletions(-) delete mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java 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 5899d000e1..95d86e5bcc 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 @@ -46,10 +46,10 @@ public class AotRepositoryBuilder { private final RepositoryInformation repositoryInformation; - private final GenerationMetadata generationMetadata; + private final TargetAotRepositoryImplementationMetadata generationMetadata; private Consumer<AotRepositoryConstructorBuilder> constructorBuilderCustomizer; - private Function<AotRepositoryMethodBuilder, Contribution> derivedMethodBuilderCustomizer; + private Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction; private RepositoryCustomizer customizer; public static AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation) { @@ -59,7 +59,7 @@ public static AotRepositoryBuilder forRepository(RepositoryInformation repositor AotRepositoryBuilder(RepositoryInformation repositoryInformation) { this.repositoryInformation = repositoryInformation; - this.generationMetadata = new GenerationMetadata(className()); + this.generationMetadata = new TargetAotRepositoryImplementationMetadata(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()) @@ -95,12 +95,14 @@ public JavaFile javaFile() { // start with the derived ones ReflectionUtils.doWithMethods(repositoryInformation.getRepositoryInterface(), method -> { - AotRepositoryDerivedMethodBuilder derivedMethodBuilder = new AotRepositoryDerivedMethodBuilder(method, - repositoryInformation, generationMetadata); - switch (derivedMethodBuilderCustomizer.apply(derivedMethodBuilder)) { - case CODE -> builder.addMethod(derivedMethodBuilder.buildMethod()); - // todo other cases, not sure which ones right now +// AotRepositoryDerivedMethodBuilder derivedMethodBuilder = new AotRepositoryDerivedMethodBuilder(method, +// repositoryInformation, generationMetadata); + AotRepositoryMethodGenerationContext context = new AotRepositoryMethodGenerationContext(method, repositoryInformation, generationMetadata); + AotRepositoryMethodBuilder methodBuilder = methodContextFunction.apply(context); + if(methodBuilder != null) { + builder.addMethod(methodBuilder.buildMethod()); } + }, it -> { /* @@ -130,8 +132,8 @@ AotRepositoryBuilder withConstructorCustomizer(Consumer<AotRepositoryConstructor return this; } - AotRepositoryBuilder withDerivedMethodCustomizer(Function<AotRepositoryMethodBuilder, Contribution> methodBuilder) { - this.derivedMethodBuilderCustomizer = methodBuilder; + AotRepositoryBuilder withDerivedMethodFunction(Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction) { + this.methodContextFunction = methodContextFunction; return this; } @@ -141,7 +143,7 @@ AotRepositoryBuilder withFileCustomizer(RepositoryCustomizer repositoryCustomize return this; } - GenerationMetadata getGenerationMetadata() { + TargetAotRepositoryImplementationMetadata getGenerationMetadata() { return generationMetadata; } @@ -159,15 +161,15 @@ private String typeName() { public interface RepositoryCustomizer { - void customize(RepositoryInformation repositoryInformation, GenerationMetadata metadata, TypeSpec.Builder builder); + void customize(RepositoryInformation repositoryInformation, TargetAotRepositoryImplementationMetadata metadata, TypeSpec.Builder builder); } - public class GenerationMetadata { + public class TargetAotRepositoryImplementationMetadata { private ClassName className; private Map<String, FieldSpec> fields = new HashMap<>(); - public GenerationMetadata(ClassName className) { + public TargetAotRepositoryImplementationMetadata(ClassName className) { this.className = className; } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java index 8f89708088..2b929b6e35 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java @@ -23,6 +23,7 @@ import javax.lang.model.element.Modifier; import org.springframework.core.ResolvableType; +import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.TargetAotRepositoryImplementationMetadata; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; @@ -34,13 +35,13 @@ public class AotRepositoryConstructorBuilder { private final RepositoryInformation repositoryInformation; - private final AotRepositoryBuilder.GenerationMetadata metadata; + private final TargetAotRepositoryImplementationMetadata metadata; private final Map<String, TypeName> constructorArguments; private ConstructorCustomizer customizer = (info, builder) -> {}; public AotRepositoryConstructorBuilder(RepositoryInformation repositoryInformation, - AotRepositoryBuilder.GenerationMetadata metadata) { + TargetAotRepositoryImplementationMetadata metadata) { this.repositoryInformation = repositoryInformation; this.metadata = metadata; diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java deleted file mode 100644 index e08b60beb8..0000000000 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryDerivedMethodBuilder.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2024 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.generate; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; - -import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; -import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.GenerationMetadata; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.javapoet.ParameterizedTypeName; -import org.springframework.javapoet.TypeName; - -/** - * @author Christoph Strobl - */ -public class AotRepositoryDerivedMethodBuilder extends AotRepositoryMethodBuilder { - - public AotRepositoryDerivedMethodBuilder(Method method, RepositoryInformation repositoryInformation, - GenerationMetadata metadata) { - - super(method, repositoryInformation, metadata); - - initReturnType(method, repositoryInformation); - initParameters(method, repositoryInformation); - } - - private void initParameters(Method method, RepositoryInformation repositoryInformation) { - - ResolvableType repositoryInterface = ResolvableType.forClass(repositoryInformation.getRepositoryInterface()); - if (method.getParameterCount() > 0) { - int index = 0; - for (Parameter parameter : method.getParameters()) { - - ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(new MethodParameter(method, index), - repositoryInterface); - - TypeName parameterType = TypeName.get(resolvableParameterType.resolve()); - if (resolvableParameterType.hasGenerics()) { - parameterType = ParameterizedTypeName.get(resolvableParameterType.resolve(), - resolvableParameterType.resolveGenerics()); - } - addParameter(parameter.getName(), parameterType); - index++; - } - - } - } - - private void initReturnType(Method method, RepositoryInformation repositoryInformation) { - - ResolvableType returnType = ResolvableType.forMethodReturnType(method, - repositoryInformation.getRepositoryInterface()); - - TypeName returnTypeName = TypeName.get(returnType.resolve()); - TypeName actualReturnTypeName = null; - if (returnType.hasGenerics()) { - Class<?>[] generics = returnType.resolveGenerics(); - returnTypeName = ParameterizedTypeName.get(returnType.resolve(), generics); - - if(generics.length == 1) { - actualReturnTypeName = TypeName.get(generics[0]); - } - } - - setReturnType(returnTypeName, actualReturnTypeName); - } - - -} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java index dd28b16ae5..0f313b7c5a 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java @@ -15,37 +15,23 @@ */ package org.springframework.data.repository.aot.generate; -import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.Collection; +import java.lang.reflect.Parameter; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; -import java.util.Optional; import java.util.stream.Collectors; import javax.lang.model.element.Modifier; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.data.domain.Limit; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort; import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.ParametersSource; -import org.springframework.data.repository.query.ReturnedType; -import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterSpec; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; -import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -53,20 +39,15 @@ */ public class AotRepositoryMethodBuilder { - private final Method method; - private final RepositoryInformation repositoryInformation; - private final MethodGenerationMetadata metadata; + private final AotRepositoryMethodGenerationContext context; - private RepositoryMethodCustomizer customizer = (info, md, builder) -> {}; - private CodeBlocks codeBlocks; + private RepositoryMethodCustomizer customizer = (context, body) -> {}; - public AotRepositoryMethodBuilder(Method method, RepositoryInformation repositoryInformation, - AotRepositoryBuilder.GenerationMetadata metadata) { + public AotRepositoryMethodBuilder(AotRepositoryMethodGenerationContext context) { - this.method = method; - this.repositoryInformation = repositoryInformation; - this.metadata = new MethodGenerationMetadata(metadata, method); - this.codeBlocks = new CodeBlocks(metadata); + this.context = context; + initReturnType(context.getMethod(), context.getRepositoryInformation()); + initParameters(context.getMethod(), context.getRepositoryInformation()); } public void addParameter(String parameterName, Class<?> type) { @@ -84,61 +65,89 @@ public void addParameter(String parameterName, TypeName type) { } public void addParameter(ParameterSpec parameter) { - this.metadata.methodArguments.put(parameter.name, parameter); + this.context.addParameter(parameter); } public void setReturnType(@Nullable TypeName returnType, @Nullable TypeName actualReturnType) { - this.metadata.returnType = returnType; - this.metadata.actualReturnType = actualReturnType; + this.context.getTargetMethodMetadata().returnType = returnType; + this.context.getTargetMethodMetadata().actualReturnType = actualReturnType; } - public void customize(RepositoryMethodCustomizer customizer) { + public AotRepositoryMethodBuilder customize(RepositoryMethodCustomizer customizer) { this.customizer = customizer; + return this; } MethodSpec buildMethod() { - MethodSpec.Builder builder = MethodSpec.methodBuilder(method.getName()).addModifiers(Modifier.PUBLIC); - if (!metadata.returnsVoid()) { - builder.returns(metadata.getReturnType()); + MethodSpec.Builder builder = MethodSpec.methodBuilder(context.getMethod().getName()).addModifiers(Modifier.PUBLIC); + if (!context.returnsVoid()) { + builder.returns(context.getReturnType()); } - builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", method.getDeclaringClass(), - method.getName(), StringUtils.collectionToCommaDelimitedString( - metadata.methodArguments.values().stream().map(it -> it.type.toString()).collect(Collectors.toList()))); - metadata.methodArguments.forEach((name, spec) -> builder.addParameter(spec)); - customizer.customize(repositoryInformation, metadata, builder); + builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", context.getMethod().getDeclaringClass(), + context.getMethod().getName(), + StringUtils.collectionToCommaDelimitedString(context.getTargetMethodMetadata().methodArguments.values().stream() + .map(it -> it.type.toString()).collect(Collectors.toList()))); + context.getTargetMethodMetadata().methodArguments.forEach((name, spec) -> builder.addParameter(spec)); + customizer.customize(context, builder); return builder.build(); } - public CodeBlocks codeBlocks() { - return codeBlocks; + private void initParameters(Method method, RepositoryInformation repositoryInformation) { + + ResolvableType repositoryInterface = ResolvableType.forClass(repositoryInformation.getRepositoryInterface()); + if (method.getParameterCount() > 0) { + int index = 0; + for (Parameter parameter : method.getParameters()) { + + ResolvableType resolvableParameterType = ResolvableType.forMethodParameter(new MethodParameter(method, index), + repositoryInterface); + + TypeName parameterType = TypeName.get(resolvableParameterType.resolve()); + if (resolvableParameterType.hasGenerics()) { + parameterType = ParameterizedTypeName.get(resolvableParameterType.resolve(), + resolvableParameterType.resolveGenerics()); + } + addParameter(parameter.getName(), parameterType); + index++; + } + + } } - public interface RepositoryMethodCustomizer { + private void initReturnType(Method method, RepositoryInformation repositoryInformation) { + + ResolvableType returnType = ResolvableType.forMethodReturnType(method, + repositoryInformation.getRepositoryInterface()); + + TypeName returnTypeName = TypeName.get(returnType.resolve()); + TypeName actualReturnTypeName = null; + if (returnType.hasGenerics()) { + Class<?>[] generics = returnType.resolveGenerics(); + returnTypeName = ParameterizedTypeName.get(returnType.resolve(), generics); + + if (generics.length == 1) { + actualReturnTypeName = TypeName.get(generics[0]); + } + } - void customize(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, - MethodSpec.Builder builder); + setReturnType(returnTypeName, actualReturnTypeName); } - public static class MethodGenerationMetadata { + public interface RepositoryMethodCustomizer { + void customize(AotRepositoryMethodGenerationContext context, MethodSpec.Builder builder); + } + + public static class TargetAotRepositoryMethodImplementationMetadata { - private final AotRepositoryBuilder.GenerationMetadata generationMetadata; - private final Method repositoryMethod; private final Map<String, ParameterSpec> methodArguments; @Nullable public TypeName actualReturnType; @Nullable private TypeName returnType; - public MethodGenerationMetadata(AotRepositoryBuilder.GenerationMetadata generationMetadata, - Method repositoryMethod) { - this.generationMetadata = generationMetadata; - this.repositoryMethod = repositoryMethod; + public TargetAotRepositoryMethodImplementationMetadata() { this.methodArguments = new LinkedHashMap<>(); } - public Method getRepositoryMethod() { - return repositoryMethod; - } - @Nullable public String getParameterNameOf(Class<?> type) { for (Entry<String, ParameterSpec> entry : methodArguments.entrySet()) { @@ -149,30 +158,6 @@ public String getParameterNameOf(Class<?> type) { return null; } - public boolean returnsVoid() { - return repositoryMethod.getReturnType().equals(Void.TYPE); - } - - public boolean returnsPage() { - return ClassUtils.isAssignable(Page.class, repositoryMethod.getReturnType()); - } - - public boolean returnsSlice() { - return ClassUtils.isAssignable(Slice.class, repositoryMethod.getReturnType()); - } - - public boolean returnsCollection() { - return ClassUtils.isAssignable(Collection.class, repositoryMethod.getReturnType()); - } - - public boolean returnsSingleValue() { - return !returnsPage() && !returnsSlice() && !returnsCollection(); - } - - public boolean returnsOptionalValue() { - return ClassUtils.isAssignable(Optional.class, repositoryMethod.getReturnType()); - } - @Nullable public TypeName getReturnType() { return returnType; @@ -183,50 +168,9 @@ public TypeName getActualReturnType() { return actualReturnType; } - @Nullable - public String getSortParameterName() { - return getParameterNameOf(Sort.class); - } - - @Nullable - public String getPageableParameterName() { - return getParameterNameOf(Pageable.class); - } - - @Nullable - public String getLimitParameterName() { - return getParameterNameOf(Limit.class); - } - public void addParameter(ParameterSpec parameterSpec) { this.methodArguments.put(parameterSpec.name, parameterSpec); } - @Nullable - public String fieldNameOf(Class<?> type) { - return generationMetadata.fieldNameOf(type); - } - - public boolean hasField(String fieldName) { - return generationMetadata.hasField(fieldName); - } - - public void addField(String fieldName, TypeName type, Modifier... modifiers) { - generationMetadata.addField(fieldName, type, modifiers); - } - - public void addField(FieldSpec fieldSpec) { - generationMetadata.addField(fieldSpec); - } - - public Map<String, FieldSpec> getFields() { - return generationMetadata.getFields(); - } - - @Nullable - public <T> T annotationValue(Class<? extends Annotation> annotation, String attribute) { - AnnotationAttributes values = AnnotatedElementUtils.getMergedAnnotationAttributes(this.repositoryMethod, annotation); - return values != null ? (T) values.get(attribute) : null; - } } } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java new file mode 100644 index 0000000000..546a377708 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java @@ -0,0 +1,178 @@ +/* + * 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. + */ + +/* + * 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.repository.aot.generate; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Optional; + +import javax.lang.model.element.Modifier; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.TargetAotRepositoryImplementationMetadata; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.TargetAotRepositoryMethodImplementationMetadata; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.javapoet.FieldSpec; +import org.springframework.javapoet.ParameterSpec; +import org.springframework.javapoet.TypeName; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class AotRepositoryMethodGenerationContext { + + private final Method method; + private final RepositoryInformation repositoryInformation; + private final TargetAotRepositoryImplementationMetadata targetTypeMetadata; + private final TargetAotRepositoryMethodImplementationMetadata targetMethodMetadata; + private final CodeBlocks codeBlocks; + + public AotRepositoryMethodGenerationContext(Method method, RepositoryInformation repositoryInformation, + TargetAotRepositoryImplementationMetadata targetTypeMetadata) { + + this.method = method; + this.repositoryInformation = repositoryInformation; + this.targetTypeMetadata = targetTypeMetadata; + this.targetMethodMetadata = new TargetAotRepositoryMethodImplementationMetadata(); + this.codeBlocks = new CodeBlocks(targetTypeMetadata); + } + + public boolean hasField(String fieldName) { + return targetTypeMetadata.hasField(fieldName); + } + + public void addField(String fieldName, TypeName type, Modifier... modifiers) { + targetTypeMetadata.addField(fieldName, type, modifiers); + } + + public void addField(FieldSpec fieldSpec) { + targetTypeMetadata.addField(fieldSpec); + } + + public String fieldNameOf(Class<?> type) { + return targetTypeMetadata.fieldNameOf(type); + } + + public RepositoryInformation getRepositoryInformation() { + return repositoryInformation; + } + + public Method getMethod() { + return method; + } + + TargetAotRepositoryImplementationMetadata getTargetTypeMetadata() { + return targetTypeMetadata; + } + + @Nullable + public String getParameterNameOf(Class<?> type) { + return targetMethodMetadata.getParameterNameOf(type); + } + + public void addParameter(ParameterSpec parameter) { + this.targetMethodMetadata.addParameter(parameter); + } + + public boolean returnsVoid() { + return getMethod().getReturnType().equals(Void.TYPE); + } + + public boolean returnsPage() { + return ClassUtils.isAssignable(Page.class, getMethod().getReturnType()); + } + + public boolean returnsSlice() { + return ClassUtils.isAssignable(Slice.class, getMethod().getReturnType()); + } + + public boolean returnsCollection() { + return ClassUtils.isAssignable(Collection.class, getMethod().getReturnType()); + } + + public boolean returnsSingleValue() { + return !returnsPage() && !returnsSlice() && !returnsCollection(); + } + + public boolean returnsOptionalValue() { + return ClassUtils.isAssignable(Optional.class, getMethod().getReturnType()); + } + + @Nullable + public TypeName getActualReturnType() { + return targetMethodMetadata.actualReturnType; + } + + @Nullable + public String getSortParameterName() { + return getParameterNameOf(Sort.class); + } + + @Nullable + public String getPageableParameterName() { + return getParameterNameOf(Pageable.class); + } + + @Nullable + public String getLimitParameterName() { + return getParameterNameOf(Limit.class); + } + + @Nullable + public <T> T annotationValue(Class<? extends Annotation> annotation, String attribute) { + AnnotationAttributes values = AnnotatedElementUtils.getMergedAnnotationAttributes(getMethod(), annotation); + return values != null ? (T) values.get(attribute) : null; + } + + @Nullable + public TypeName getReturnType() { + return targetMethodMetadata.getReturnType(); + } + + TargetAotRepositoryMethodImplementationMetadata getTargetMethodMetadata() { + return targetMethodMetadata; + } + + public CodeBlocks codeBlocks() { + return codeBlocks; + } +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java b/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java index 680cf7c2db..2554a29fa2 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /* * Copyright 2025 the original author or authors. * @@ -21,7 +5,7 @@ * 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, @@ -32,33 +16,51 @@ package org.springframework.data.repository.aot.generate; import org.apache.commons.logging.Log; -import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.GenerationMetadata; +import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.TargetAotRepositoryImplementationMetadata; import org.springframework.javapoet.CodeBlock; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** + * Helper to write contextual pieces of code during code generation. + * * @author Christoph Strobl - * @since 2025/01 */ public class CodeBlocks { - private final GenerationMetadata metadata; + private final TargetAotRepositoryImplementationMetadata metadata; - public CodeBlocks(GenerationMetadata metadata) { + public CodeBlocks(TargetAotRepositoryImplementationMetadata metadata) { this.metadata = metadata; } - private CodeBlock log(String level, String message) { + /** + * @param level the log level eg. `debug`. + * @param message the message to print/ + * @param args optional args to be applied to the message. + * @return a {@link CodeBlock} containing a level guarded logging statement. + */ + private CodeBlock log(String level, String message, Object... args) { CodeBlock.Builder builder = CodeBlock.builder(); builder.beginControlFlow("if($L.is$LEnabled())", metadata.fieldNameOf(Log.class), StringUtils.capitalize(level)); - builder.addStatement("$L.$L($S)", metadata.fieldNameOf(Log.class), level, message); + if (ObjectUtils.isEmpty(args)) { + builder.addStatement("$L.$L($S)", metadata.fieldNameOf(Log.class), level, message); + } else { + builder.addStatement("$L.$L($S.formatted($L))", metadata.fieldNameOf(Log.class), level, message, + StringUtils.arrayToCommaDelimitedString(args)); + } builder.endControlFlow(); return builder.build(); } - public CodeBlock logDebug(String message) { - return log("debug", message); + /** + * @param message the logging message. + * @param args optional args to apply to the message. + * @return a {@link CodeBlock} containing a debug level guarded logging statement. + */ + public CodeBlock logDebug(String message, Object... args) { + return log("debug", message, args); } } 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 0cd80f119e..e788b81eab 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.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; +import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.TargetAotRepositoryImplementationMetadata; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.JavaFile; @@ -47,7 +48,7 @@ public void contribute(GenerationContext generationContext) { AotRepositoryBuilder builder = AotRepositoryBuilder.forRepository(repositoryInformation); builder.withFileCustomizer(this::customizeFile); builder.withConstructorCustomizer(this::customizeConstructor); - builder.withDerivedMethodCustomizer(this::customizeDerivedMethod); + builder.withDerivedMethodFunction(this::contributeRepositoryMethod); JavaFile file = builder.javaFile(); String typeName = "%s.%s".formatted(file.packageName, file.typeSpec.name); @@ -78,12 +79,12 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB } - protected void customizeFile(RepositoryInformation information, AotRepositoryBuilder.GenerationMetadata metadata, + protected void customizeFile(RepositoryInformation information, TargetAotRepositoryImplementationMetadata metadata, TypeSpec.Builder builder) { } - protected Contribution customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { - return Contribution.SKIP; + protected AotRepositoryMethodBuilder contributeRepositoryMethod(AotRepositoryMethodGenerationContext context) { + return null; } } 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 6889e5ed97..19ab041fe7 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 @@ -35,15 +35,14 @@ void testCompile() { DummyModuleAotRepositoryContext aotContext = new DummyModuleAotRepositoryContext(UserRepository.class, null); RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { - @Override - protected Contribution customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { - methodBuilder.customize(((repositoryInformation, metadata, builder) -> { - if (!metadata.returnsVoid()) { + protected AotRepositoryMethodBuilder contributeRepositoryMethod(AotRepositoryMethodGenerationContext context) { + + return new AotRepositoryMethodBuilder(context).customize(((ctx, builder) -> { + if (!ctx.returnsVoid()) { builder.addStatement("return null"); } })); - return Contribution.CODE; } }; From a11576888e2a6dd1d8a1316cbe6d34f652984958 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Wed, 15 Jan 2025 07:43:32 +0100 Subject: [PATCH 13/14] Remove code no longer needed --- .../aot/generate/AotRepositoryBuilder.java | 17 ++++---- .../repository/aot/generate/Contribution.java | 40 ------------------- 2 files changed, 10 insertions(+), 47 deletions(-) delete mode 100644 src/main/java/org/springframework/data/repository/aot/generate/Contribution.java 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 95d86e5bcc..fc83c65a6a 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,6 +29,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.aot.generate.Generated; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.ClassName; @@ -75,6 +76,7 @@ public JavaFile javaFile() { // start creating the type TypeSpec.Builder builder = TypeSpec.classBuilder(this.generationMetadata.getTargetTypeName()) // .addModifiers(Modifier.PUBLIC) // + .addAnnotation(Generated.class) // .addAnnotation(Component.class) // .addJavadoc("AOT generated repository implementation for {@link $T}.\n", repositoryInformation.getRepositoryInterface()) // @@ -95,11 +97,10 @@ public JavaFile javaFile() { // start with the derived ones ReflectionUtils.doWithMethods(repositoryInformation.getRepositoryInterface(), method -> { -// AotRepositoryDerivedMethodBuilder derivedMethodBuilder = new AotRepositoryDerivedMethodBuilder(method, -// repositoryInformation, generationMetadata); - AotRepositoryMethodGenerationContext context = new AotRepositoryMethodGenerationContext(method, repositoryInformation, generationMetadata); + AotRepositoryMethodGenerationContext context = new AotRepositoryMethodGenerationContext(method, + repositoryInformation, generationMetadata); AotRepositoryMethodBuilder methodBuilder = methodContextFunction.apply(context); - if(methodBuilder != null) { + if (methodBuilder != null) { builder.addMethod(methodBuilder.buildMethod()); } @@ -110,7 +111,7 @@ the isBaseClassMethod(it) check seems to have some issues. need to hard code it here */ - if(ReflectionUtils.findMethod(CrudRepository.class, it.getName(), it.getParameterTypes()) != null) { + if (ReflectionUtils.findMethod(CrudRepository.class, it.getName(), it.getParameterTypes()) != null) { return false; } @@ -132,7 +133,8 @@ AotRepositoryBuilder withConstructorCustomizer(Consumer<AotRepositoryConstructor return this; } - AotRepositoryBuilder withDerivedMethodFunction(Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction) { + AotRepositoryBuilder withDerivedMethodFunction( + Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction) { this.methodContextFunction = methodContextFunction; return this; } @@ -161,7 +163,8 @@ private String typeName() { public interface RepositoryCustomizer { - void customize(RepositoryInformation repositoryInformation, TargetAotRepositoryImplementationMetadata metadata, TypeSpec.Builder builder); + void customize(RepositoryInformation repositoryInformation, TargetAotRepositoryImplementationMetadata metadata, + TypeSpec.Builder builder); } public class TargetAotRepositoryImplementationMetadata { diff --git a/src/main/java/org/springframework/data/repository/aot/generate/Contribution.java b/src/main/java/org/springframework/data/repository/aot/generate/Contribution.java deleted file mode 100644 index acf39b525d..0000000000 --- a/src/main/java/org/springframework/data/repository/aot/generate/Contribution.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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. - */ - -/* - * 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.repository.aot.generate; - -/** - * @author Christoph Strobl - * @since 2025/01 - */ -public enum Contribution { - CODE, SKIP -} From 464a761ba34c38a7a9aa8983c1c9f39b7806cf95 Mon Sep 17 00:00:00 2001 From: Christoph Strobl <christoph.strobl@broadcom.com> Date: Wed, 15 Jan 2025 13:52:53 +0100 Subject: [PATCH 14/14] Use part tree in method context to determine count/delete/... --- .../aot/generate/AotRepositoryBuilder.java | 63 +-------------- .../AotRepositoryConstructorBuilder.java | 5 +- .../AotRepositoryImplementationMetadata.java | 81 +++++++++++++++++++ .../generate/AotRepositoryMethodBuilder.java | 44 +--------- .../AotRepositoryMethodGenerationContext.java | 35 +++++--- ...epositoryMethodImplementationMetadata.java | 74 +++++++++++++++++ .../repository/aot/generate/CodeBlocks.java | 5 +- .../aot/generate/RepositoryContributor.java | 3 +- 8 files changed, 194 insertions(+), 116 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryImplementationMetadata.java create mode 100644 src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodImplementationMetadata.java 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 fc83c65a6a..f830f108b0 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 @@ -18,9 +18,6 @@ import java.time.YearMonth; import java.time.ZoneId; import java.time.temporal.ChronoField; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; import java.util.function.Consumer; import java.util.function.Function; @@ -37,7 +34,6 @@ import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; @@ -47,7 +43,7 @@ public class AotRepositoryBuilder { private final RepositoryInformation repositoryInformation; - private final TargetAotRepositoryImplementationMetadata generationMetadata; + private final AotRepositoryImplementationMetadata generationMetadata; private Consumer<AotRepositoryConstructorBuilder> constructorBuilderCustomizer; private Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction; @@ -60,7 +56,7 @@ public static AotRepositoryBuilder forRepository(RepositoryInformation repositor AotRepositoryBuilder(RepositoryInformation repositoryInformation) { this.repositoryInformation = repositoryInformation; - this.generationMetadata = new TargetAotRepositoryImplementationMetadata(className()); + this.generationMetadata = new AotRepositoryImplementationMetadata(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()) @@ -145,7 +141,7 @@ AotRepositoryBuilder withFileCustomizer(RepositoryCustomizer repositoryCustomize return this; } - TargetAotRepositoryImplementationMetadata getGenerationMetadata() { + AotRepositoryImplementationMetadata getGenerationMetadata() { return generationMetadata; } @@ -163,58 +159,7 @@ private String typeName() { public interface RepositoryCustomizer { - void customize(RepositoryInformation repositoryInformation, TargetAotRepositoryImplementationMetadata metadata, + void customize(RepositoryInformation repositoryInformation, AotRepositoryImplementationMetadata metadata, TypeSpec.Builder builder); } - - public class TargetAotRepositoryImplementationMetadata { - - private ClassName className; - private Map<String, FieldSpec> fields = new HashMap<>(); - - public TargetAotRepositoryImplementationMetadata(ClassName className) { - this.className = className; - } - - @Nullable - public String fieldNameOf(Class<?> type) { - - TypeName lookup = TypeName.get(type).withoutAnnotations(); - for (Entry<String, FieldSpec> field : fields.entrySet()) { - if (field.getValue().type.withoutAnnotations().equals(lookup)) { - return field.getKey(); - } - } - - return null; - } - - public ClassName getTargetTypeName() { - return className; - } - - public String getTargetTypeSimpleName() { - return className.simpleName(); - } - - public String getTargetTypePackageName() { - return className.packageName(); - } - - public boolean hasField(String fieldName) { - return fields.containsKey(fieldName); - } - - public void addField(String fieldName, TypeName type, Modifier... modifiers) { - fields.put(fieldName, FieldSpec.builder(type, fieldName, modifiers).build()); - } - - public void addField(FieldSpec fieldSpec) { - fields.put(fieldSpec.name, fieldSpec); - } - - Map<String, FieldSpec> getFields() { - return fields; - } - } } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java index 2b929b6e35..d5c6d60ce4 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryConstructorBuilder.java @@ -23,7 +23,6 @@ import javax.lang.model.element.Modifier; import org.springframework.core.ResolvableType; -import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.TargetAotRepositoryImplementationMetadata; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; @@ -35,13 +34,13 @@ public class AotRepositoryConstructorBuilder { private final RepositoryInformation repositoryInformation; - private final TargetAotRepositoryImplementationMetadata metadata; + private final AotRepositoryImplementationMetadata metadata; private final Map<String, TypeName> constructorArguments; private ConstructorCustomizer customizer = (info, builder) -> {}; public AotRepositoryConstructorBuilder(RepositoryInformation repositoryInformation, - TargetAotRepositoryImplementationMetadata metadata) { + AotRepositoryImplementationMetadata metadata) { this.repositoryInformation = repositoryInformation; this.metadata = metadata; diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryImplementationMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryImplementationMetadata.java new file mode 100644 index 0000000000..ed7dc2f2db --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryImplementationMetadata.java @@ -0,0 +1,81 @@ +/* + * 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.generate; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import javax.lang.model.element.Modifier; + +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.FieldSpec; +import org.springframework.javapoet.TypeName; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +class AotRepositoryImplementationMetadata { + + private ClassName className; + private Map<String, FieldSpec> fields = new HashMap<>(); + + public AotRepositoryImplementationMetadata(ClassName className) { + this.className = className; + } + + @Nullable + public String fieldNameOf(Class<?> type) { + + TypeName lookup = TypeName.get(type).withoutAnnotations(); + for (Entry<String, FieldSpec> field : fields.entrySet()) { + if (field.getValue().type.withoutAnnotations().equals(lookup)) { + return field.getKey(); + } + } + + return null; + } + + public ClassName getTargetTypeName() { + return className; + } + + public String getTargetTypeSimpleName() { + return className.simpleName(); + } + + public String getTargetTypePackageName() { + return className.packageName(); + } + + public boolean hasField(String fieldName) { + return fields.containsKey(fieldName); + } + + public void addField(String fieldName, TypeName type, Modifier... modifiers) { + fields.put(fieldName, FieldSpec.builder(type, fieldName, modifiers).build()); + } + + public void addField(FieldSpec fieldSpec) { + fields.put(fieldSpec.name, fieldSpec); + } + + Map<String, FieldSpec> getFields() { + return fields; + } +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java index 0f313b7c5a..f4ca542646 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodBuilder.java @@ -69,8 +69,8 @@ public void addParameter(ParameterSpec parameter) { } public void setReturnType(@Nullable TypeName returnType, @Nullable TypeName actualReturnType) { - this.context.getTargetMethodMetadata().returnType = returnType; - this.context.getTargetMethodMetadata().actualReturnType = actualReturnType; + this.context.getTargetMethodMetadata().setReturnType(returnType); + this.context.getTargetMethodMetadata().setActualReturnType(actualReturnType); } public AotRepositoryMethodBuilder customize(RepositoryMethodCustomizer customizer) { @@ -86,9 +86,9 @@ MethodSpec buildMethod() { } builder.addJavadoc("AOT generated implementation of {@link $T#$L($L)}.", context.getMethod().getDeclaringClass(), context.getMethod().getName(), - StringUtils.collectionToCommaDelimitedString(context.getTargetMethodMetadata().methodArguments.values().stream() + StringUtils.collectionToCommaDelimitedString(context.getTargetMethodMetadata().getMethodArguments().values().stream() .map(it -> it.type.toString()).collect(Collectors.toList()))); - context.getTargetMethodMetadata().methodArguments.forEach((name, spec) -> builder.addParameter(spec)); + context.getTargetMethodMetadata().getMethodArguments().forEach((name, spec) -> builder.addParameter(spec)); customizer.customize(context, builder); return builder.build(); } @@ -137,40 +137,4 @@ private void initReturnType(Method method, RepositoryInformation repositoryInfor public interface RepositoryMethodCustomizer { void customize(AotRepositoryMethodGenerationContext context, MethodSpec.Builder builder); } - - public static class TargetAotRepositoryMethodImplementationMetadata { - - private final Map<String, ParameterSpec> methodArguments; - @Nullable public TypeName actualReturnType; - @Nullable private TypeName returnType; - - public TargetAotRepositoryMethodImplementationMetadata() { - this.methodArguments = new LinkedHashMap<>(); - } - - @Nullable - public String getParameterNameOf(Class<?> type) { - for (Entry<String, ParameterSpec> entry : methodArguments.entrySet()) { - if (entry.getValue().type.equals(TypeName.get(type))) { - return entry.getKey(); - } - } - return null; - } - - @Nullable - public TypeName getReturnType() { - return returnType; - } - - @Nullable - public TypeName getActualReturnType() { - return actualReturnType; - } - - public void addParameter(ParameterSpec parameterSpec) { - this.methodArguments.put(parameterSpec.name, parameterSpec); - } - - } } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java index 546a377708..99dedcffe4 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodGenerationContext.java @@ -45,9 +45,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; -import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.TargetAotRepositoryImplementationMetadata; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.TargetAotRepositoryMethodImplementationMetadata; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.query.parser.PartTree; import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.ParameterSpec; import org.springframework.javapoet.TypeName; @@ -62,18 +61,24 @@ public class AotRepositoryMethodGenerationContext { private final Method method; private final RepositoryInformation repositoryInformation; - private final TargetAotRepositoryImplementationMetadata targetTypeMetadata; - private final TargetAotRepositoryMethodImplementationMetadata targetMethodMetadata; + private final AotRepositoryImplementationMetadata targetTypeMetadata; + private final AotRepositoryMethodImplementationMetadata targetMethodMetadata; private final CodeBlocks codeBlocks; + @Nullable PartTree partTree; public AotRepositoryMethodGenerationContext(Method method, RepositoryInformation repositoryInformation, - TargetAotRepositoryImplementationMetadata targetTypeMetadata) { + AotRepositoryImplementationMetadata targetTypeMetadata) { this.method = method; this.repositoryInformation = repositoryInformation; this.targetTypeMetadata = targetTypeMetadata; - this.targetMethodMetadata = new TargetAotRepositoryMethodImplementationMetadata(); + this.targetMethodMetadata = new AotRepositoryMethodImplementationMetadata(); this.codeBlocks = new CodeBlocks(targetTypeMetadata); + try { + this.partTree = new PartTree(method.getName(), repositoryInformation.getDomainType()); + } catch (Exception e) { + // not a part tree quer + } } public boolean hasField(String fieldName) { @@ -100,7 +105,7 @@ public Method getMethod() { return method; } - TargetAotRepositoryImplementationMetadata getTargetTypeMetadata() { + AotRepositoryImplementationMetadata getTargetTypeMetadata() { return targetTypeMetadata; } @@ -137,9 +142,21 @@ public boolean returnsOptionalValue() { return ClassUtils.isAssignable(Optional.class, getMethod().getReturnType()); } + public boolean isCountMethod() { + return partTree != null ? partTree.isCountProjection() : method.getName().startsWith("count"); + } + + public boolean isExistsMethod() { + return partTree != null ? partTree.isExistsProjection() : method.getName().startsWith("exists"); + } + + public boolean isDeleteMethod() { + return partTree != null ? partTree.isDelete() : method.getName().startsWith("delete"); + } + @Nullable public TypeName getActualReturnType() { - return targetMethodMetadata.actualReturnType; + return targetMethodMetadata.getActualReturnType(); } @Nullable @@ -168,7 +185,7 @@ public TypeName getReturnType() { return targetMethodMetadata.getReturnType(); } - TargetAotRepositoryMethodImplementationMetadata getTargetMethodMetadata() { + AotRepositoryMethodImplementationMetadata getTargetMethodMetadata() { return targetMethodMetadata; } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodImplementationMetadata.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodImplementationMetadata.java new file mode 100644 index 0000000000..791c217fb1 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryMethodImplementationMetadata.java @@ -0,0 +1,74 @@ +/* + * 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.generate; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.javapoet.ParameterSpec; +import org.springframework.javapoet.TypeName; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +class AotRepositoryMethodImplementationMetadata { + + private final Map<String, ParameterSpec> methodArguments; + @Nullable private TypeName actualReturnType; + @Nullable private TypeName returnType; + + public AotRepositoryMethodImplementationMetadata() { + this.methodArguments = new LinkedHashMap<>(); + } + + @Nullable + public String getParameterNameOf(Class<?> type) { + for (Entry<String, ParameterSpec> entry : methodArguments.entrySet()) { + if (entry.getValue().type.equals(TypeName.get(type))) { + return entry.getKey(); + } + } + return null; + } + + @Nullable + public TypeName getReturnType() { + return returnType; + } + + @Nullable + public TypeName getActualReturnType() { + return actualReturnType; + } + + public void addParameter(ParameterSpec parameterSpec) { + this.methodArguments.put(parameterSpec.name, parameterSpec); + } + + Map<String, ParameterSpec> getMethodArguments() { + return methodArguments; + } + + void setActualReturnType(@Nullable TypeName actualReturnType) { + this.actualReturnType = actualReturnType; + } + + void setReturnType(@Nullable TypeName returnType) { + this.returnType = returnType; + } +} diff --git a/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java b/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java index 2554a29fa2..742984c6a8 100644 --- a/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java +++ b/src/main/java/org/springframework/data/repository/aot/generate/CodeBlocks.java @@ -16,7 +16,6 @@ package org.springframework.data.repository.aot.generate; import org.apache.commons.logging.Log; -import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.TargetAotRepositoryImplementationMetadata; import org.springframework.javapoet.CodeBlock; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -28,9 +27,9 @@ */ public class CodeBlocks { - private final TargetAotRepositoryImplementationMetadata metadata; + private final AotRepositoryImplementationMetadata metadata; - public CodeBlocks(TargetAotRepositoryImplementationMetadata metadata) { + public CodeBlocks(AotRepositoryImplementationMetadata metadata) { this.metadata = metadata; } 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 e788b81eab..6bcdb34464 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,7 +20,6 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.TypeReference; -import org.springframework.data.repository.aot.generate.AotRepositoryBuilder.TargetAotRepositoryImplementationMetadata; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.JavaFile; @@ -79,7 +78,7 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB } - protected void customizeFile(RepositoryInformation information, TargetAotRepositoryImplementationMetadata metadata, + protected void customizeFile(RepositoryInformation information, AotRepositoryImplementationMetadata metadata, TypeSpec.Builder builder) { }