diff --git a/pom.xml b/pom.xml index c6dc5a59a0..02b03f6889 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.4.0-SNAPSHOT + 3.4.0-METADATA-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. 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 3c91ce0aa1..1d85f90e47 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. +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. -.Registering a fragment implementation through `META-INF/spring.factories` +.Expose Repository Metadata +[tabs] +====== +Marker Interface:: ++ ==== -[source,properties] +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"] ---- -com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension +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 { + + // ... +} ---- ==== -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. - +Bean Post Processor:: ++ ==== -[source,java] +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 88965a4abe..0b73d66a0f 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 @@ -127,10 +127,16 @@ public RepositoryFactorySupport() { * retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain * repository information. *

- * Default is {@literal "false"}, in order to avoid unnecessary extra interception. This means that no guarantees are provided - * that {@code RepositoryMethodContext} access will work consistently within any method of the advised object. - * - * @since 3.4.0 + * Default is {@literal "false"}, in order to avoid unnecessary extra interception. This means that no guarantees are + * provided that {@code RepositoryMethodContext} access will work consistently within any method of the advised + * object. + *

+ * Repository method metadata is also exposed if implementations within the {@link RepositoryFragments repository + * composition} implement {@link RepositoryMetadataAccess}. + * + * @since 3.4 + * @see RepositoryMethodContext + * @see RepositoryMetadataAccess */ public void setExposeMetadata(boolean exposeMetadata) { this.exposeMetadata = exposeMetadata; @@ -342,10 +348,16 @@ public T getRepository(Class repositoryInterface, RepositoryFragments fra 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) { + 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); } @@ -365,6 +377,9 @@ public T getRepository(Class repositoryInterface, RepositoryFragments fra } if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) { + if (logger.isTraceEnabled()) { + logger.trace(LogMessage.format("Register DefaultMethodInvokingMethodInterceptor for %s…", repositoryInterface.getName())); + } result.addAdvice(new DefaultMethodInvokingMethodInterceptor()); } @@ -616,6 +631,23 @@ private Lazy createProjectionFactory() { 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) { + if (fragment.getImplementation().filter(RepositoryMetadataAccess.class::isInstance).isPresent()) { + return true; + } + } + return false; + } + /** * Method interceptor that calls methods on the {@link RepositoryComposition}. * 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 96a8e9526d..45807de00e 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.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 @@ * 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 @@ static RepositoryFragment implemented(T implementation) { * @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 Class getSignatureContributor() { @Override public RepositoryFragment withImplementation(T implementation) { - return new ImplementedRepositoryFragment<>(Optional.of(interfaceOrImplementation), implementation); + return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation); } @Override @@ -164,9 +166,20 @@ public int hashCode() { 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") + 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,36 @@ class ImplementedRepositoryFragment implements 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), + () -> String.format("Fragment implementation %s does not implement %s", + 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 +228,7 @@ public RepositoryFragment withImplementation(T implementation) { 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 +247,13 @@ public boolean equals(Object o) { 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 new file mode 100644 index 0000000000..fb4a7b82ce --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java @@ -0,0 +1,34 @@ +/* + * 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.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. 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 + * interface does not. + * + * @author Mark Paluch + * @since 3.4 + * @see RepositoryMethodContext + */ +public interface RepositoryMetadataAccess { + +} diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java index 79f2862bf9..2661998cb2 100755 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java @@ -271,6 +271,27 @@ record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocati assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname"); } + @Test // GH-3090 + void capturesRepositoryMetadataWithMetadataAccess() { + + record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) { + } + + when(factory.queryOne.execute(any(Object[].class))) + .then(invocation -> new Metadata(RepositoryMethodContext.currentMethod(), + ExposeInvocationInterceptor.currentInvocation())); + + var repository = factory.getRepository(ObjectRepository.class, new RepositoryMetadataAccess() {}); + var metadataByLastname = repository.findMetadataByLastname(); + + assertThat(metadataByLastname).isInstanceOf(Metadata.class); + + Metadata metadata = (Metadata) metadataByLastname; + assertThat(metadata.context().getMethod().getName()).isEqualTo("findMetadataByLastname"); + assertThat(metadata.context().getRepository().getDomainType()).isEqualTo(Object.class); + assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname"); + } + @Test // DATACMNS-509, DATACMNS-1764 void convertsWithSameElementType() {