Skip to content

Add SPI mechanism for ahead-of-time repository fragment registration #3093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>3.4.0-SNAPSHOT</version>
<version>3.4.0-GH-3090-SNAPSHOT</version>

<name>Spring Data Core</name>
<description>Core Spring concepts underpinning every Spring Data module.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,133 @@ XML::
======
====

[[repositories.spring-factories]]
==== Registering Fragments with spring.factories

As already mentioned in the <<repositories.configuration>> section, the infrastructure only auto-detects fragments within the repository base-package.
Therefore, fragments residing in another location or want to be contributed by an external archive will not be found if they do not share a common namespace.
Registering fragments within `spring.factories` allows you to circumvent this restriction as explained in the following section.

Imagine you'd like to provide some custom search functionality usable across multiple repositories for your organization leveraging a text search index.

First all you need is the fragment interface.
Note the generic `<T>` parameter to align the fragment with the repository domain type.

====
[source,java]
----
package com.acme.search;

public interface SearchExtension<T> {

List<T> search(String text, Limit limit);
}
----
====

Let's assume the actual full-text search is available via a `SearchService` that is registered as a `Bean` within the context so you can consume it in our `SearchExtension` implementation.
All you need to run the search is the collection (or index) name and an object mapper that converts the search results into actual domain objects as sketched out below.

====
[source,java]
----
package com.acme.search;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit;
import org.springframework.data.repository.core.support.RepositoryMethodContext;

class DefaultSearchExtension<T> implements SearchExtension<T> {

private final SearchService service;

DefaultSearchExtension(SearchService service) {
this.service = service;
}

public List<T> search(String text, Limit limit) {
return search(RepositoryMethodContext.currentMethod(), text, limit);
}

List<T> search(RepositoryMethodContext metadata, String text, Limit limit) {

Class<T> domainType = metadata.getRepository().getDomainType();

String indexName = domainType.getSimpleName().toLowerCase();
List<String> jsonResult = service.search(indexName, text, 0, limit.max());

return jsonResult.stream().map(…).collect(toList());
}
}
----
====

In the example above `RepositoryMethodContext.currentMethod()` is used to retrieve metadata for the actual method invocation.
`RepositoryMethodContext` exposes information attached to the repository such as the domain type.
In this case we use the repository domain type to identify the name of the index to be searched.

Now that you've got both, the fragment declaration and implementation you can register it in the `META-INF/spring.factories` file, package things up if needed, and you're good to go.

.Registering a fragment implementation through `META-INF/spring.factories`
====
[source,properties]
----
com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension
----
====

Exposing invocation metadata is costly, hence it is disabled by default.
To access `RepositoryMethodContext.currentMethod()` you need to advise the repository factory responsible for creating the actual repository to expose method metadata by setting the `exposeMetadata` flag.

====
[source,java]
----
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.lang.Nullable;

@Configuration
class MyConfiguration implements BeanPostProcessor {

@Bean
static BeanPostProcessor exposeMethodMetadata() {

return new BeanPostProcessor() {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {

if(bean instanceof RepositoryFactoryBeanSupport<?,?,?> factoryBean) {
factoryBean.setExposeMetadata(true);
}
}
};
}
}
----

The above example outlines how to enable metadata exposure by setting the `exposeMetadata` flag using a `BeanPostProcessor`.
Please do not just copy/paste the above but consider your actual use case which may require a more fine-grained approach as the above will simply enable the flag on every repository.
You may want to have a look at our https://github.com/spring-projects/spring-data-examples/tree/main/bom[spring-data-examples] project to draw inspiration.
====

Now you are ready to make use of your extension; Simply add the interface to your repository:

====
[source,java]
----
package io.my.movies;

import com.acme.search.SearchExtension;
import org.springframework.data.repository.CrudRepository;

interface MovieRepository extends CrudRepository<Movie, String>, SearchExtension<Movie> {

}
----
====

[[repositories.customize-base-repository]]
== Customize the Base Repository

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ public Stream<String> getFragmentInterfaces(String interfaceName) {
* Returns whether the given interface is a fragment candidate.
*
* @param interfaceName must not be {@literal null} or empty.
* @param factory must not be {@literal null}.
* @return
*/
private boolean isCandidate(String interfaceName) {
Expand All @@ -69,7 +68,6 @@ private boolean isCandidate(String interfaceName) {

AnnotationMetadata metadata = getAnnotationMetadata(interfaceName);
return !metadata.hasAnnotation(NoRepositoryBean.class.getName());

}

private AnnotationMetadata getAnnotationMetadata(String className) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@

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;
Expand All @@ -35,14 +39,18 @@
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.
Expand All @@ -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},
Expand All @@ -83,7 +92,7 @@ public RepositoryBeanDefinitionBuilder(BeanDefinitionRegistry registry, Reposito
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);
Expand Down Expand Up @@ -139,6 +148,7 @@ public BeanDefinitionBuilder build(RepositoryConfiguration<?> configuration) {
}

// 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
Expand Down Expand Up @@ -223,21 +233,71 @@ private Stream<RepositoryFragmentConfiguration> registerRepositoryFragmentsImple
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,
Expand Down Expand Up @@ -314,10 +374,47 @@ private void potentiallyRegisterRepositoryFragment(RepositoryConfiguration<?> co
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());
}
}
}
Loading