Polishing.

Update documentation.
Additional logging for repository bootstrap procedure.
Limit usage of Optional in RepositoryFragment.

Original Pull Request: #3145
This commit is contained in:
Christoph Strobl
2024-09-03 12:06:56 +02:00
parent f7895eb306
commit d9bdd2b550
4 changed files with 96 additions and 50 deletions

View File

@@ -250,7 +250,7 @@ Imagine you'd like to provide some custom search functionality usable across mul
First all you need is the fragment interface.
Note the generic `<T>` parameter to align the fragment with the repository domain type.
====
.Fragment Interface
[source,java]
----
package com.acme.search;
@@ -260,12 +260,11 @@ 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.
====
.Fragment implementation
[source,java]
----
package com.acme.search;
@@ -297,27 +296,44 @@ class DefaultSearchExtension<T> implements SearchExtension<T> {
}
}
----
====
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 almost 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.
To access `RepositoryMethodContext.currentMethod()` you need to advise the repository factory responsible for creating the actual repository to expose method metadata.
.Expose Repository Metadata
[tabs]
======
Marker Interface::
+
====
[source,java]
Adding the `RepositoryMetadataAccess` marker interface to the fragments implementation will trigger the infrastructure and enable metadata exposure for those repositories using the fragment.
[source,java,role="primary"]
----
package com.acme.search;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit;
import org.springframework.data.repository.core.support.RepositoryMetadataAccess;
import org.springframework.data.repository.core.support.RepositoryMethodContext;
class DefaultSearchExtension<T> implements SearchExtension<T>, RepositoryMetadataAccess {
// ...
}
----
====
Bean Post Processor::
+
====
The `exposeMetadata` flag can be set directly on the repository factory bean via a `BeanPostProcessor`.
[source,java,role="secondary"]
----
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
@@ -345,14 +361,21 @@ class MyConfiguration {
}
----
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:
Having both, the fragment declaration and implementation in place you can register the extension in the `META-INF/spring.factories` file and package things up if needed.
====
.Register the fragment in `META-INF/spring.factories`
[source,properties]
----
com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension
----
Now you are ready to make use of your extension; Simply add the interface to your repository.
.Using it
[source,java]
----
package io.my.movies;
@@ -364,7 +387,6 @@ interface MovieRepository extends CrudRepository<Movie, String>, SearchExtension
}
----
====
[[repositories.customize-base-repository]]
== Customize the Base Repository

View File

