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 84f344331..d7c0c9dd9 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.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import javax.lang.model.element.Modifier; @@ -43,6 +44,7 @@ import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; +import org.springframework.util.Assert; /** * Builder for AOT repository fragments. @@ -59,9 +61,9 @@ class AotRepositoryBuilder { private final ProjectionFactory projectionFactory; private final AotRepositoryFragmentMetadata generationMetadata; - private @Nullable ConstructorCustomizer constructorCustomizer; + private @Nullable Consumer constructorCustomizer; private @Nullable MethodContributorFactory methodContributorFactory; - private ClassCustomizer customizer; + private Consumer classCustomizer; private AotRepositoryBuilder(RepositoryInformation repositoryInformation, String moduleName, ProjectionFactory projectionFactory) { @@ -76,7 +78,7 @@ class AotRepositoryBuilder { .initializer("$T.getLog($T.class)", TypeName.get(LogFactory.class), this.generationMetadata.getTargetTypeName()) .build()); - this.customizer = (info, builder) -> {}; + this.classCustomizer = (builder) -> {}; } /** @@ -93,14 +95,14 @@ class AotRepositoryBuilder { } /** - * Configure a {@link ClassCustomizer} customizer. + * Configure a {@link AotRepositoryConstructorBuilder} customizer. * * @param classCustomizer must not be {@literal null}. * @return {@code this}. */ - public AotRepositoryBuilder withClassCustomizer(ClassCustomizer classCustomizer) { + public AotRepositoryBuilder withClassCustomizer(Consumer classCustomizer) { - this.customizer = classCustomizer; + this.classCustomizer = classCustomizer; return this; } @@ -110,7 +112,8 @@ class AotRepositoryBuilder { * @param constructorCustomizer must not be {@literal null}. * @return {@code this}. */ - public AotRepositoryBuilder withConstructorCustomizer(ConstructorCustomizer constructorCustomizer) { + public AotRepositoryBuilder withConstructorCustomizer( + Consumer constructorCustomizer) { this.constructorCustomizer = constructorCustomizer; return this; @@ -162,7 +165,12 @@ class AotRepositoryBuilder { generationMetadata.getFields().values().forEach(builder::addField); // finally customize the file itself - this.customizer.customize(repositoryInformation, builder); + this.classCustomizer.accept(customizer -> { + + Assert.notNull(customizer, "ClassCustomizer must not be null"); + customizer.customize(builder); + }); + JavaFile javaFile = JavaFile.builder(packageName(), builder.build()).build(); AotRepositoryMetadata metadata = getAotRepositoryMetadata(methodMetadata); @@ -171,11 +179,11 @@ class AotRepositoryBuilder { private MethodSpec buildConstructor() { - AotRepositoryConstructorBuilder constructorBuilder = new AotRepositoryConstructorBuilder(repositoryInformation, + RepositoryConstructorBuilder constructorBuilder = new RepositoryConstructorBuilder( generationMetadata); if (constructorCustomizer != null) { - constructorCustomizer.customize(constructorBuilder); + constructorCustomizer.accept(constructorBuilder); } return constructorBuilder.buildConstructor(); @@ -213,8 +221,7 @@ class AotRepositoryBuilder { if (repositoryInformation.isQueryMethod(method) && methodContributorFactory != null) { - MethodContributor contributor = methodContributorFactory.create(method, - repositoryInformation); + MethodContributor contributor = methodContributorFactory.create(method); if (contributor != null) { @@ -273,20 +280,6 @@ class AotRepositoryBuilder { return projectionFactory; } - /** - * Customizer interface to customize the AOT repository fragment class after it has been defined. - */ - public interface ClassCustomizer { - - /** - * Apply customization ot the AOT repository fragment class after it has been defined. - * - * @param information the repository information that is used for the AOT fragment. - * @param builder the class builder to be customized. - */ - void customize(RepositoryInformation information, TypeSpec.Builder builder); - - } /** * Customizer interface to customize the AOT repository fragment constructor through @@ -313,12 +306,11 @@ class AotRepositoryBuilder { * Apply customization ot the AOT repository fragment constructor. * * @param method the method to be contributed. - * @param information repository information. * @return the {@link MethodContributor} to be used. Can be {@literal null} if the method and method metadata should * not be contributed. */ @Nullable - MethodContributor create(Method method, RepositoryInformation information); + MethodContributor create(Method method); } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryClassBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryClassBuilder.java new file mode 100644 index 000000000..9c37487ec --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/AotRepositoryClassBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.aot.generate; + +import org.springframework.javapoet.TypeSpec; + +/** + * Builder for AOT repository fragment classes. + * + * @author Mark Paluch + * @since 4.0 + */ +public interface AotRepositoryClassBuilder { + + /** + * Add a class customizer. Customizer is invoked after building the class. + * + * @param customizer the customizer with direct access to the {@link TypeSpec.Builder type builder}. + */ + void customize(ClassCustomizer customizer); + + /** + * Customizer interface to customize the AOT repository fragment class after it has been defined. + */ + interface ClassCustomizer { + + /** + * Apply customization ot the AOT repository fragment class after it has been defined. + * + * @param builder the class builder to be customized. + */ + void customize(TypeSpec.Builder builder); + + } + +} 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 51b08d63c..45d964d07 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 @@ -15,15 +15,7 @@ */ package org.springframework.data.repository.aot.generate; -import java.util.Map.Entry; - -import javax.lang.model.element.Modifier; - -import org.springframework.core.ResolvableType; -import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata.ConstructorArgument; -import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.MethodSpec; -import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; /** @@ -33,18 +25,7 @@ import org.springframework.javapoet.TypeName; * @author Mark Paluch * @since 4.0 */ -public class AotRepositoryConstructorBuilder { - - private final RepositoryInformation repositoryInformation; - private final AotRepositoryFragmentMetadata metadata; - - private ConstructorCustomizer customizer = (info, builder) -> {}; - - AotRepositoryConstructorBuilder(RepositoryInformation repositoryInformation, AotRepositoryFragmentMetadata metadata) { - - this.repositoryInformation = repositoryInformation; - this.metadata = metadata; - } +public interface AotRepositoryConstructorBuilder { /** * Add constructor parameter and create a field storing its value. @@ -52,16 +33,7 @@ public class AotRepositoryConstructorBuilder { * @param parameterName name of the parameter. * @param type parameter type. */ - 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())); - } + void addParameter(String parameterName, Class type); /** * Add constructor parameter and create a field storing its value. @@ -69,7 +41,7 @@ public class AotRepositoryConstructorBuilder { * @param parameterName name of the parameter. * @param type parameter type. */ - public void addParameter(String parameterName, TypeName type) { + default void addParameter(String parameterName, TypeName type) { addParameter(parameterName, type, true); } @@ -80,14 +52,7 @@ public class AotRepositoryConstructorBuilder { * @param type parameter type. * @param createField whether to create a field for the parameter and assign its value to the field. */ - public void addParameter(String parameterName, TypeName type, boolean createField) { - - this.metadata.addConstructorArgument(parameterName, type, createField ? parameterName : null); - - if (createField) { - this.metadata.addField(parameterName, type, Modifier.PRIVATE, Modifier.FINAL); - } - } + void addParameter(String parameterName, TypeName type, boolean createField); /** * Add constructor customizer. Customizer is invoked after adding constructor arguments and before assigning @@ -95,41 +60,19 @@ public class AotRepositoryConstructorBuilder { * * @param customizer the customizer with direct access to the {@link MethodSpec.Builder constructor builder}. */ - public void customize(ConstructorCustomizer customizer) { - this.customizer = customizer; - } - - MethodSpec buildConstructor() { - - MethodSpec.Builder builder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); - - for (Entry parameter : this.metadata.getConstructorArguments().entrySet()) { - builder.addParameter(parameter.getValue().typeName(), parameter.getKey()); - } - - customizer.customize(repositoryInformation, builder); - - for (Entry parameter : this.metadata.getConstructorArguments().entrySet()) { - if (parameter.getValue().isForLocalField()) { - builder.addStatement("this.$N = $N", parameter.getKey(), parameter.getKey()); - } - } - - return builder.build(); - } + void customize(ConstructorCustomizer customizer); /** * Customizer for the AOT repository constructor. */ - public interface ConstructorCustomizer { + interface ConstructorCustomizer { /** * Customize the constructor. * - * @param information the repository information that is used for the AOT fragment. * @param builder the constructor builder to be customized. */ - void customize(RepositoryInformation information, MethodSpec.Builder builder); + void customize(MethodSpec.Builder builder); } diff --git a/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java new file mode 100644 index 000000000..fabb0dba5 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/generate/RepositoryConstructorBuilder.java @@ -0,0 +1,124 @@ +/* + * 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.Map.Entry; + +import javax.lang.model.element.Modifier; + +import org.springframework.core.ResolvableType; +import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata.ConstructorArgument; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.javapoet.TypeName; +import org.springframework.util.Assert; + +/** + * Builder for AOT Repository Constructors. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 4.0 + */ +class RepositoryConstructorBuilder implements AotRepositoryConstructorBuilder { + + private final AotRepositoryFragmentMetadata metadata; + + private ConstructorCustomizer customizer = (builder) -> {}; + + RepositoryConstructorBuilder(AotRepositoryFragmentMetadata metadata) { + this.metadata = metadata; + } + + /** + * Add constructor parameter and create a field storing its value. + * + * @param parameterName name of the parameter. + * @param type parameter type. + */ + @Override + 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())); + } + + /** + * Add constructor parameter and create a field storing its value. + * + * @param parameterName name of the parameter. + * @param type parameter type. + */ + @Override + public void addParameter(String parameterName, TypeName type) { + addParameter(parameterName, type, true); + } + + /** + * Add constructor parameter. + * + * @param parameterName name of the parameter. + * @param type parameter type. + * @param createField whether to create a field for the parameter and assign its value to the field. + */ + @Override + public void addParameter(String parameterName, TypeName type, boolean createField) { + + this.metadata.addConstructorArgument(parameterName, type, createField ? parameterName : null); + + if (createField) { + this.metadata.addField(parameterName, type, Modifier.PRIVATE, Modifier.FINAL); + } + } + + /** + * Add constructor customizer. Customizer is invoked after adding constructor arguments and before assigning + * constructor arguments to fields. + * + * @param customizer the customizer with direct access to the {@link MethodSpec.Builder constructor builder}. + */ + @Override + public void customize(ConstructorCustomizer customizer) { + + Assert.notNull(customizer, "ConstructorCustomizer must not be null"); + this.customizer = customizer; + } + + public MethodSpec buildConstructor() { + + MethodSpec.Builder builder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); + + for (Entry parameter : this.metadata.getConstructorArguments().entrySet()) { + builder.addParameter(parameter.getValue().typeName(), parameter.getKey()); + } + + customizer.customize(builder); + + for (Entry parameter : this.metadata.getConstructorArguments().entrySet()) { + if (parameter.getValue().isForLocalField()) { + builder.addStatement("this.$N = $N", parameter.getKey(), parameter.getKey()); + } + } + + return builder.build(); + } + +} 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 ef7926932..b02572379 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 @@ -31,6 +31,7 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.QueryMethod; import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.TypeName; +import org.springframework.util.StringUtils; import org.springframework.javapoet.TypeSpec; /** @@ -136,23 +137,21 @@ public class RepositoryContributor { /** * Customization hook for store implementations to customize class after building the entire class. */ - protected void customizeClass(RepositoryInformation information, - TypeSpec.Builder builder) { + protected void customizeClass(AotRepositoryClassBuilder builder) { } /** * Customization hook for store implementations to customize the fragment constructor after building the constructor. */ - protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + protected void customizeConstructor(AotRepositoryConstructorBuilder builder) { } /** * Customization hook for store implementations to contribute a query method. */ - protected @Nullable MethodContributor contributeQueryMethod(Method method, - RepositoryInformation repositoryInformation) { + protected @Nullable MethodContributor contributeQueryMethod(Method method) { return null; } diff --git a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java index 1ac8d043b..51dee1e56 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/AotRepositoryBuilderUnitTests.java @@ -96,7 +96,7 @@ class AotRepositoryBuilderUnitTests { AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", new SpelAwareProxyProjectionFactory()); repoBuilder.withConstructorCustomizer(ctor -> { - ctor.customize((info, code) -> { + ctor.customize((code) -> { code.addStatement("throw new $T($S)", IllegalStateException.class, "initialization error"); }); }); @@ -110,7 +110,9 @@ class AotRepositoryBuilderUnitTests { AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", new SpelAwareProxyProjectionFactory()); - repoBuilder.withClassCustomizer((info, metadata, clazz) -> { + repoBuilder.withClassCustomizer((builder) -> { + + builder.customize(clazz -> { clazz.addField(Float.class, "f", Modifier.PRIVATE, Modifier.STATIC); clazz.addField(Double.class, "d", Modifier.PUBLIC); @@ -119,6 +121,7 @@ class AotRepositoryBuilderUnitTests { clazz.addAnnotation(Repository.class); clazz.addMethod(MethodSpec.methodBuilder("oops").build()); + }); }); assertThat(repoBuilder.build().javaFile().toString()) // @@ -138,7 +141,7 @@ class AotRepositoryBuilderUnitTests { AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, "Commons", new SpelAwareProxyProjectionFactory()); - repoBuilder.withQueryMethodContributor((method, info) -> { + repoBuilder.withQueryMethodContributor((method) -> { return new MethodContributor<>(mock(QueryMethod.class), null) { diff --git a/src/test/java/org/springframework/data/repository/aot/generate/MethodCapturingRepositoryContributor.java b/src/test/java/org/springframework/data/repository/aot/generate/MethodCapturingRepositoryContributor.java index 033c7fbe1..109e2e37c 100644 --- a/src/test/java/org/springframework/data/repository/aot/generate/MethodCapturingRepositoryContributor.java +++ b/src/test/java/org/springframework/data/repository/aot/generate/MethodCapturingRepositoryContributor.java @@ -15,15 +15,15 @@ */ package org.springframework.data.repository.aot.generate; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; import java.util.List; import org.assertj.core.api.MapAssert; import org.jspecify.annotations.Nullable; + import org.springframework.data.repository.config.AotRepositoryContext; -import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.QueryMethod; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -41,8 +41,7 @@ public class MethodCapturingRepositoryContributor extends RepositoryContributor } @Override - protected @Nullable MethodContributor contributeQueryMethod(Method method, - RepositoryInformation repositoryInformation) { + protected @Nullable MethodContributor contributeQueryMethod(Method method) { capturedInvocations.add(method.getName(), method); 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 915670400..1c0d254bd 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 @@ -58,10 +58,10 @@ class RepositoryContributorUnitTests { RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) { @Override - protected @Nullable MethodContributor contributeQueryMethod(Method method, - RepositoryInformation repositoryInformation) { + protected @Nullable MethodContributor contributeQueryMethod(Method method) { - return MethodContributor.forQueryMethod(new QueryMethod(method, repositoryInformation, getProjectionFactory())) + return MethodContributor + .forQueryMethod(new QueryMethod(method, getRepositoryInformation(), getProjectionFactory())) .withMetadata(new QueryMetadata() { @Override