Skip to content

Commit 4df7a16

Browse files
mp911dechristophstrobl
authored andcommitted
Consider fragments and repository contributions via spring.factories.
Closes: #3090 Original Pull Request: #3093
1 parent 201657e commit 4df7a16

File tree

8 files changed

+196
-24
lines changed

8 files changed

+196
-24
lines changed

Diff for: src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java

+106-9
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@
1919

2020
import java.io.IOException;
2121
import java.util.ArrayList;
22+
import java.util.Collections;
2223
import java.util.List;
2324
import java.util.Locale;
25+
import java.util.Map;
2426
import java.util.Optional;
2527
import java.util.stream.Collectors;
2628
import java.util.stream.Stream;
2729

2830
import org.apache.commons.logging.Log;
2931
import org.apache.commons.logging.LogFactory;
32+
33+
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
3034
import org.springframework.beans.factory.config.BeanDefinition;
3135
import org.springframework.beans.factory.config.RuntimeBeanReference;
3236
import org.springframework.beans.factory.support.AbstractBeanDefinition;
@@ -35,14 +39,18 @@
3539
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3640
import org.springframework.core.env.Environment;
3741
import org.springframework.core.io.ResourceLoader;
42+
import org.springframework.core.io.support.SpringFactoriesLoader;
3843
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
44+
import org.springframework.core.type.classreading.MetadataReader;
3945
import org.springframework.core.type.classreading.MetadataReaderFactory;
4046
import org.springframework.data.config.ParsingUtils;
4147
import org.springframework.data.repository.core.support.RepositoryFragment;
4248
import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean;
4349
import org.springframework.data.util.Optionals;
50+
import org.springframework.lang.Nullable;
4451
import org.springframework.util.Assert;
4552
import org.springframework.util.ClassUtils;
53+
import org.springframework.util.StringUtils;
4654

