Skip to content

Commit c4b2a78

Browse files
committed
Fallback to default fragment implementation if no candidates found
It's an enhancement that should not break existing behaviors. The default implementation is `$FragmentInterfaceName + $ImplementationPostfix`, for example `com.example.FragmentImpl` is the default implementation of `com.example.Fragment`. It's useful for sharing repository fragments as library, application doesn't have to include library package in `@Enable…Repositories`, and it will back off if application provides custom implementation. See spring-projects/spring-data-jpa#3287
1 parent 98d265f commit c4b2a78

File tree

7 files changed

+178
-3
lines changed

7 files changed

+178
-3
lines changed

src/main/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetector.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.util.Collection;
1919
import java.util.Optional;
2020
import java.util.Set;
21-
import java.util.function.Supplier;
2221
import java.util.stream.Collectors;
2322

2423
import org.springframework.beans.factory.config.BeanDefinition;
@@ -29,6 +28,7 @@
2928
import org.springframework.data.util.Lazy;
3029
import org.springframework.data.util.StreamUtils;
3130
import org.springframework.util.Assert;
31+
import org.springframework.util.ClassUtils;
3232

3333
/**
3434
* Detects the custom implementation for a {@link org.springframework.data.repository.Repository} instance. If
@@ -43,6 +43,7 @@
4343
* @author Peter Rietzler
4444
* @author Jens Schauder
4545
* @author Mark Paluch
46+
* @author Yanming Zhou
4647
*/
4748
public class CustomRepositoryImplementationDetector {
4849

@@ -97,7 +98,7 @@ public CustomRepositoryImplementationDetector(Environment environment, ResourceL
9798
* Tries to detect a custom implementation for a repository bean by classpath scanning.
9899
*
99100
* @param lookup must not be {@literal null}.
100-
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found.
101+
* @return the {@code Optional<AbstractBeanDefinition>} of the custom implementation or empty {@code Optional} if none found.
101102
*/
102103
public Optional<AbstractBeanDefinition> detectCustomImplementation(ImplementationLookupConfiguration lookup) {
103104

@@ -108,7 +109,7 @@ public Optional<AbstractBeanDefinition> detectCustomImplementation(Implementatio
108109
.filter(lookup::matches) //
109110
.collect(StreamUtils.toUnmodifiableSet());
110111

111-
return selectImplementationCandidate(lookup, definitions);
112+
return definitions.isEmpty() ? findDefaultBeanDefinition(lookup) : selectImplementationCandidate(lookup, definitions);
112113
}
113114

114115
private static Optional<AbstractBeanDefinition> selectImplementationCandidate(
@@ -143,6 +144,28 @@ private Set<BeanDefinition> findCandidateBeanDefinitions(ImplementationDetection
143144
.collect(Collectors.toSet());
144145
}
145146

147+
private Optional<AbstractBeanDefinition> findDefaultBeanDefinition(ImplementationLookupConfiguration lookup) {
148+
149+
if (lookup instanceof DefaultImplementationLookupConfiguration defaultLookup) {
150+
151+
String interfaceName = defaultLookup.getInterfaceName();
152+
String packageName = ClassUtils.getPackageName(interfaceName);
153+
String className = ClassUtils.getShortName(interfaceName);
154+
String defaultImplementationClass = className + lookup.getImplementationPostfix() + ".class";
155+
156+
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false,
157+
environment);
158+
provider.setResourceLoader(resourceLoader);
159+
provider.setResourcePattern(defaultImplementationClass);
160+
provider.setMetadataReaderFactory(lookup.getMetadataReaderFactory());
161+
provider.addIncludeFilter((reader, factory) -> true);
162+
163+
return provider.findCandidateComponents(packageName).stream().map(AbstractBeanDefinition.class::cast).findAny();
164+
}
165+
166+
return Optional.empty();
167+
}
168+
146169
private static Optional<BeanDefinition> throwAmbiguousCustomImplementationException(
147170
Collection<BeanDefinition> definitions) {
148171

src/main/java/org/springframework/data/repository/config/DefaultImplementationLookupConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* @author Oliver Gierke
3434
* @author Mark Paluch
3535
* @author Kyrylo Merzlikin
36+
* @author Yanming Zhou
3637
* @since 2.1
3738
*/
3839
class DefaultImplementationLookupConfiguration implements ImplementationLookupConfiguration {
@@ -53,6 +54,10 @@ class DefaultImplementationLookupConfiguration implements ImplementationLookupCo
5354
this.beanName = beanName;
5455
}
5556

57+
public String getInterfaceName() {
58+
return this.interfaceName;
59+
}
60+
5661
@Override
5762
public String getImplementationBeanName() {
5863
return beanName;

src/test/java/org/springframework/data/repository/config/CustomRepositoryImplementationDetectorUnitTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.springframework.core.type.classreading.MetadataReaderFactory;
3232
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
3333
import org.springframework.data.repository.config.CustomRepositoryImplementationDetectorUnitTests.First.CanonicalSampleRepositoryTestImpl;
34+
import org.springframework.data.repository.config.lib.MyExtension;
35+
import org.springframework.data.repository.config.lib.MyExtensionImpl;
3436
import org.springframework.data.util.Streamable;
3537
import org.springframework.mock.env.MockEnvironment;
3638

@@ -39,6 +41,7 @@
3941
*
4042
* @author Jens Schauder
4143
* @author Mark Paluch
44+
* @author Yanming Zhou
4245
*/
4346
class CustomRepositoryImplementationDetectorUnitTests {
4447

@@ -115,6 +118,30 @@ void throwsExceptionWhenMultipleImplementationAreFound() {
115118
});
116119
}
117120

121+
@Test
122+
void returnsDefaultBeanDefinitionIfNoCandidatesFound() {
123+
124+
Class<?> type = MyExtension.class;
125+
// use a basePackage where no candidates present
126+
String basePackage = this.getClass().getPackage().getName() + ".other";
127+
128+
when(configuration.getImplementationPostfix()).thenReturn("Impl");
129+
when(configuration.getBasePackages()).thenReturn(Streamable.of(basePackage));
130+
131+
RepositoryConfiguration<?> repositoryConfiguration = mock(RepositoryConfiguration.class);
132+
when(repositoryConfiguration.getRepositoryInterface()).thenReturn(type.getName());
133+
when(repositoryConfiguration.getImplementationBeanName())
134+
.thenReturn(Introspector.decapitalize(type.getSimpleName()) + "Impl");
135+
when(repositoryConfiguration.getImplementationBasePackages())
136+
.thenReturn(Streamable.of(basePackage));
137+
138+
var lookup = configuration.forRepositoryConfiguration(repositoryConfiguration);
139+
140+
assertThat(detector.detectCustomImplementation(lookup)) //
141+
.hasValueSatisfying(
142+
it -> assertThat(it.getBeanClassName()).isEqualTo(MyExtensionImpl.class.getName()));
143+
}
144+
118145
private RepositoryConfiguration<?> configFor(Class<?> type) {
119146

120147
RepositoryConfiguration<?> configuration = mock(RepositoryConfiguration.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2019-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.config;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
20+
import org.springframework.context.annotation.Configuration;
21+
import org.springframework.data.mapping.Child;
22+
import org.springframework.data.repository.config.app.ChildRepository;
23+
24+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
25+
26+
/**
27+
* @author Yanming Zhou
28+
*/
29+
class DefaultFragmentImplementationIntegrationTests {
30+
31+
@Test
32+
void defaultRepositoryFragmentImplementationIsUsing() {
33+
34+
var context = new AnnotationConfigApplicationContext(Config.class);
35+
36+
assertThatThrownBy(() -> context.getBean(ChildRepository.class).extensionMethod(new Child(1, "", "")))
37+
.isInstanceOf(UnsupportedOperationException.class).hasMessage("Not Implemented");
38+
}
39+
40+
@Configuration
41+
@EnableRepositories(basePackageClasses = ChildRepository.class)
42+
static class Config {}
43+
44+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2022-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.config.app;
17+
18+
import org.springframework.data.mapping.Child;
19+
import org.springframework.data.repository.CrudRepository;
20+
import org.springframework.data.repository.config.lib.MyExtension;
21+
22+
/**
23+
* @author Yanming Zhou
24+
*/
25+
public interface ChildRepository extends CrudRepository<Child, String>, MyExtension<Child> {}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2022-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.config.lib;
17+
18+
/**
19+
* @author Yanming Zhou
20+
*/
21+
public interface MyExtension<T> {
22+
23+
void extensionMethod(T entity);
24+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2022-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.config.lib;
17+
18+
/**
19+
* @author Yanming Zhou
20+
*/
21+
public class MyExtensionImpl<T> implements MyExtension<T> {
22+
23+
@Override
24+
public void extensionMethod(T entity) {
25+
throw new UnsupportedOperationException("Not Implemented");
26+
}
27+
}

0 commit comments

Comments
 (0)