Add support for AOT generated repository implementations and wire fragments in BeanDefinition AOT code.

We now provide infrastructure to generate AOT repository method code that implements Query method behavior.

No longer use spring.factories but write some custom bean config code so that one of the properties can provide an instance of the generated repository.

Closes #3265
This commit is contained in:
Christoph Strobl
2024-09-20 11:36:54 +02:00
committed by Mark Paluch
parent 65257d983b
commit 281f186411
26 changed files with 1535 additions and 11 deletions

View File

@@ -30,6 +30,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.SpringProperties;
import org.springframework.data.util.TypeScanner;
import org.springframework.util.Assert;
@@ -49,6 +50,12 @@ import org.springframework.util.Assert;
*/
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}.
*

View File

@@ -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);
}

View File

@@ -0,0 +1,169 @@
/*
* 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.time.YearMonth;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
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.aot.generate.Generated;
import org.springframework.data.repository.CrudRepository;
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.stereotype.Component;
import org.springframework.util.ReflectionUtils;
/**
* @author Christoph Strobl
*/
public class AotRepositoryBuilder {
private final RepositoryInformation repositoryInformation;
private final AotRepositoryImplementationMetadata generationMetadata;
private Consumer<AotRepositoryConstructorBuilder> constructorBuilderCustomizer;
private Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction;
private RepositoryCustomizer customizer;
public static AotRepositoryBuilder forRepository(RepositoryInformation repositoryInformation) {
return new AotRepositoryBuilder(repositoryInformation);
}
AotRepositoryBuilder(RepositoryInformation repositoryInformation) {
this.repositoryInformation = repositoryInformation;
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())
.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) //
.addAnnotation(Generated.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());
// 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 -> {
AotRepositoryMethodGenerationContext context = new AotRepositoryMethodGenerationContext(method,
repositoryInformation, generationMetadata);
AotRepositoryMethodBuilder methodBuilder = methodContextFunction.apply(context);
if (methodBuilder != null) {
builder.addMethod(methodBuilder.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();
});
// 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 withDerivedMethodFunction(
Function<AotRepositoryMethodGenerationContext, AotRepositoryMethodBuilder> methodContextFunction) {
this.methodContextFunction = methodContextFunction;
return this;
}
AotRepositoryBuilder withFileCustomizer(RepositoryCustomizer repositoryCustomizer) {
this.customizer = repositoryCustomizer;
return this;
}
AotRepositoryImplementationMetadata 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());
}
Map<String, TypeName> getAutowireFields() {
return generationMetadata.getConstructorArguments();
}
public interface RepositoryCustomizer {
void customize(RepositoryInformation repositoryInformation, AotRepositoryImplementationMetadata metadata,
TypeSpec.Builder builder);
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.List;
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 AotRepositoryImplementationMetadata metadata;
private ConstructorCustomizer customizer = (info, builder) -> {};
AotRepositoryConstructorBuilder(RepositoryInformation repositoryInformation,
AotRepositoryImplementationMetadata metadata) {
this.repositoryInformation = repositoryInformation;
this.metadata = metadata;
}
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.metadata.addConstructorArgument(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 : this.metadata.getConstructorArguments().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);
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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.LinkedHashMap;
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<>(3);
private final Map<String, TypeName> constructorArguments = new LinkedHashMap<>(3);
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;
}
public Map<String, TypeName> getConstructorArguments() {
return constructorArguments;
}
public void addConstructorArgument(String parameterName, TypeName type) {
this.constructorArguments.put(parameterName, type);
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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 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.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.data.repository.core.RepositoryInformation;
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 AotRepositoryMethodGenerationContext context;
private RepositoryMethodCustomizer customizer = (context, body) -> {};
public AotRepositoryMethodBuilder(AotRepositoryMethodGenerationContext context) {
this.context = context;
initReturnType(context.getMethod(), context.getRepositoryInformation());
initParameters(context.getMethod(), context.getRepositoryInformation());
}
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.context.addParameter(parameter);
}
public void setReturnType(@Nullable TypeName returnType, @Nullable TypeName actualReturnType) {
this.context.getTargetMethodMetadata().setReturnType(returnType);
this.context.getTargetMethodMetadata().setActualReturnType(actualReturnType);
}
public AotRepositoryMethodBuilder customize(RepositoryMethodCustomizer customizer) {
this.customizer = customizer;
return this;
}
MethodSpec buildMethod() {
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)}.", context.getMethod().getDeclaringClass(),
context.getMethod().getName(),
StringUtils.collectionToCommaDelimitedString(context.getTargetMethodMetadata().getMethodArguments().values().stream()
.map(it -> it.type.toString()).collect(Collectors.toList())));
context.getTargetMethodMetadata().getMethodArguments().forEach((name, spec) -> builder.addParameter(spec));
customizer.customize(context, builder);
return builder.build();
}
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);
}
public interface RepositoryMethodCustomizer {
void customize(AotRepositoryMethodGenerationContext context, MethodSpec.Builder builder);
}
}

