diff --git a/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java b/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java index c1c676a968..650d646a94 100644 --- a/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java +++ b/src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java @@ -18,7 +18,6 @@ import java.util.Collection; import java.util.Optional; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; import org.springframework.beans.factory.config.BeanDefinition; @@ -29,6 +28,7 @@ import org.springframework.data.util.Lazy; import org.springframework.data.util.StreamUtils; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * Detects the custom implementation for a {@link org.springframework.data.repository.Repository} instance. If @@ -43,6 +43,7 @@ * @author Peter Rietzler * @author Jens Schauder * @author Mark Paluch + * @author Yanming Zhou */ public class CustomRepositoryImplementationDetector { @@ -97,7 +98,7 @@ public CustomRepositoryImplementationDetector(Environment environment, ResourceL * Tries to detect a custom implementation for a repository bean by classpath scanning. * * @param lookup must not be {@literal null}. - * @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found. + * @return the {@code Optional} of the custom implementation or empty {@code Optional} if none found. */ public Optional detectCustomImplementation(ImplementationLookupConfiguration lookup) { @@ -108,7 +109,7 @@ public Optional detectCustomImplementation(Implementatio .filter(lookup::matches) // .collect(StreamUtils.toUnmodifiableSet()); - return selectImplementationCandidate(lookup, definitions); + return definitions.isEmpty() ? findDefaultBeanDefinition(lookup) : selectImplementationCandidate(lookup, definitions); } private static Optional selectImplementationCandidate( @@ -143,6 +144,28 @@ private Set findCandidateBeanDefinitions(ImplementationDetection .collect(Collectors.toSet()); } + private Optional findDefaultBeanDefinition(ImplementationLookupConfiguration lookup) { + + if (lookup instanceof DefaultImplementationLookupConfiguration defaultLookup) { + + String interfaceName = defaultLookup.getInterfaceName(); + String packageName = ClassUtils.getPackageName(interfaceName); + String className = ClassUtils.getShortName(interfaceName); + String defaultImplementationClass = className + lookup.getImplementationPostfix() + ".class"; + + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false, + environment); + provider.setResourceLoader(resourceLoader); + provider.setResourcePattern(defaultImplementationClass); + provider.setMetadataReaderFactory(lookup.getMetadataReaderFactory()); + provider.addIncludeFilter((reader, factory) -> true); + + return provider.findCandidateComponents(packageName).stream().map(AbstractBeanDefinition.class::cast).findAny(); + } + + return Optional.empty(); + } + private static Optional throwAmbiguousCustomImplementationException( Collection definitions) { diff --git a/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java b/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java index 93a55b3b1a..779b9e8fe5 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java @@ -33,6 +33,7 @@ * @author Oliver Gierke * @author Mark Paluch * @author Kyrylo Merzlikin + * @author Yanming Zhou * @since 2.1 */ class DefaultImplementationLookupConfiguration implements ImplementationLookupConfiguration { @@ -53,6 +54,10 @@ class DefaultImplementationLookupConfiguration implements ImplementationLookupCo this.beanName = beanName; } + public String getInterfaceName() { + return this.interfaceName; + } + @Override public String getImplementationBeanName() { return beanName; diff --git a/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java b/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java index fd964bbb74..35e6a0bb5f 100644 --- a/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java @@ -31,6 +31,8 @@ import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.data.repository.config.CustomRepositoryImplementationDetectorUnitTests.First.CanonicalSampleRepositoryTestImpl; +import org.springframework.data.repository.config.lib.MyExtension; +import org.springframework.data.repository.config.lib.MyExtensionImpl; import org.springframework.data.util.Streamable; import org.springframework.mock.env.MockEnvironment; @@ -39,6 +41,7 @@ * * @author Jens Schauder * @author Mark Paluch + * @author Yanming Zhou */ class CustomRepositoryImplementationDetectorUnitTests { @@ -115,6 +118,30 @@ void throwsExceptionWhenMultipleImplementationAreFound() { }); } + @Test + void returnsDefaultBeanDefinitionIfNoCandidatesFound() { + + Class type = MyExtension.class; + // use a basePackage where no candidates present + String basePackage = this.getClass().getPackage().getName() + ".other"; + + when(configuration.getImplementationPostfix()).thenReturn("Impl"); + when(configuration.getBasePackages()).thenReturn(Streamable.of(basePackage)); + + RepositoryConfiguration repositoryConfiguration = mock(RepositoryConfiguration.class); + when(repositoryConfiguration.getRepositoryInterface()).thenReturn(type.getName()); + when(repositoryConfiguration.getImplementationBeanName()) + .thenReturn(Introspector.decapitalize(type.getSimpleName()) + "Impl"); + when(repositoryConfiguration.getImplementationBasePackages()) + .thenReturn(Streamable.of(basePackage)); + + var lookup = configuration.forRepositoryConfiguration(repositoryConfiguration); + + assertThat(detector.detectCustomImplementation(lookup)) // + .hasValueSatisfying( + it -> assertThat(it.getBeanClassName()).isEqualTo(MyExtensionImpl.class.getName())); + } + private RepositoryConfiguration configFor(Class type) { RepositoryConfiguration configuration = mock(RepositoryConfiguration.class); diff --git a/src/test/java/org/springframework/data/repository/config/DefaultFragmentImplementationIntegrationTests.java b/src/test/java/org/springframework/data/repository/config/DefaultFragmentImplementationIntegrationTests.java new file mode 100644 index 0000000000..afbf395b95 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/DefaultFragmentImplementationIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019-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.config; + +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mapping.Child; +import org.springframework.data.repository.config.app.ChildRepository; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Yanming Zhou + */ +class DefaultFragmentImplementationIntegrationTests { + + @Test + void defaultFragmentImplementationIsUsing() { + + var context = new AnnotationConfigApplicationContext(Config.class); + + assertThatThrownBy(() -> context.getBean(ChildRepository.class).extensionMethod(new Child(1, "", ""))) + .isInstanceOf(UnsupportedOperationException.class).hasMessage("Not Implemented"); + } + + @Configuration + @EnableRepositories(basePackageClasses = ChildRepository.class) + static class Config {} + +} diff --git a/src/test/java/org/springframework/data/repository/config/app/ChildRepository.java b/src/test/java/org/springframework/data/repository/config/app/ChildRepository.java new file mode 100644 index 0000000000..ee42f16104 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/app/ChildRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright 2022-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.config.app; + +import org.springframework.data.mapping.Child; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.config.lib.MyExtension; + +/** + * @author Yanming Zhou + */ +public interface ChildRepository extends CrudRepository, MyExtension {} diff --git a/src/test/java/org/springframework/data/repository/config/lib/MyExtension.java b/src/test/java/org/springframework/data/repository/config/lib/MyExtension.java new file mode 100644 index 0000000000..8eed6c93c7 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/lib/MyExtension.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022-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.config.lib; + +/** + * @author Yanming Zhou + */ +public interface MyExtension { + + void extensionMethod(T entity); +} diff --git a/src/test/java/org/springframework/data/repository/config/lib/MyExtensionImpl.java b/src/test/java/org/springframework/data/repository/config/lib/MyExtensionImpl.java new file mode 100644 index 0000000000..da95daa513 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/config/lib/MyExtensionImpl.java @@ -0,0 +1,27 @@ +/* + * Copyright 2022-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.config.lib; + +/** + * @author Yanming Zhou + */ +public class MyExtensionImpl implements MyExtension { + + @Override + public void extensionMethod(T entity) { + throw new UnsupportedOperationException("Not Implemented"); + } +}