@@ -348,10 +348,16 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
if (MethodInvocationValidator.supports(repositoryInterface)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Register MethodInvocationValidator for %s…", repositoryInterface.getName()));
}
result.addAdvice(new MethodInvocationValidator());
}
if (this.exposeMetadata || shouldExposeMetadata(fragments)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Register ExposeMetadataInterceptor for %s…", repositoryInterface.getName()));
}
result.addAdvice(new ExposeMetadataInterceptor(metadata));
result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
}
@@ -371,6 +377,9 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
}
if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Register DefaultMethodInvokingMethodInterceptor for %s…", repositoryInterface.getName()));
}
result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
}
@@ -622,6 +631,13 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
return Lazy.of(() -> getProjectionFactory(this.classLoader, this.beanFactory));
}
/**
* Checks if at least one {@link RepositoryFragment} indicates need to access to {@link RepositoryMetadata} by being
* flagged with {@link RepositoryMetadataAccess}.
*
* @param fragments
* @return {@literal true} if access to metadata is required.
*/
private static boolean shouldExposeMetadata(RepositoryFragments fragments) {
for (RepositoryFragment<?> fragment : fragments) {

View File

@@ -20,6 +20,7 @@ import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@@ -41,6 +42,7 @@ import org.springframework.util.ReflectionUtils;
* Fragments are immutable.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.0
* @see RepositoryComposition
*/
@@ -53,7 +55,7 @@ public interface RepositoryFragment<T> {
* @return
*/
static <T> RepositoryFragment<T> implemented(T implementation) {
return new ImplementedRepositoryFragment<T>(Optional.empty(), implementation);
return new ImplementedRepositoryFragment<>((Class<T>) null, implementation);
}
/**
@@ -64,7 +66,7 @@ public interface RepositoryFragment<T> {
* @return
*/
static <T> RepositoryFragment<T> implemented(Class<T> interfaceClass, T implementation) {
return new ImplementedRepositoryFragment<>(Optional.of(interfaceClass), implementation);
return new ImplementedRepositoryFragment<>(interfaceClass, implementation);
}
/**
@@ -134,7 +136,7 @@ public interface RepositoryFragment<T> {
@Override
public RepositoryFragment<T> withImplementation(T implementation) {
return new ImplementedRepositoryFragment<>(Optional.of(interfaceOrImplementation), implementation);
return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation);
}
@Override
@@ -164,9 +166,20 @@ public interface RepositoryFragment<T> {
class ImplementedRepositoryFragment<T> implements RepositoryFragment<T> {
private final Optional<Class<T>> interfaceClass;
private final @Nullable Class<T> interfaceClass;
private final T implementation;
private final Optional<T> optionalImplementation;
/**
* Creates a new {@link ImplementedRepositoryFragment} for the given interface class and implementation.
*
* @param interfaceClass
* @param implementation
* @deprecated since 3.4 - use {@link ImplementedRepositoryFragment(Class, Object)} instead.
*/
@Deprecated(since = "3.4", forRemoval = true)
public ImplementedRepositoryFragment(Optional<Class<T>> interfaceClass, T implementation) {
this(interfaceClass.orElse(null), implementation);
}
/**
* Creates a new {@link ImplementedRepositoryFragment} for the given interface class and implementation.
@@ -174,37 +187,37 @@ public interface RepositoryFragment<T> {
* @param interfaceClass must not be {@literal null}.
* @param implementation must not be {@literal null}.
*/
public ImplementedRepositoryFragment(Optional<Class<T>> interfaceClass, T implementation) {
public ImplementedRepositoryFragment(@Nullable Class<T> interfaceClass, T implementation) {
Assert.notNull(interfaceClass, "Interface class must not be null");
Assert.notNull(implementation, "Implementation object must not be null");
interfaceClass.ifPresent(it -> {
if (interfaceClass != null) {
Assert.isTrue(ClassUtils.isAssignableValue(it, implementation),
() -> String.format("Fragment implementation %s does not implement %s",
ClassUtils.getQualifiedName(implementation.getClass()), ClassUtils.getQualifiedName(it)));
});
Assert.isTrue(ClassUtils.isAssignableValue(interfaceClass, implementation),
() -> "Fragment implementation %s does not implement %s".formatted(
ClassUtils.getQualifiedName(implementation.getClass()),
ClassUtils.getQualifiedName(interfaceClass)));
}
this.interfaceClass = interfaceClass;
this.implementation = implementation;
this.optionalImplementation = Optional.of(implementation);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public Class<?> getSignatureContributor() {
return interfaceClass.orElseGet(() -> {
if(implementation instanceof Class type) {
return type;
}
return (Class<T>) implementation.getClass();
});
if (interfaceClass != null) {
return interfaceClass;
}
if (implementation instanceof Class<?> type) {
return type;
}
return implementation.getClass();
}
@Override
public Optional<T> getImplementation() {
return optionalImplementation;
return Optional.of(implementation);
}
@Override
@@ -216,7 +229,7 @@ public interface RepositoryFragment<T> {
public String toString() {
return String.format("ImplementedRepositoryFragment %s%s",
interfaceClass.map(ClassUtils::getShortName).map(it -> it + ":").orElse(""),
interfaceClass != null ? (ClassUtils.getShortName(interfaceClass) + ":") : "",
ClassUtils.getShortName(implementation.getClass()));
}
@@ -235,18 +248,13 @@ public interface RepositoryFragment<T> {
return false;
}
if (!ObjectUtils.nullSafeEquals(implementation, that.implementation)) {
return false;
}
return ObjectUtils.nullSafeEquals(optionalImplementation, that.optionalImplementation);
return ObjectUtils.nullSafeEquals(implementation, that.implementation);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(interfaceClass);
result = 31 * result + ObjectUtils.nullSafeHashCode(implementation);
result = 31 * result + ObjectUtils.nullSafeHashCode(optionalImplementation);
return result;
}
}

View File

@@ -19,7 +19,7 @@ package org.springframework.data.repository.core.support;
* Marker for repository fragment implementation that intend to access repository method invocation metadata.
* <p>
* Note that this is a marker interface in the style of {@link java.io.Serializable}, semantically applying to a
* fragment implementation class rather. In other words, this marker applies to a particular repository composition that
* fragment implementation class. In other words, this marker applies to a particular repository composition that
* enables metadata access for the repository proxy when the composition contain fragments implementing this interface.
* <p>
* Ideally, in a repository composition only the fragment implementation uses this interface while the fragment