diff --git a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc index 3c91ce0aa..1d85f90e4 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc @@ -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 `` 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 { List 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 implements SearchExtension { } } ---- -==== 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 implements SearchExtension, 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, SearchExtension } ---- -==== [[repositories.customize-base-repository]] == Customize the Base Repository diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 650c5aefd..0b73d66a0 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -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) { diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java index 96a8e9526..a9d98e87f 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java @@ -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 { * @return */ static RepositoryFragment implemented(T implementation) { - return new ImplementedRepositoryFragment(Optional.empty(), implementation); + return new ImplementedRepositoryFragment<>((Class) null, implementation); } /** @@ -64,7 +66,7 @@ public interface RepositoryFragment { * @return */ static RepositoryFragment implemented(Class interfaceClass, T implementation) { - return new ImplementedRepositoryFragment<>(Optional.of(interfaceClass), implementation); + return new ImplementedRepositoryFragment<>(interfaceClass, implementation); } /** @@ -134,7 +136,7 @@ public interface RepositoryFragment { @Override public RepositoryFragment withImplementation(T implementation) { - return new ImplementedRepositoryFragment<>(Optional.of(interfaceOrImplementation), implementation); + return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation); } @Override @@ -164,9 +166,20 @@ public interface RepositoryFragment { class ImplementedRepositoryFragment implements RepositoryFragment { - private final Optional> interfaceClass; + private final @Nullable Class interfaceClass; private final T implementation; - private final Optional 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> 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 { * @param interfaceClass must not be {@literal null}. * @param implementation must not be {@literal null}. */ - public ImplementedRepositoryFragment(Optional> interfaceClass, T implementation) { + public ImplementedRepositoryFragment(@Nullable Class 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) implementation.getClass(); - }); + if (interfaceClass != null) { + return interfaceClass; + } + + if (implementation instanceof Class type) { + return type; + } + return implementation.getClass(); } @Override public Optional getImplementation() { - return optionalImplementation; + return Optional.of(implementation); } @Override @@ -216,7 +229,7 @@ public interface RepositoryFragment { 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 { 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; } } diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java index 24f3353d2..fb4a7b82c 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java @@ -19,7 +19,7 @@ package org.springframework.data.repository.core.support; * Marker for repository fragment implementation that intend to access repository method invocation metadata. *

* 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. *

* Ideally, in a repository composition only the fragment implementation uses this interface while the fragment