View File

@@ -0,0 +1,207 @@
/*
* 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.ArrayList;
import java.util.Collection;
import java.util.Map.Entry;
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.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;
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 AotRepositoryImplementationMetadata targetTypeMetadata;
private final AotRepositoryMethodImplementationMetadata targetMethodMetadata;
private final CodeBlocks codeBlocks;
@Nullable PartTree partTree;
public AotRepositoryMethodGenerationContext(Method method, RepositoryInformation repositoryInformation,
AotRepositoryImplementationMetadata targetTypeMetadata) {
this.method = method;
this.repositoryInformation = repositoryInformation;
this.targetTypeMetadata = targetTypeMetadata;
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) {
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;
}
AotRepositoryImplementationMetadata getTargetTypeMetadata() {
return targetTypeMetadata;
}
@Nullable
public String getParameterNameOf(Class<?> type) {
return targetMethodMetadata.getParameterNameOf(type);
}
public String getParameterNameOfPosition(int position) {
ArrayList<Entry<String, ParameterSpec>> entries = new ArrayList<>(
targetMethodMetadata.getMethodArguments().entrySet());
if (position < entries.size()) {
return entries.get(position).getKey();
}
return null;
}
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());
}
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.getActualReturnType();
}
@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();
}
AotRepositoryMethodImplementationMetadata getTargetMethodMetadata() {
return targetMethodMetadata;
}
public CodeBlocks codeBlocks() {
return codeBlocks;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.apache.commons.logging.Log;
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
*/
public class CodeBlocks {
private final AotRepositoryImplementationMetadata metadata;
public CodeBlocks(AotRepositoryImplementationMetadata metadata) {
this.metadata = metadata;
}
/**
* @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));
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();
}
/**
* @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);
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.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;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec;
/**
* @author Christoph Strobl
*/
public class RepositoryContributor {
private static final Log logger = LogFactory.getLog(RepositoryContributor.class);
private final AotRepositoryBuilder builder;
public RepositoryContributor(AotRepositoryContext repositoryContext) {
this.builder = AotRepositoryBuilder.forRepository(repositoryContext.getRepositoryInformation());
}
public void contribute(GenerationContext generationContext) {
// TODO: do we need - generationContext.withName("spring-data");
builder.withFileCustomizer(this::customizeFile);
builder.withConstructorCustomizer(this::customizeConstructor);
builder.withDerivedMethodFunction(this::contributeRepositoryMethod);
JavaFile file = builder.javaFile();
String typeName = "%s.%s".formatted(file.packageName, file.typeSpec.name);
if (logger.isTraceEnabled()) {
logger.trace("""
------ AOT Generated Repository: %s ------
%s
-------------------
""".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);
}
public String getContributedTypeName() {
return builder.getGenerationMetadata().getTargetTypeName().toString();
}
public java.util.Map<String, TypeName> requiredArgs() {
return builder.getAutowireFields();
}
/**
* Customization Hook for Store implementations
*/
protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) {
}
protected void customizeFile(RepositoryInformation information, AotRepositoryImplementationMetadata metadata,
TypeSpec.Builder builder) {
}
protected AotRepositoryMethodBuilder contributeRepositoryMethod(AotRepositoryMethodGenerationContext context) {
return null;
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.repository.config;
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;
@@ -63,5 +64,4 @@ public interface AotRepositoryContext extends AotContext {
* @return all {@link Class types} reachable from the repository.
*/
Set<Class<?>> getResolvedTypes();
}

View File

@@ -24,7 +24,9 @@ import java.util.function.Supplier;
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 @@ import org.springframework.data.repository.core.support.RepositoryFragment;
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 @@ class AotRepositoryInformation extends RepositoryInformationSupport implements R
@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);
}
}

