Browse Source
Add module identifier and base repository implementation properties. Fix fragment function previously overriding already set property due to name clash. Extend tests for bean definition resolution and code block creation. See: #3279 Original Pull Request: #3282pull/3304/head
18 changed files with 800 additions and 73 deletions
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
/* |
||||
* 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 example; |
||||
|
||||
import example.UserRepository.User; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
public interface UserRepositoryExtension { |
||||
User findUserByExtensionMethod(); |
||||
} |
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
/* |
||||
* 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 example; |
||||
|
||||
import example.UserRepository.User; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
public class UserRepositoryExtensionImpl implements UserRepositoryExtension { |
||||
|
||||
@Override |
||||
public User findUserByExtensionMethod() { |
||||
return null; |
||||
} |
||||
} |
||||
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
/* |
||||
* 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 static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.Mockito.doReturn; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import example.UserRepository; |
||||
import example.UserRepository.User; |
||||
|
||||
import java.util.TimeZone; |
||||
|
||||
import javax.lang.model.element.Modifier; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mockito; |
||||
import org.springframework.data.geo.Metric; |
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; |
||||
import org.springframework.data.repository.core.RepositoryInformation; |
||||
import org.springframework.data.repository.query.QueryMethod; |
||||
import org.springframework.data.util.TypeInformation; |
||||
import org.springframework.javapoet.MethodSpec; |
||||
import org.springframework.javapoet.TypeName; |
||||
import org.springframework.stereotype.Repository; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
class AotRepositoryBuilderUnitTests { |
||||
|
||||
RepositoryInformation repositoryInformation; |
||||
|
||||
@BeforeEach |
||||
void beforeEach() { |
||||
|
||||
repositoryInformation = mock(RepositoryInformation.class); |
||||
doReturn(UserRepository.class).when(repositoryInformation).getRepositoryInterface(); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void writesClassSkeleton() { |
||||
|
||||
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, |
||||
new SpelAwareProxyProjectionFactory()); |
||||
assertThat(repoBuilder.build().javaFile().toString()) |
||||
.contains("package %s;".formatted(UserRepository.class.getPackageName())) // same package as source repo
|
||||
.contains("@Generated") // marked as generated source
|
||||
.contains("public class %sImpl__Aot".formatted(UserRepository.class.getSimpleName())) // target name
|
||||
.contains("public UserRepositoryImpl__Aot()"); // default constructor if not arguments to wire
|
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void appliesCtorArguments() { |
||||
|
||||
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, |
||||
new SpelAwareProxyProjectionFactory()); |
||||
repoBuilder.withConstructorCustomizer(ctor -> { |
||||
ctor.addParameter("param1", Metric.class); |
||||
ctor.addParameter("param2", String.class); |
||||
ctor.addParameter("ctorScoped", TypeName.OBJECT, false); |
||||
}); |
||||
assertThat(repoBuilder.build().javaFile().toString()) //
|
||||
.contains("private final Metric param1;") //
|
||||
.contains("private final String param2;") //
|
||||
.doesNotContain("private final Object ctorScoped;") //
|
||||
.contains("public UserRepositoryImpl__Aot(Metric param1, String param2, Object ctorScoped)") //
|
||||
.contains("this.param1 = param1") //
|
||||
.contains("this.param2 = param2") //
|
||||
.doesNotContain("this.ctorScoped = ctorScoped"); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void appliesCtorCodeBlock() { |
||||
|
||||
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, |
||||
new SpelAwareProxyProjectionFactory()); |
||||
repoBuilder.withConstructorCustomizer(ctor -> { |
||||
ctor.customize((info, code) -> { |
||||
code.addStatement("throw new $T($S)", IllegalStateException.class, "initialization error"); |
||||
}); |
||||
}); |
||||
assertThat(repoBuilder.build().javaFile().toString()).containsIgnoringWhitespaces( |
||||
"UserRepositoryImpl__Aot() { throw new IllegalStateException(\"initialization error\"); }"); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void appliesClassCustomizations() { |
||||
|
||||
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, |
||||
new SpelAwareProxyProjectionFactory()); |
||||
|
||||
repoBuilder.withClassCustomizer((info, metadata, clazz) -> { |
||||
|
||||
clazz.addField(Float.class, "f", Modifier.PRIVATE, Modifier.STATIC); |
||||
clazz.addField(Double.class, "d", Modifier.PUBLIC); |
||||
clazz.addField(TimeZone.class, "t", Modifier.FINAL); |
||||
|
||||
clazz.addAnnotation(Repository.class); |
||||
|
||||
clazz.addMethod(MethodSpec.methodBuilder("oops").build()); |
||||
}); |
||||
|
||||
assertThat(repoBuilder.build().javaFile().toString()) //
|
||||
.contains("@Repository") //
|
||||
.contains("private static Float f;") //
|
||||
.contains("public Double d;") //
|
||||
.contains("final TimeZone t;") //
|
||||
.containsIgnoringWhitespaces("void oops() { }"); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void appliesQueryMethodContributor() { |
||||
|
||||
AotRepositoryBuilder repoBuilder = AotRepositoryBuilder.forRepository(repositoryInformation, |
||||
new SpelAwareProxyProjectionFactory()); |
||||
|
||||
when(repositoryInformation.isQueryMethod(Mockito.argThat(arg -> arg.getName().equals("findByFirstname")))) |
||||
.thenReturn(true); |
||||
doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any()); |
||||
|
||||
repoBuilder.withQueryMethodContributor((method, info) -> { |
||||
|
||||
return new MethodContributor<>(mock(QueryMethod.class), null) { |
||||
|
||||
@Override |
||||
public MethodSpec contribute(AotQueryMethodGenerationContext context) { |
||||
return MethodSpec.methodBuilder("oops").build(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean contributesMethodSpec() { |
||||
return true; |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
assertThat(repoBuilder.build().javaFile().toString()) //
|
||||
.containsIgnoringWhitespaces("void oops() { }"); |
||||
} |
||||
} |
||||
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
/* |
||||
* 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 static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.Mockito.doReturn; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import example.UserRepository; |
||||
import example.UserRepository.User; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.List; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mockito; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.data.repository.core.RepositoryInformation; |
||||
import org.springframework.data.util.TypeInformation; |
||||
import org.springframework.javapoet.ParameterSpec; |
||||
import org.springframework.javapoet.ParameterizedTypeName; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
class AotRepositoryMethodBuilderUnitTests { |
||||
|
||||
RepositoryInformation repositoryInformation; |
||||
AotQueryMethodGenerationContext methodGenerationContext; |
||||
|
||||
@BeforeEach |
||||
void beforeEach() { |
||||
repositoryInformation = Mockito.mock(RepositoryInformation.class); |
||||
methodGenerationContext = Mockito.mock(AotQueryMethodGenerationContext.class); |
||||
|
||||
when(methodGenerationContext.getRepositoryInformation()).thenReturn(repositoryInformation); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void generatesMethodSkeletonBasedOnGenerationMetadata() throws NoSuchMethodException { |
||||
|
||||
Method method = UserRepository.class.getMethod("findByFirstname", String.class); |
||||
when(methodGenerationContext.getMethod()).thenReturn(method); |
||||
when(methodGenerationContext.getReturnType()).thenReturn(ResolvableType.forClass(User.class)); |
||||
doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any()); |
||||
MethodMetadata methodMetadata = new MethodMetadata(repositoryInformation, method); |
||||
methodMetadata.addParameter(ParameterSpec.builder(String.class, "firstname").build()); |
||||
when(methodGenerationContext.getTargetMethodMetadata()).thenReturn(methodMetadata); |
||||
|
||||
AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(methodGenerationContext); |
||||
assertThat(builder.buildMethod().toString()) //
|
||||
.containsPattern("public .*User findByFirstname\\(.*String firstname\\)"); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void generatesMethodWithGenerics() throws NoSuchMethodException { |
||||
|
||||
Method method = UserRepository.class.getMethod("findByFirstnameIn", List.class); |
||||
when(methodGenerationContext.getMethod()).thenReturn(method); |
||||
when(methodGenerationContext.getReturnType()) |
||||
.thenReturn(ResolvableType.forClassWithGenerics(List.class, User.class)); |
||||
doReturn(TypeInformation.of(User.class)).when(repositoryInformation).getReturnType(any()); |
||||
MethodMetadata methodMetadata = new MethodMetadata(repositoryInformation, method); |
||||
methodMetadata |
||||
.addParameter(ParameterSpec.builder(ParameterizedTypeName.get(List.class, String.class), "firstnames").build()); |
||||
when(methodGenerationContext.getTargetMethodMetadata()).thenReturn(methodMetadata); |
||||
|
||||
AotRepositoryMethodBuilder builder = new AotRepositoryMethodBuilder(methodGenerationContext); |
||||
assertThat(builder.buildMethod().toString()) //
|
||||
.containsPattern("public .*List<.*User> findByFirstnameIn\\(") //
|
||||
.containsPattern(".*List<.*String> firstnames\\)"); |
||||
} |
||||
} |
||||
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
/* |
||||
* 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 static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
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; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
public class MethodCapturingRepositoryContributor extends RepositoryContributor { |
||||
|
||||
MultiValueMap<String, Method> capturedInvocations; |
||||
|
||||
public MethodCapturingRepositoryContributor(AotRepositoryContext repositoryContext) { |
||||
super(repositoryContext); |
||||
this.capturedInvocations = new LinkedMultiValueMap<>(3); |
||||
} |
||||
|
||||
@Override |
||||
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method, |
||||
RepositoryInformation repositoryInformation) { |
||||
capturedInvocations.add(method.getName(), method); |
||||
return null; |
||||
} |
||||
|
||||
void verifyContributionFor(String methodName) { |
||||
assertThat(capturedInvocations).containsKey(methodName); |
||||
} |
||||
|
||||
MapAssert<String, List<Method>> verifyContributedMethods() { |
||||
return assertThat(capturedInvocations); |
||||
} |
||||
} |
||||
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
/* |
||||
* 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.config; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mockito; |
||||
import org.springframework.aot.hint.RuntimeHints; |
||||
import org.springframework.beans.factory.support.RegisteredBean; |
||||
import org.springframework.beans.factory.support.RootBeanDefinition; |
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||
import org.springframework.data.aot.sample.ConfigWithCustomImplementation; |
||||
import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass; |
||||
import org.springframework.data.aot.sample.ConfigWithCustomRepositoryBaseClass.CustomerRepositoryWithCustomBaseRepo; |
||||
import org.springframework.data.aot.sample.ConfigWithSimpleCrudRepository; |
||||
import org.springframework.data.repository.core.RepositoryInformation; |
||||
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
class RepositoryBeanDefinitionReaderTests { |
||||
|
||||
@Test // GH-3279
|
||||
void readsSimpleConfigFromBeanFactory() { |
||||
|
||||
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class); |
||||
|
||||
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class); |
||||
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName()); |
||||
|
||||
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, |
||||
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); |
||||
|
||||
assertThat(repositoryInformation.getRepositoryInterface()).isEqualTo(ConfigWithSimpleCrudRepository.MyRepo.class); |
||||
assertThat(repositoryInformation.getDomainType()).isEqualTo(ConfigWithSimpleCrudRepository.Person.class); |
||||
assertThat(repositoryInformation.getFragments()).isEmpty(); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void readsCustomRepoBaseClassFromBeanFactory() { |
||||
|
||||
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithCustomRepositoryBaseClass.class); |
||||
|
||||
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class); |
||||
Class<?> repositoryInterfaceType = CustomerRepositoryWithCustomBaseRepo.class; |
||||
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); |
||||
|
||||
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, |
||||
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); |
||||
|
||||
assertThat(repositoryInformation.getRepositoryBaseClass()) |
||||
.isEqualTo(ConfigWithCustomRepositoryBaseClass.RepoBaseClass.class); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void readsFragmentsFromBeanFactory() { |
||||
|
||||
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithCustomImplementation.class); |
||||
|
||||
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class); |
||||
Class<?> repositoryInterfaceType = ConfigWithCustomImplementation.RepositoryWithCustomImplementation.class; |
||||
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(repositoryInterfaceType.getName()); |
||||
|
||||
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, |
||||
repoFactoryBean.getMergedBeanDefinition(), repoFactoryBean.getBeanFactory()); |
||||
|
||||
assertThat(repositoryInformation.getFragments()).satisfiesExactly(fragment -> { |
||||
assertThat(fragment.getSignatureContributor()) |
||||
.isEqualTo(ConfigWithCustomImplementation.CustomImplInterface.class); |
||||
}); |
||||
} |
||||
|
||||
@Test // GH-3279
|
||||
void fallsBackToModuleBaseClassIfSetAndNoRepoBaseDefined() { |
||||
|
||||
RegisteredBean repoFactoryBean = repositoryFactory(ConfigWithSimpleCrudRepository.class); |
||||
RootBeanDefinition rootBeanDefinition = repoFactoryBean.getMergedBeanDefinition().cloneBeanDefinition(); |
||||
// need to unset because its defined as non default
|
||||
rootBeanDefinition.getPropertyValues().removePropertyValue("repositoryBaseClass"); |
||||
rootBeanDefinition.getPropertyValues().add("moduleBaseClass", ModuleBase.class.getName()); |
||||
|
||||
RepositoryConfiguration<?> repoConfig = mock(RepositoryConfiguration.class); |
||||
Mockito.when(repoConfig.getRepositoryInterface()).thenReturn(ConfigWithSimpleCrudRepository.MyRepo.class.getName()); |
||||
|
||||
RepositoryInformation repositoryInformation = RepositoryBeanDefinitionReader.repositoryInformation(repoConfig, |
||||
rootBeanDefinition, repoFactoryBean.getBeanFactory()); |
||||
|
||||
assertThat(repositoryInformation.getRepositoryBaseClass()).isEqualTo(ModuleBase.class); |
||||
} |
||||
|
||||
static RegisteredBean repositoryFactory(Class<?> configClass) { |
||||
|
||||
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); |
||||
applicationContext.register(configClass); |
||||
applicationContext.refreshForAotProcessing(new RuntimeHints()); |
||||
|
||||
String[] beanNamesForType = applicationContext.getBeanNamesForType(RepositoryFactoryBeanSupport.class); |
||||
if (beanNamesForType.length != 1) { |
||||
throw new IllegalStateException("Unable to find repository FactoryBean"); |
||||
} |
||||
|
||||
return RegisteredBean.of(applicationContext.getBeanFactory(), beanNamesForType[0]); |
||||
} |
||||
|
||||
static class ModuleBase {} |
||||
} |
||||
Loading…
Reference in new issue