4755
/**
4856
* Builder to create {@link BeanDefinitionBuilder} instance to eventually create Spring Data repository instances.
@@ -63,6 +71,7 @@ class RepositoryBeanDefinitionBuilder {
6371
private final MetadataReaderFactory metadataReaderFactory;
6472
private final FragmentMetadata fragmentMetadata;
6573
private final CustomRepositoryImplementationDetector implementationDetector;
74+
private final RepositoryFactoriesLoader factoriesLoader;
6675

6776
/**
6877
* Creates a new {@link RepositoryBeanDefinitionBuilder} from the given {@link BeanDefinitionRegistry},
@@ -83,7 +92,7 @@ public RepositoryBeanDefinitionBuilder(BeanDefinitionRegistry registry, Reposito
8392
this.registry = registry;
8493
this.extension = extension;
8594
this.resourceLoader = resourceLoader;
86-
95+
this.factoriesLoader = RepositoryFactoriesLoader.forDefaultResourceLocation(resourceLoader.getClassLoader());
8796
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
8897

8998
this.fragmentMetadata = new FragmentMetadata(metadataReaderFactory);
@@ -139,6 +148,7 @@ public BeanDefinitionBuilder build(RepositoryConfiguration<?> configuration) {
139148
}
140149

141150
// TODO: merge that with the one that creates the BD
151+
// TODO: Add support for fragments discovered from spring.factories
142152
RepositoryConfigurationAdapter<?> buildMetadata(RepositoryConfiguration<?> configuration) {
143153

144154
ImplementationDetectionConfiguration config = configuration
@@ -223,21 +233,71 @@ private Stream<RepositoryFragmentConfiguration> registerRepositoryFragmentsImple
223233
ImplementationDetectionConfiguration config = configuration
224234
.toImplementationDetectionConfiguration(metadataReaderFactory);
225235

226-
return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) //
227-
.map(it -> detectRepositoryFragmentConfiguration(it, config, configuration)) //
228-
.flatMap(Optionals::toStream) //
236+
Stream<RepositoryFragmentConfiguration> discovered = discoverFragments(configuration, config);
237+
Stream<RepositoryFragmentConfiguration> loaded = loadFragments(configuration);
238+
239+
return Stream.concat(discovered, loaded) //
229240
.peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) //
230241
.peek(it -> potentiallyRegisterRepositoryFragment(configuration, it));
231242
}
232243

244+
private Stream<RepositoryFragmentConfiguration> discoverFragments(RepositoryConfiguration<?> configuration,
245+
ImplementationDetectionConfiguration config) {
246+
return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface())
247+
.map(it -> detectRepositoryFragmentConfiguration(it, config, configuration)) //
248+
.flatMap(Optionals::toStream);
249+
}
250+
251+
private Stream<RepositoryFragmentConfiguration> loadFragments(RepositoryConfiguration<?> configuration) {
252+
253+
List<String> names = factoriesLoader.loadFactoryNames(configuration.getRepositoryInterface());
254+
255+
if (names.isEmpty()) {
256+
return Stream.empty();
257+
}
258+
259+
return names.stream().map(it -> createFragmentConfiguration(null, configuration, it));
260+
}
261+
233262
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(String fragmentInterface,
234263
ImplementationDetectionConfiguration config, RepositoryConfiguration<?> configuration) {
235264

236-
ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface);
237-
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(lookup);
265+
List<String> names = factoriesLoader.loadFactoryNames(fragmentInterface);
266+
267+
if (names.isEmpty()) {
268+
269+
ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface);
270+
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(lookup);
271+
272+
return beanDefinition.map(bd -> createFragmentConfiguration(fragmentInterface, configuration, bd));
273+
}
274+
275+
if (names.size() > 1) {
276+
logger.debug(String.format("Multiple fragment implementations %s registered for fragment interface %s", names,
277+
fragmentInterface));
278+
}
279+
280+
return Optional.of(createFragmentConfiguration(fragmentInterface, configuration, names.get(0)));
281+
}
282+
283+
private RepositoryFragmentConfiguration createFragmentConfiguration(@Nullable String fragmentInterface,
284+
RepositoryConfiguration<?> configuration, String className) {
285+
286+
try {
238287

239-
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd,
240-
configuration.getConfigurationSource().generateBeanName(bd)));
288+
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className);
289+
AnnotatedGenericBeanDefinition bd = new AnnotatedGenericBeanDefinition(metadataReader.getAnnotationMetadata());
290+
return createFragmentConfiguration(fragmentInterface, configuration, bd);
291+
} catch (IOException e) {
292+
throw new IllegalStateException(e);
293+
}
294+
}
295+
296+
private static RepositoryFragmentConfiguration createFragmentConfiguration(@Nullable String fragmentInterface,
297+
RepositoryConfiguration<?> configuration, AbstractBeanDefinition beanDefinition) {
298+
299+
return new RepositoryFragmentConfiguration(fragmentInterface, beanDefinition,
300+
configuration.getConfigurationSource().generateBeanName(beanDefinition));
241301
}
242302

243303
private String potentiallyRegisterRepositoryImplementation(RepositoryConfiguration<?> configuration,
@@ -314,10 +374,47 @@ private void potentiallyRegisterRepositoryFragment(RepositoryConfiguration<?> co
314374
BeanDefinitionBuilder fragmentBuilder = BeanDefinitionBuilder.rootBeanDefinition(RepositoryFragment.class,
315375
"implemented");
316376

317-
fragmentBuilder.addConstructorArgValue(fragmentConfiguration.getInterfaceName());
377+
if (StringUtils.hasText(fragmentConfiguration.getInterfaceName())) {
378+
fragmentBuilder.addConstructorArgValue(fragmentConfiguration.getInterfaceName());
379+
}
318380
fragmentBuilder.addConstructorArgReference(fragmentConfiguration.getImplementationBeanName());
319381

320382
registry.registerBeanDefinition(beanName,
321383
ParsingUtils.getSourceBeanDefinition(fragmentBuilder, configuration.getSource()));
322384
}
385+
386+
static class RepositoryFactoriesLoader extends SpringFactoriesLoader {
387+
388+
private final Map<String, List<String>> factories;
389+
390+
/**
391+
* Create a new {@link SpringFactoriesLoader} instance.
392+
*
393+
* @param classLoader the classloader used to instantiate the factories
394+
* @param factories a map of factory class name to implementation class names
395+
*/
396+
protected RepositoryFactoriesLoader(@Nullable ClassLoader classLoader, Map<String, List<String>> factories) {
397+
super(classLoader, factories);
398+
this.factories = factories;
399+
}
400+
401+
/**
402+
* Create a {@link RepositoryFactoriesLoader} instance that will load and instantiate the factory implementations
403+
* from the default location, using the given class loader.
404+
*
405+
* @param classLoader the ClassLoader to use for loading resources; can be {@code null} to use the default
406+
* @return a {@link RepositoryFactoriesLoader} instance
407+
* @see #forResourceLocation(String)
408+
*/
409+
public static RepositoryFactoriesLoader forDefaultResourceLocation(@Nullable ClassLoader classLoader) {
410+
ClassLoader resourceClassLoader = (classLoader != null ? classLoader
411+
: SpringFactoriesLoader.class.getClassLoader());
412+
return new RepositoryFactoriesLoader(classLoader,
413+
loadFactoriesResource(resourceClassLoader, FACTORIES_RESOURCE_LOCATION));
414+
}
415+
416+
List<String> loadFactoryNames(String factoryType) {
417+
return this.factories.getOrDefault(factoryType, Collections.emptyList());
418+
}
419+
}
323420
}

