Polishing.
Update documentation. Additional logging for repository bootstrap procedure. Limit usage of Optional in RepositoryFragment. Original Pull Request: #3145
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user