Skip to content

Add RepositoryMetadataAccess interface #3145

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 5 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-METADATA-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 @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
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<T> implements SearchExtension<T>, 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;
Expand Down Expand Up @@ -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;
Expand All @@ -364,7 +387,6 @@ interface MovieRepository extends CrudRepository<Movie, String>, SearchExtension

}
----
====

[[repositories.customize-base-repository]]
== Customize the Base Repository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,16 @@ public RepositoryFactorySupport() {
* retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain
* repository information.
* <p>
* 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.
* <p>
* 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;
Expand Down Expand Up @@ -342,10 +348,16 @@ public <T> T getRepository(Class<T> 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);
}
Expand All @@ -365,6 +377,9 @@ public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fra
}

if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Register DefaultMethodInvokingMethodInterceptor for %s…", repositoryInterface.getName()));
}
result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
}

Expand Down Expand Up @@ -616,6 +631,23 @@ private Lazy<ProjectionFactory> 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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,6 +42,7 @@
* Fragments are immutable.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.0
* @see RepositoryComposition
*/
Expand All @@ -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);
}

/**
Expand All @@ -64,7 +66,7 @@ static <T> RepositoryFragment<T> implemented(T implementation) {
* @return
*/
static <T> RepositoryFragment<T> implemented(Class<T> interfaceClass, T implementation) {
return new ImplementedRepositoryFragment<>(Optional.of(interfaceClass), implementation);
return new ImplementedRepositoryFragment<>(interfaceClass, implementation);
}

/**
Expand Down Expand Up @@ -134,7 +136,7 @@ public Class<?> getSignatureContributor() {

@Override
public RepositoryFragment<T> withImplementation(T implementation) {
return new ImplementedRepositoryFragment<>(Optional.of(interfaceOrImplementation), implementation);
return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation);
}

@Override
Expand Down Expand Up @@ -164,47 +166,57 @@ public int hashCode() {

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")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a forRemoval=true so that we clean up the API with Spring Data 4.0

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.
*
* @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),
() -> 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<T>) implementation.getClass();
});
if(interfaceClass != null) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Formatting

return interfaceClass;
}

if(implementation instanceof Class<?> type) {
return type;
}
return implementation.getClass();
}

@Override
public Optional<T> getImplementation() {
return optionalImplementation;
return Optional.of(implementation);
}

@Override
Expand All @@ -216,7 +228,7 @@ public RepositoryFragment<T> 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) + ";") : "",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: A slight change in : vs. ;

ClassUtils.getShortName(implementation.getClass()));
}

Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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 {

}
Loading