Consider fragments and repository contributions via spring.factories.
Closes: #3090 Original Pull Request: #3093
This commit is contained in:
committed by
Christoph Strobl
parent
201657e78a
commit
4df7a164e9
@@ -19,14 +19,18 @@ import static org.springframework.beans.factory.config.BeanDefinition.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
@@ -35,14 +39,18 @@ import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.data.config.ParsingUtils;
|
||||
import org.springframework.data.repository.core.support.RepositoryFragment;
|
||||
import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean;
|
||||
import org.springframework.data.util.Optionals;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Builder to create {@link BeanDefinitionBuilder} instance to eventually create Spring Data repository instances.
|
||||
@@ -63,6 +71,7 @@ class RepositoryBeanDefinitionBuilder {
|
||||
private final MetadataReaderFactory metadataReaderFactory;
|
||||
private final FragmentMetadata fragmentMetadata;
|
||||
private final CustomRepositoryImplementationDetector implementationDetector;
|
||||
private final RepositoryFactoriesLoader factoriesLoader;
|
||||
|
||||
/**
|
||||
* Creates a new {@link RepositoryBeanDefinitionBuilder} from the given {@link BeanDefinitionRegistry},
|
||||
@@ -83,7 +92,7 @@ class RepositoryBeanDefinitionBuilder {
|
||||
this.registry = registry;
|
||||
this.extension = extension;
|
||||
this.resourceLoader = resourceLoader;
|
||||
|
||||
this.factoriesLoader = RepositoryFactoriesLoader.forDefaultResourceLocation(resourceLoader.getClassLoader());
|
||||
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
|
||||
|
||||
this.fragmentMetadata = new FragmentMetadata(metadataReaderFactory);
|
||||
@@ -139,6 +148,7 @@ class RepositoryBeanDefinitionBuilder {
|
||||
}
|
||||
|
||||
// TODO: merge that with the one that creates the BD
|
||||
// TODO: Add support for fragments discovered from spring.factories
|
||||
RepositoryConfigurationAdapter<?> buildMetadata(RepositoryConfiguration<?> configuration) {
|
||||
|
||||
ImplementationDetectionConfiguration config = configuration
|
||||
@@ -223,21 +233,71 @@ class RepositoryBeanDefinitionBuilder {
|
||||
ImplementationDetectionConfiguration config = configuration
|
||||
.toImplementationDetectionConfiguration(metadataReaderFactory);
|
||||
|
||||
return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) //
|
||||
.map(it -> detectRepositoryFragmentConfiguration(it, config, configuration)) //
|
||||
.flatMap(Optionals::toStream) //
|
||||
Stream<RepositoryFragmentConfiguration> discovered = discoverFragments(configuration, config);
|
||||
Stream<RepositoryFragmentConfiguration> loaded = loadFragments(configuration);
|
||||
|
||||
return Stream.concat(discovered, loaded) //
|
||||
.peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) //
|
||||
.peek(it -> potentiallyRegisterRepositoryFragment(configuration, it));
|
||||
}
|
||||
|
||||
private Stream<RepositoryFragmentConfiguration> discoverFragments(RepositoryConfiguration<?> configuration,
|
||||
ImplementationDetectionConfiguration config) {
|
||||
return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface())
|
||||
.map(it -> detectRepositoryFragmentConfiguration(it, config, configuration)) //
|
||||
.flatMap(Optionals::toStream);
|
||||
}
|
||||
|
||||
private Stream<RepositoryFragmentConfiguration> loadFragments(RepositoryConfiguration<?> configuration) {
|
||||
|
||||
List<String> names = factoriesLoader.loadFactoryNames(configuration.getRepositoryInterface());
|
||||
|
||||
if (names.isEmpty()) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
return names.stream().map(it -> createFragmentConfiguration(null, configuration, it));
|
||||
}
|
||||
|
||||
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(String fragmentInterface,
|
||||
ImplementationDetectionConfiguration config, RepositoryConfiguration<?> configuration) {
|
||||
|
||||
ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface);
|
||||
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(lookup);
|
||||
List<String> names = factoriesLoader.loadFactoryNames(fragmentInterface);
|
||||
|
||||
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd,
|
||||
configuration.getConfigurationSource().generateBeanName(bd)));
|
||||
if (names.isEmpty()) {
|
||||
|
||||
ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface);
|
||||
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(lookup);
|
||||
|
||||
return beanDefinition.map(bd -> createFragmentConfiguration(fragmentInterface, configuration, bd));
|
||||
}
|
||||
|
||||
if (names.size() > 1) {
|
||||
logger.debug(String.format("Multiple fragment implementations %s registered for fragment interface %s", names,
|
||||
fragmentInterface));
|
||||
}
|
||||
|
||||
return Optional.of(createFragmentConfiguration(fragmentInterface, configuration, names.get(0)));
|
||||
}
|
||||
|
||||
private RepositoryFragmentConfiguration createFragmentConfiguration(@Nullable String fragmentInterface,
|
||||
RepositoryConfiguration<?> configuration, String className) {
|
||||
|
||||
try {
|
||||
|
||||
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className);
|
||||
AnnotatedGenericBeanDefinition bd = new AnnotatedGenericBeanDefinition(metadataReader.getAnnotationMetadata());
|
||||
return createFragmentConfiguration(fragmentInterface, configuration, bd);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static RepositoryFragmentConfiguration createFragmentConfiguration(@Nullable String fragmentInterface,
|
||||
RepositoryConfiguration<?> configuration, AbstractBeanDefinition beanDefinition) {
|
||||
|
||||
return new RepositoryFragmentConfiguration(fragmentInterface, beanDefinition,
|
||||
configuration.getConfigurationSource().generateBeanName(beanDefinition));
|
||||
}
|
||||
|
||||
private String potentiallyRegisterRepositoryImplementation(RepositoryConfiguration<?> configuration,
|
||||
@@ -314,10 +374,47 @@ class RepositoryBeanDefinitionBuilder {
|
||||
BeanDefinitionBuilder fragmentBuilder = BeanDefinitionBuilder.rootBeanDefinition(RepositoryFragment.class,
|
||||
"implemented");
|
||||
|
||||
fragmentBuilder.addConstructorArgValue(fragmentConfiguration.getInterfaceName());
|
||||
if (StringUtils.hasText(fragmentConfiguration.getInterfaceName())) {
|
||||
fragmentBuilder.addConstructorArgValue(fragmentConfiguration.getInterfaceName());
|
||||
}
|
||||
fragmentBuilder.addConstructorArgReference(fragmentConfiguration.getImplementationBeanName());
|
||||
|
||||
registry.registerBeanDefinition(beanName,
|
||||
ParsingUtils.getSourceBeanDefinition(fragmentBuilder, configuration.getSource()));
|
||||
}
|
||||
|
||||
static class RepositoryFactoriesLoader extends SpringFactoriesLoader {
|
||||
|
||||
private final Map<String, List<String>> factories;
|
||||
|
||||
/**
|
||||
* Create a new {@link SpringFactoriesLoader} instance.
|
||||
*
|
||||
* @param classLoader the classloader used to instantiate the factories
|
||||
* @param factories a map of factory class name to implementation class names
|
||||
*/
|
||||
protected RepositoryFactoriesLoader(@Nullable ClassLoader classLoader, Map<String, List<String>> factories) {
|
||||
super(classLoader, factories);
|
||||
this.factories = factories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RepositoryFactoriesLoader} instance that will load and instantiate the factory implementations
|
||||
* from the default location, using the given class loader.
|
||||
*
|
||||
* @param classLoader the ClassLoader to use for loading resources; can be {@code null} to use the default
|
||||
* @return a {@link RepositoryFactoriesLoader} instance
|
||||
* @see #forResourceLocation(String)
|
||||
*/
|
||||
public static RepositoryFactoriesLoader forDefaultResourceLocation(@Nullable ClassLoader classLoader) {
|
||||
ClassLoader resourceClassLoader = (classLoader != null ? classLoader
|
||||
: SpringFactoriesLoader.class.getClassLoader());
|
||||
return new RepositoryFactoriesLoader(classLoader,
|
||||
loadFactoriesResource(resourceClassLoader, FACTORIES_RESOURCE_LOCATION));
|
||||
}
|
||||
|
||||
List<String> loadFactoryNames(String factoryType) {
|
||||
return this.factories.getOrDefault(factoryType, Collections.emptyList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.data.config.ConfigurationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -33,7 +34,7 @@ import org.springframework.util.ObjectUtils;
|
||||
*/
|
||||
public final class RepositoryFragmentConfiguration {
|
||||
|
||||
private final String interfaceName;
|
||||
private final Optional<String> interfaceName;
|
||||
private final String className;
|
||||
private final Optional<AbstractBeanDefinition> beanDefinition;
|
||||
private final String beanName;
|
||||
@@ -42,10 +43,10 @@ public final class RepositoryFragmentConfiguration {
|
||||
* Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@code className} of the
|
||||
* implementation.
|
||||
*
|
||||
* @param interfaceName must not be {@literal null} or empty.
|
||||
* @param interfaceName
|
||||
* @param className must not be {@literal null} or empty.
|
||||
*/
|
||||
public RepositoryFragmentConfiguration(String interfaceName, String className) {
|
||||
public RepositoryFragmentConfiguration(@Nullable String interfaceName, String className) {
|
||||
this(interfaceName, className, Optional.empty(), generateBeanName(className));
|
||||
}
|
||||
|
||||
@@ -53,33 +54,32 @@ public final class RepositoryFragmentConfiguration {
|
||||
* Creates a {@link RepositoryFragmentConfiguration} given {@code interfaceName} and {@link AbstractBeanDefinition} of
|
||||
* the implementation.
|
||||
*
|
||||
* @param interfaceName must not be {@literal null} or empty.
|
||||
* @param interfaceName
|
||||
* @param beanDefinition must not be {@literal null}.
|
||||
*/
|
||||
public RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinition beanDefinition) {
|
||||
public RepositoryFragmentConfiguration(@Nullable String interfaceName, AbstractBeanDefinition beanDefinition) {
|
||||
|
||||
Assert.hasText(interfaceName, "Interface name must not be null or empty");
|
||||
Assert.notNull(beanDefinition, "Bean definition must not be null");
|
||||
|
||||
this.interfaceName = interfaceName;
|
||||
this.interfaceName = Optional.ofNullable(interfaceName);
|
||||
this.className = ConfigurationUtils.getRequiredBeanClassName(beanDefinition);
|
||||
this.beanDefinition = Optional.of(beanDefinition);
|
||||
this.beanName = generateBeanName();
|
||||
}
|
||||
|
||||
RepositoryFragmentConfiguration(String interfaceName, AbstractBeanDefinition beanDefinition, String beanName) {
|
||||
RepositoryFragmentConfiguration(@Nullable String interfaceName, AbstractBeanDefinition beanDefinition,
|
||||
String beanName) {
|
||||
this(interfaceName, ConfigurationUtils.getRequiredBeanClassName(beanDefinition), Optional.of(beanDefinition),
|
||||
beanName);
|
||||
}
|
||||
|
||||
private RepositoryFragmentConfiguration(String interfaceName, String className,
|
||||
private RepositoryFragmentConfiguration(@Nullable String interfaceName, String className,
|
||||
Optional<AbstractBeanDefinition> beanDefinition, String beanName) {
|
||||
|
||||
Assert.hasText(interfaceName, "Interface name must not be null or empty");
|
||||
Assert.notNull(beanDefinition, "Bean definition must not be null");
|
||||
Assert.notNull(beanName, "Bean name must not be null");
|
||||
|
||||
this.interfaceName = interfaceName;
|
||||
this.interfaceName = Optional.ofNullable(interfaceName);
|
||||
this.className = className;
|
||||
this.beanDefinition = beanDefinition;
|
||||
this.beanName = beanName;
|
||||
@@ -107,8 +107,9 @@ public final class RepositoryFragmentConfiguration {
|
||||
return getImplementationBeanName() + "Fragment";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getInterfaceName() {
|
||||
return this.interfaceName;
|
||||
return this.interfaceName.orElse(null);
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
@@ -83,7 +84,8 @@ class RepositoryBeanDefinitionRegistrarSupportUnitTests {
|
||||
AnnotationMetadata metadata = new StandardAnnotationMetadata(SampleConfiguration.class, true);
|
||||
|
||||
registrar.registerBeanDefinitions(metadata, registry);
|
||||
verify(registry, atLeast(1)).registerBeanDefinition(eq("commons.MyRepository.fragments#0"), any(BeanDefinition.class));
|
||||
verify(registry, atLeast(1)).registerBeanDefinition(eq("commons.MyRepository.fragments#0"),
|
||||
any(BeanDefinition.class));
|
||||
}
|
||||
|
||||
@Test // DATACMNS-1754
|
||||
@@ -109,7 +111,7 @@ class RepositoryBeanDefinitionRegistrarSupportUnitTests {
|
||||
assertNoBeanDefinitionRegisteredFor("excludedRepositoryImpl");
|
||||
}
|
||||
|
||||
@Test // DATACMNS-1172
|
||||
@Test // DATACMNS-1172, GH-3090
|
||||
void shouldLimitImplementationBasePackages() {
|
||||
|
||||
AnnotationMetadata metadata = new StandardAnnotationMetadata(LimitsImplementationBasePackages.class, true);
|
||||
@@ -118,6 +120,8 @@ class RepositoryBeanDefinitionRegistrarSupportUnitTests {
|
||||
|
||||
assertBeanDefinitionRegisteredFor("personRepository");
|
||||
assertNoBeanDefinitionRegisteredFor("fragmentImpl");
|
||||
assertBeanDefinitionRegisteredFor("spiFragmentImplFragment");
|
||||
assertBeanDefinitionRegisteredFor("spiContribution");
|
||||
}
|
||||
|
||||
@Test // DATACMNS-360
|
||||
|
||||
@@ -17,8 +17,9 @@ package org.springframework.data.repository.config.basepackage.repo;
|
||||
|
||||
import org.springframework.data.mapping.Person;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.config.spifragment.SpiFragment;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public interface PersonRepository extends Repository<Person, String>, Fragment {}
|
||||
public interface PersonRepository extends Repository<Person, String>, Fragment, SpiFragment {}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.config.spifragment;
|
||||
|
||||
/**
|
||||
* Class included through spring.factories for PersonRepository.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class SpiContribution {}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.config.spifragment;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public interface SpiFragment {}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.config.spifragment;
|
||||
|
||||
/**
|
||||
* Fragment for {@link SpiFragment} included through spring.factories for PersonRepository.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class SpiFragmentImpl implements SpiFragment {}
|
||||
@@ -1,2 +1,4 @@
|
||||
org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SampleMixin
|
||||
org.springframework.data.util.ProxyUtils$ProxyDetector=org.springframework.data.util.ProxyUtilsUnitTests$SampleProxyDetector
|
||||
org.springframework.data.repository.config.basepackage.repo.PersonRepository=org.springframework.data.repository.config.spifragment.SpiContribution
|
||||
org.springframework.data.repository.config.spifragment.SpiFragment=org.springframework.data.repository.config.spifragment.SpiFragmentImpl
|
||||
|
||||
Reference in New Issue
Block a user