Diff for: src/main/java/org/springframework/data/repository/config/RepositoryFragmentConfiguration.java

+13-12
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.beans.factory.support.AbstractBeanDefinition;
2222
import org.springframework.data.config.ConfigurationUtils;
23+
import org.springframework.lang.Nullable;
2324
import org.springframework.util.Assert;
2425
import org.springframework.util.ClassUtils;
2526
import org.springframework.util.ObjectUtils;
@@ -33,7 +34,7 @@
3334
*/
3435
public final class RepositoryFragmentConfiguration {
3536

36-
private final String interfaceName;
37+
private final Optional<String> interfaceName;
3738
private final String className;
3839
private final Optional<AbstractBeanDefinition> beanDefinition;
3940
private final String beanName;
@@ -42,44 +43,43 @@ public final class RepositoryFragmentConfiguration {
4243
* Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@code className} of the
4344
* implementation.
4445
*
45-
* @param interfaceName must not be {@literal null} or empty.
46+
* @param interfaceName
4647
* @param className must not be {@literal null} or empty.
4748
*/
48-
public RepositoryFragmentConfiguration(String interfaceName, String className) {
49+
public RepositoryFragmentConfiguration(@Nullable String interfaceName, String className) {
4950
this(interfaceName, className, Optional.empty(), generateBeanName(className));
5051
}
5152

5253
/**
5354
* Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@link AbstractBeanDefinition} of
5455
* the implementation.
5556
*
56-
* @param interfaceName must not be {@literal null} or empty.
57+
* @param interfaceName
5758
* @param beanDefinition must not be {@literal null}.
5859
*/
59-
public RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinition beanDefinition) {
60+
public RepositoryFragmentConfiguration(@Nullable String interfaceName, AbstractBeanDefinition beanDefinition) {
6061

61-
Assert.hasText(interfaceName, "Interface name must not be null or empty");
6262
Assert.notNull(beanDefinition, "Bean definition must not be null");
6363

64-
this.interfaceName = interfaceName;
64+
this.interfaceName = Optional.ofNullable(interfaceName);
6565
this.className = ConfigurationUtils.getRequiredBeanClassName(beanDefinition);
6666
this.beanDefinition = Optional.of(beanDefinition);
6767
this.beanName = generateBeanName();
6868
}
6969

70-
RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinition beanDefinition, String beanName) {
70+
RepositoryFragmentConfiguration(@Nullable String interfaceName, AbstractBeanDefinition beanDefinition,
71+
String beanName) {
7172
this(interfaceName, ConfigurationUtils.getRequiredBeanClassName(beanDefinition), Optional.of(beanDefinition),
7273
beanName);
7374
}
7475

75-
private RepositoryFragmentConfiguration(String interfaceName, String className,
76+
private RepositoryFragmentConfiguration(@Nullable String interfaceName, String className,
7677
Optional<AbstractBeanDefinition> beanDefinition, String beanName) {
7778

78-
Assert.hasText(interfaceName, "Interface name must not be null or empty");
7979
Assert.notNull(beanDefinition, "Bean definition must not be null");
8080
Assert.notNull(beanName, "Bean name must not be null");
8181

82-
this.interfaceName = interfaceName;
82+
this.interfaceName = Optional.ofNullable(interfaceName);
8383
this.className = className;
8484
this.beanDefinition = beanDefinition;
8585
this.beanName = beanName;
@@ -107,8 +107,9 @@ public String getFragmentBeanName() {
107107
return getImplementationBeanName() + "Fragment";
108108
}
109109

110+
@Nullable
110111
public String getInterfaceName() {
111-
return this.interfaceName;
112+
return this.interfaceName.orElse(null);
112113
}
113114

114115
public String getClassName() {

Diff for: src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.junit.jupiter.api.extension.ExtendWith;
2626
import org.mockito.Mock;
2727
import org.mockito.junit.jupiter.MockitoExtension;
28+
2829
import org.springframework.beans.factory.config.BeanDefinition;
2930
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3031
import org.springframework.beans.factory.support.BeanNameGenerator;
@@ -83,7 +84,8 @@ void shouldExposeFragmentsAsBean() {
8384
AnnotationMetadata metadata = new StandardAnnotationMetadata(SampleConfiguration.class, true);
8485

8586
registrar.registerBeanDefinitions(metadata, registry);
86-
verify(registry, atLeast(1)).registerBeanDefinition(eq("commons.MyRepository.fragments#0"), any(BeanDefinition.class));
87+
verify(registry, atLeast(1)).registerBeanDefinition(eq("commons.MyRepository.fragments#0"),
88+
any(BeanDefinition.class));
8789
}
8890

8991
@Test // DATACMNS-1754
@@ -109,7 +111,7 @@ void registersBeanDefinitionWithoutFragmentImplementations() {
109111
assertNoBeanDefinitionRegisteredFor("excludedRepositoryImpl");
110112
}
111113

112-
@Test // DATACMNS-1172
114+
@Test // DATACMNS-1172, GH-3090
113115
void shouldLimitImplementationBasePackages() {
114116

115117
AnnotationMetadata metadata = new StandardAnnotationMetadata(LimitsImplementationBasePackages.class, true);
@@ -118,6 +120,8 @@ void shouldLimitImplementationBasePackages() {
118120

119121
assertBeanDefinitionRegisteredFor("personRepository");
120122
assertNoBeanDefinitionRegisteredFor("fragmentImpl");
123+
assertBeanDefinitionRegisteredFor("spiFragmentImplFragment");
124+
assertBeanDefinitionRegisteredFor("spiContribution");
121125
}
122126

123127
@Test // DATACMNS-360

Diff for: src/test/java/org/springframework/data/repository/config/basepackage/repo/PersonRepository.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
import org.springframework.data.mapping.Person;
1919
import org.springframework.data.repository.Repository;
20+
import org.springframework.data.repository.config.spifragment.SpiFragment;
2021

2122
/**
2223
* @author Mark Paluch
2324
*/
24-
public interface PersonRepository extends Repository<Person, String>, Fragment {}
25+
public interface PersonRepository extends Repository<Person, String>, Fragment, SpiFragment {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 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.spifragment;
17+
18+
/**
19+
* Class included through spring.factories for PersonRepository.
20+
*
21+
* @author Mark Paluch
22+
*/
23+
public class SpiContribution {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 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.spifragment;
17+
18+
/**
19+
* @author Mark Paluch
20+
*/
21+
public interface SpiFragment {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 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.spifragment;
17+
18+
/**
19+
* Fragment for {@link SpiFragment} included through spring.factories for PersonRepository.
20+
*
21+
* @author Mark Paluch
22+
*/
23+
public class SpiFragmentImpl implements SpiFragment {}

Diff for: src/test/resources/META-INF/spring.factories

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SampleMixin
22
org.springframework.data.util.ProxyUtils$ProxyDetector=org.springframework.data.util.ProxyUtilsUnitTests$SampleProxyDetector
3+
org.springframework.data.repository.config.basepackage.repo.PersonRepository=org.springframework.data.repository.config.spifragment.SpiContribution
4+
org.springframework.data.repository.config.spifragment.SpiFragment=org.springframework.data.repository.config.spifragment.SpiFragmentImpl

0 commit comments

Comments
 (0)