Browse Source

Consider fragments and repository contributions via spring.factories.

Closes: #3090
Original Pull Request: #3093
pull/3136/head
Mark Paluch 2 years ago committed by Christoph Strobl
parent
commit
4df7a164e9
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 115
      src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java
  2. 25
      src/main/java/org/springframework/data/repository/config/RepositoryFragmentConfiguration.java
  3. 8
      src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java
  4. 3
      src/test/java/org/springframework/data/repository/config/basepackage/repo/PersonRepository.java
  5. 23
      src/test/java/org/springframework/data/repository/config/spifragment/SpiContribution.java
  6. 21
      src/test/java/org/springframework/data/repository/config/spifragment/SpiFragment.java
  7. 23
      src/test/java/org/springframework/data/repository/config/spifragment/SpiFragmentImpl.java
  8. 2
      src/test/resources/META-INF/spring.factories

115
src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java

@ -19,14 +19,18 @@ import static org.springframework.beans.factory.config.BeanDefinition.*; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
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 {
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd,
configuration.getConfigurationSource().generateBeanName(bd)));
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 { @@ -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());
}
}
}

25
src/main/java/org/springframework/data/repository/config/RepositoryFragmentConfiguration.java

@ -20,6 +20,7 @@ import java.util.Optional; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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() {

8
src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java

@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; @@ -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 { @@ -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 { @@ -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 { @@ -118,6 +120,8 @@ class RepositoryBeanDefinitionRegistrarSupportUnitTests {
assertBeanDefinitionRegisteredFor("personRepository");
assertNoBeanDefinitionRegisteredFor("fragmentImpl");
assertBeanDefinitionRegisteredFor("spiFragmentImplFragment");
assertBeanDefinitionRegisteredFor("spiContribution");
}
@Test // DATACMNS-360

3
src/test/java/org/springframework/data/repository/config/basepackage/repo/PersonRepository.java

@ -17,8 +17,9 @@ package org.springframework.data.repository.config.basepackage.repo; @@ -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 {}

23
src/test/java/org/springframework/data/repository/config/spifragment/SpiContribution.java

@ -0,0 +1,23 @@ @@ -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 {}

21
src/test/java/org/springframework/data/repository/config/spifragment/SpiFragment.java

@ -0,0 +1,21 @@ @@ -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 {}

23
src/test/java/org/springframework/data/repository/config/spifragment/SpiFragmentImpl.java

@ -0,0 +1,23 @@ @@ -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 {}

2
src/test/resources/META-INF/spring.factories

@ -1,2 +1,4 @@ @@ -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

Loading…
Cancel
Save