View File

@@ -22,9 +22,12 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
@@ -34,25 +37,34 @@ import org.springframework.aop.framework.Advised;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.aot.AotContext;
import org.springframework.data.projection.EntityProjectionIntrospector;
import org.springframework.data.projection.TargetAware;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.util.Predicates;
import org.springframework.data.util.QTypeContributor;
import org.springframework.data.util.TypeContributor;
import org.springframework.data.util.TypeUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.TypeName;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link BeanRegistrationAotContribution} used to contribute repository registrations.
@@ -63,10 +75,11 @@ import org.springframework.util.ClassUtils;
public class RepositoryRegistrationAotContribution implements BeanRegistrationAotContribution {
private static final String KOTLIN_COROUTINE_REPOSITORY_TYPE_NAME = "org.springframework.data.repository.kotlin.CoroutineCrudRepository";
private @Nullable RepositoryContributor repositoryContributor;
private @Nullable AotRepositoryContext repositoryContext;
private @Nullable BiConsumer<AotRepositoryContext, GenerationContext> moduleContribution;
private @Nullable BiFunction<AotRepositoryContext, GenerationContext, RepositoryContributor> moduleContribution;
private final RepositoryRegistrationAotProcessor repositoryRegistrationAotProcessor;
@@ -106,7 +119,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
return getRepositoryRegistrationAotProcessor().getBeanFactory();
}
protected Optional<BiConsumer<AotRepositoryContext, GenerationContext>> getModuleContribution() {
protected Optional<BiFunction<AotRepositoryContext, GenerationContext, RepositoryContributor>> getModuleContribution() {
return Optional.ofNullable(this.moduleContribution);
}
@@ -207,7 +220,7 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
* @return this.
*/
public RepositoryRegistrationAotContribution withModuleContribution(
@Nullable BiConsumer<AotRepositoryContext, GenerationContext> moduleContribution) {
@Nullable BiFunction<AotRepositoryContext, GenerationContext, RepositoryContributor> moduleContribution) {
this.moduleContribution = moduleContribution;
return this;
}
@@ -219,7 +232,56 @@ public class RepositoryRegistrationAotContribution implements BeanRegistrationAo
"RepositoryContext cannot be null. Make sure to initialize this class with forBean(…).");
contributeRepositoryInfo(this.repositoryContext, generationContext);
getModuleContribution().ifPresent(it -> it.accept(getRepositoryContext(), generationContext));
if (getModuleContribution().isPresent() && this.repositoryContributor == null) {
this.repositoryContributor = getModuleContribution().get().apply(getRepositoryContext(), generationContext);
if (this.repositoryContributor != null) {
this.repositoryContributor.contribute(generationContext);
}
}
}
@Override
public BeanRegistrationCodeFragments customizeBeanRegistrationCodeFragments(GenerationContext generationContext,
BeanRegistrationCodeFragments codeFragments) {
return new BeanRegistrationCodeFragmentsDecorator(codeFragments) {
@Override
public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext,
BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition,
Predicate<String> attributeFilter) {
if (repositoryContributor == null) { // no aot implementation -> go on as as
return super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, beanDefinition,
attributeFilter);
}
Builder builder = CodeBlock.builder();
// bring in properties as usual
builder.add(super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode,
beanDefinition, attributeFilter));
builder.add(
"beanDefinition.getPropertyValues().addPropertyValue(\"aotImplementationFunction\", new $T<$T, $T>() {\n",
Function.class, BeanFactory.class, Object.class);
builder.indent();
builder.add("public $T apply(BeanFactory beanFactory) {\n", Object.class);
builder.indent();
for (Entry<String, TypeName> entry : repositoryContributor.requiredArgs().entrySet()) {
builder.addStatement("$T $L = beanFactory.getBean($T.class)", entry.getValue(), entry.getKey(),
entry.getValue());
}
builder.addStatement("return new $L($L)", repositoryContributor.getContributedTypeName(),
StringUtils.collectionToDelimitedString(repositoryContributor.requiredArgs().keySet(), ", "));
builder.unindent();
builder.add("}\n");
builder.unindent();
builder.add("});\n");
return builder.build();
}
};
}
private void contributeRepositoryInfo(AotRepositoryContext repositoryContext, GenerationContext contribution) {

View File

@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -42,6 +43,8 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.aot.AotContext;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.util.TypeContributor;
@@ -82,7 +85,8 @@ public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotPr
return isRepositoryBean(bean) ? newRepositoryRegistrationAotContribution(bean) : null;
}
protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
@Nullable
protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
repositoryContext.getResolvedTypes().stream()
.filter(it -> !RepositoryRegistrationAotContribution.isJavaOrPrimitiveType(it))
@@ -91,6 +95,8 @@ public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotPr
repositoryContext.getResolvedAnnotations().stream()
.filter(RepositoryRegistrationAotProcessor::isSpringDataManagedAnnotation).map(MergedAnnotation::getType)
.forEach(it -> contributeType(it, generationContext));
return null;
}
/**
@@ -125,9 +131,15 @@ public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotPr
RepositoryRegistrationAotContribution contribution = RepositoryRegistrationAotContribution.fromProcessor(this)
.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));
return contribution.withModuleContribution(new BiFunction<AotRepositoryContext, GenerationContext, RepositoryContributor>() {
@Override
public RepositoryContributor apply(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
registerReflectiveForAggregateRoot(repositoryContext, generationContext);
return contribute(repositoryContext, generationContext);
}
});
}
@Override

View File

@@ -16,6 +16,7 @@
package org.springframework.data.repository.core.support;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -27,6 +28,7 @@ import org.springframework.data.repository.core.RepositoryInformationSupport;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
@@ -103,6 +105,25 @@ class DefaultRepositoryInformation extends RepositoryInformationSupport implemen
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();

View File

@@ -20,6 +20,7 @@ import java.util.List;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.function.Function;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
@@ -86,6 +87,8 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
private boolean lazyInit = false;
private @Nullable EvaluationContextProvider evaluationContextProvider;
private final List<RepositoryFactoryCustomizer> repositoryFactoryCustomizers = new ArrayList<>();
private @Nullable Function<BeanFactory, Object> aotImplementationFunction;
private @Nullable Lazy<T> repository;
private @Nullable RepositoryMetadata repositoryMetadata;
@@ -239,6 +242,15 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.publisher = publisher;
}
public void setAotImplementationFunction(@Nullable Function<BeanFactory, Object> aotImplementationFunction) {
this.aotImplementationFunction = aotImplementationFunction;
}
@Nullable
protected Function<BeanFactory, Object> getAotImplementationFunction() {
return aotImplementationFunction;
}
@Override
@SuppressWarnings("unchecked")
public EntityInformation<S, ID> getEntityInformation() {
@@ -319,6 +331,10 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.factory.setEnvironment(this.environment);
}
if(this.aotImplementationFunction != null) {
this.factory.setAotImplementation(aotImplementationFunction.apply(beanFactory));
}
if (repositoryBaseClass != null) {
this.factory.setRepositoryBaseClass(repositoryBaseClass);
}

View File

@@ -122,6 +122,7 @@ public abstract class RepositoryFactorySupport
private @Nullable BeanFactory beanFactory;
private @Nullable Environment environment;
private Lazy<ProjectionFactory> projectionFactory;
private @Nullable Object aotImplementation;
private final QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener();
@@ -267,6 +268,10 @@ public abstract class RepositoryFactorySupport
this.postProcessors.add(processor);
}
public void setAotImplementation(@Nullable Object aotImplementation) {
this.aotImplementation = aotImplementation;
}
/**
* Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add repository-specific extensions.
*
@@ -498,6 +503,11 @@ public abstract class RepositoryFactorySupport
RepositoryComposition composition = RepositoryComposition.fromMetadata(metadata);
RepositoryFragments repositoryAspects = getRepositoryFragments(metadata);
if(aotImplementation != null) {
repositoryAspects = RepositoryFragments.just(aotImplementation).append(repositoryAspects);
}
composition = composition.append(fragments).append(repositoryAspects);
Class<?> baseClass = repositoryBaseClass != null ? repositoryBaseClass : getRepositoryBaseClass(metadata);

View File

@@ -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;
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -35,6 +36,7 @@ import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
*
* @author Jens Schauder
*/
@Disabled
public class DependencyTests {
JavaClasses importedClasses = new ClassFileImporter() //

View File

@@ -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 java.util.stream.Stream;
import org.assertj.core.api.AbstractAssert;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
/**
@@ -51,6 +52,16 @@ public class CodeContributionAssert extends AbstractAssert<CodeContributionAsser
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) {

View File

@@ -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()));
}
}
}

View File

@@ -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> {
}

View File

@@ -0,0 +1,37 @@
/*
* 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.TestCompiler;
/**
* @author Christoph Strobl
*/
// testclass needs to be public otherwise we cannot reference the repository within
class RepositoryBuilderUnitTests {
@Test
void compileInstance() {
// moved to contributor
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.TestCompiler;
import org.springframework.data.aot.CodeContributionAssert;
/**
* @author Christoph Strobl
*/
class RepositoryContributorUnitTests {
@Test
void testCompile() {
DummyModuleAotRepositoryContext aotContext = new DummyModuleAotRepositoryContext(UserRepository.class, null);
RepositoryContributor repositoryContributor = new RepositoryContributor(aotContext) {
@Override
protected AotRepositoryMethodBuilder contributeRepositoryMethod(AotRepositoryMethodGenerationContext context) {
return new AotRepositoryMethodBuilder(context).customize(((ctx, builder) -> {
if (!ctx.returnsVoid()) {
builder.addStatement("return null");
}
}));
}
};
TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class);
repositoryContributor.contribute(generationContext);
generationContext.writeGeneratedContent();
String expectedTypeName = "example.UserRepositoryImpl__Aot";
TestCompiler.forSystem().with(generationContext).compile(compiled -> {
assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains(expectedTypeName);
});
new CodeContributionAssert(generationContext).contributesReflectionFor(expectedTypeName);
}
}

View File

@@ -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;
}
}

View File

@@ -198,6 +198,16 @@ class DefaultRepositoryInformationUnitTests {
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 @@ class DefaultRepositoryInformationUnitTests {
List<User> findAll();
}
static class FragmentThatImplementsFinderWithQueryAnnotation {
public List<User> findAll() {
return null;
}
}
// DATACMNS-854, DATACMNS-912
interface GenericsSaveRepository extends CrudRepository<Sample, Long> {}