Skip to content

Commit b8a142c

Browse files
Hacking - Allow RepositoryMethodMetadata to be accessed during invocation.
1 parent 291fbcd commit b8a142c

File tree

8 files changed

+335
-36
lines changed

8 files changed

+335
-36
lines changed

Diff for: src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc

+91
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,97 @@ XML::
238238
======
239239
====
240240

241+
[[repositories.spring-factories]]
242+
==== Registering Fragments with spring.factories
243+
244+
As already mentioned in the <<repositories.configuration>> section, the infrastructure only auto detects fragments within the repositories base package. Therefore fragments residing in another location or maybe contributed by an external archive will not be found if they do not share a common namespace.
245+
Registering fragments within `spring.factories` allows you to circumvent this restriction as explained in the following section.
246+
247+
Imagine you'd like to provide some custom search functionality usable across multiple repositories for your organization leveraging a text search index.
248+
249+
First all you need is the fragment interface. Please note the generic `<T>` parameter to align the fragment with the repository domain type.
250+
251+
====
252+
[source,java]
253+
----
254+
package com.acme.search;
255+
256+
public interface SearchExtension<T> {
257+
258+
List<T> search(String text, Limit limit);
259+
}
260+
----
261+
====
262+
263+
Let's assume the actual full text search is available via a `SearchService` that is registered as a `Bean` within the context so we can consume it in our `SearchExtension` implementation. All we need to run the search is the collection/index name and a object mapper that converts the search results into actual domain objects as sketched out below.
264+
265+
====
266+
[source,java]
267+
----
268+
package com.acme.search;
269+
270+
import org.springframework.beans.factory.annotation.Autowired;
271+
import org.springframework.data.domain.Limit;
272+
import org.springframework.data.repository.core.support.RepositoryMethodMetadata;
273+
274+
class DefaultSearchExtension<T> implements SearchExtension<T> {
275+
276+
private SearchService service;
277+
278+
DefaultSearchExtension(@Autowired SearchService service) {
279+
this.service = service;
280+
}
281+
282+
public List<T> search(String text, Limit limit) {
283+
return search(RepositoryMethodMetadata.get(), text, limit);
284+
}
285+
286+
List<T> search(RepositoryMethodMetadata metadata, String text, Limit limit) {
287+
288+
Class<T> domainType = metadata.repository().getDomainType();
289+
290+
String indexName = domainType.getSimpleName().toLowerCase();
291+
List<String> jsonResult = service.search(indexName, text, 0, limit.max());
292+
293+
return jsonResult.stream().map( ... ).collect(toList());
294+
}
295+
}
296+
----
297+
====
298+
299+
In the snipped above we use `RepositoryMethodMetadata.get()` to get hold of metadata for the actual method invocation. In doing so we can access additional information attached to the repository. In this case we use the repositories domain type to identify the name of the index to be searched.
300+
301+
[TIP]
302+
====
303+
For testing you can use `TransactionSynchronizationManager.bindResource(RepositoryMethodMetadata.class, metadata)` to provide repository method metadata.
304+
====
305+
306+
Now that we've got both, the fragments declaration and implementation we can register it in the `META-INF/spring.factories` file, package things up if needed and we're good to go.
307+
308+
====
309+
[source,properties]
310+
----
311+
com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension
312+
----
313+
====
314+
315+
To make use of the extension simply add the interface to the repository as shown below. The infrastructure will take care placing the required `RepositoryMethodMetadata` so all that
316+
317+
====
318+
[source,java]
319+
----
320+
package io.my.movies;
321+
322+
import com.acme.search.SearchExtension;
323+
import org.springframework.data.repository.CrudRepository;
324+
325+
326+
public interface MovieRepository extends CrudRepository<Movie, String>, SearchExtension<Movie> {
327+
328+
}
329+
----
330+
====
331+
241332
[[repositories.customize-base-repository]]
242333
== Customize the Base Repository
243334

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.core.support;
17+
18+
import java.lang.reflect.Method;
19+
20+
import org.springframework.data.repository.core.RepositoryMetadata;
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.transaction.support.TransactionSynchronizationManager;
23+
24+
/**
25+
* @author Christoph Strobl
26+
*/
27+
class DefaultRepositoryMethodMetadata implements RepositoryMethodMetadata {
28+
29+
private final RepositoryMetadata repositoryMetadata;
30+
private final MethodMetadata methodMetadata;
31+
32+
DefaultRepositoryMethodMetadata(RepositoryMetadata repositoryMetadata, MethodMetadata methodMetadata) {
33+
34+
this.repositoryMetadata = repositoryMetadata;
35+
this.methodMetadata = methodMetadata;
36+
}
37+
38+
static DefaultRepositoryMethodMetadata repositoryMethodMetadata(RepositoryMetadata repositoryMetadata,
39+
Method declaredMethod) {
40+
41+
return repositoryMethodMetadata(repositoryMetadata, declaredMethod, null);
42+
}
43+
44+
static DefaultRepositoryMethodMetadata repositoryMethodMetadata(RepositoryMetadata repositoryMetadata,
45+
Method declaredMethod, @Nullable Method targetMethod) {
46+
47+
return new DefaultRepositoryMethodMetadata(repositoryMetadata,
48+
new DefaultMethodMetadata(declaredMethod, targetMethod));
49+
}
50+
51+
static void bind(RepositoryMethodMetadata metadata) {
52+
TransactionSynchronizationManager.bindResource(RepositoryMethodMetadata.class, metadata);
53+
}
54+
55+
static void unbind() {
56+
TransactionSynchronizationManager.unbindResourceIfPossible(RepositoryMethodMetadata.class);
57+
}
58+
59+
@Override
60+
public RepositoryMetadata repository() {
61+
return repositoryMetadata;
62+
}
63+
64+
@Override
65+
public MethodMetadata method() {
66+
return methodMetadata;
67+
}
68+
69+
@Override
70+
public String toString() {
71+
return "DefaultRepositoryMethodMetadata{" + "repository=" + repositoryMetadata.getRepositoryInterface()
72+
+ ", domainType=" + repositoryMetadata.getDomainType() + ", invokedMethod=" + methodMetadata.declaredMethod()
73+
+ ", targetMethod=" + methodMetadata.targetMethod() + '}';
74+
}
75+
76+
record DefaultMethodMetadata(Method declaredMethod, @Nullable Method targetMethod) implements MethodMetadata {
77+
}
78+
79+
}

Diff for: src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.data.repository.core.RepositoryInformation;
3434
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster;
3535
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster;
36+
import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata;
3637
import org.springframework.data.repository.query.QueryCreationException;
3738
import org.springframework.data.repository.query.QueryLookupStrategy;
3839
import org.springframework.data.repository.query.QueryMethod;
@@ -163,7 +164,9 @@ private Object doInvoke(MethodInvocation invocation) throws Throwable {
163164
RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method);
164165

165166
if (invocationMetadata == null) {
166-
invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method));
167+
168+
DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(repositoryInformation, method);
169+
invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(repositoryMethodMetadata, queries.get(method));
167170
invocationMetadataCache.put(method, invocationMetadata);
168171
}
169172

Diff for: src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java

+13-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.repository.core.RepositoryMetadata;
3333
import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod;
3434
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster;
35+
import org.springframework.data.repository.core.support.RepositoryMethodMetadata.MethodMetadata;
3536
import org.springframework.data.repository.util.ReactiveWrapperConverters;
3637
import org.springframework.data.util.ReactiveWrappers;
3738
import org.springframework.data.util.Streamable;
@@ -281,7 +282,7 @@ Object invoke(RepositoryInvocationMulticaster listener, Method method, Object[]
281282

282283
ReflectionUtils.makeAccessible(methodToCall);
283284

284-
return fragments.invoke(metadata != null ? metadata.getRepositoryInterface() : method.getDeclaringClass(), listener,
285+
return fragments.invoke(metadata, listener,
285286
method, methodToCall, argumentConverter.apply(methodToCall, args));
286287
}
287288

@@ -369,7 +370,6 @@ public static class RepositoryFragments implements Streamable<RepositoryFragment
369370
private final List<RepositoryFragment<?>> fragments;
370371

371372
private RepositoryFragments(List<RepositoryFragment<?>> fragments) {
372-
373373
this.fragments = fragments;
374374
}
375375

@@ -382,6 +382,10 @@ public static RepositoryFragments empty() {
382382
return EMPTY;
383383
}
384384

385+
public static RepositoryFragments empty(RepositoryMetadata metadata) {
386+
return EMPTY;
387+
}
388+
385389
/**
386390
* Create {@link RepositoryFragments} from just implementation objects.
387391
*
@@ -484,7 +488,7 @@ public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) t
484488
/**
485489
* Invoke {@link Method} by resolving the fragment that implements a suitable method.
486490
*
487-
* @param repositoryInterface
491+
* @param metadata
488492
* @param listener
489493
* @param invokedMethod invoked method as per invocation on the interface.
490494
* @param methodToCall backend method that is backing the call.
@@ -493,7 +497,7 @@ public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) t
493497
* @throws Throwable
494498
*/
495499
@Nullable
496-
Object invoke(Class<?> repositoryInterface, RepositoryInvocationMulticaster listener, Method invokedMethod,
500+
Object invoke(@Nullable RepositoryMetadata metadata, RepositoryInvocationMulticaster listener, Method invokedMethod,
497501
Method methodToCall, Object[] args) throws Throwable {
498502

499503
RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment);
@@ -507,12 +511,15 @@ Object invoke(Class<?> repositoryInterface, RepositoryInvocationMulticaster list
507511

508512
if (repositoryMethodInvoker == null) {
509513

510-
repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(invokedMethod, optional.get(),
514+
DefaultRepositoryMethodMetadata repositoryMethodMetadata = DefaultRepositoryMethodMetadata.repositoryMethodMetadata(metadata, invokedMethod, methodToCall);
515+
repositoryMethodInvoker = RepositoryMethodInvoker.forFragmentMethod(repositoryMethodMetadata, optional.get(),
511516
methodToCall);
517+
512518
invocationMetadataCache.put(invokedMethod, repositoryMethodInvoker);
513519
}
514520

515-
return repositoryMethodInvoker.invoke(repositoryInterface, listener, args);
521+
Class<?> target = (metadata != null && metadata.getRepositoryInterface() != null) ? metadata.getRepositoryInterface() : invokedMethod.getDeclaringClass();
522+
return repositoryMethodInvoker.invoke(target, listener, args);
516523
}
517524

518525
private RepositoryFragment<?> findImplementationFragment(Method key) {

Diff for: src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java

+29-10
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@
1818
import kotlin.Unit;
1919
import kotlin.reflect.KFunction;
2020
import kotlinx.coroutines.flow.Flow;
21+
import org.springframework.core.type.MethodMetadata;
22+
import org.springframework.data.repository.core.CrudMethods;
23+
import org.springframework.data.repository.core.RepositoryInformation;
24+
import org.springframework.data.repository.core.RepositoryMetadata;
25+
import org.springframework.data.util.TypeInformation;
26+
import org.springframework.transaction.support.TransactionSynchronizationManager;
2127
import reactor.core.publisher.Flux;
2228
import reactor.core.publisher.Mono;
2329

2430
import java.lang.reflect.InvocationTargetException;
2531
import java.lang.reflect.Method;
2632
import java.util.Collection;
33+
import java.util.Set;
2734
import java.util.stream.Stream;
2835

2936
import org.reactivestreams.Publisher;
@@ -46,8 +53,8 @@
4653
* @author Mark Paluch
4754
* @author Christoph Strobl
4855
* @since 2.4
49-
* @see #forFragmentMethod(Method, Object, Method)
50-
* @see #forRepositoryQuery(Method, RepositoryQuery)
56+
// * @see #forFragmentMethod(Method, Object, Method)
57+
// * @see #forRepositoryQuery(Method, RepositoryQuery)
5158
* @see RepositoryQuery
5259
* @see RepositoryComposition
5360
*/
@@ -57,11 +64,13 @@ abstract class RepositoryMethodInvoker {
5764
private final Class<?> returnedType;
5865
private final Invokable invokable;
5966
private final boolean suspendedDeclaredMethod;
67+
protected RepositoryMethodMetadata repositoryMethodMetadata;
6068

6169
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
62-
protected RepositoryMethodInvoker(Method method, Invokable invokable) {
70+
protected RepositoryMethodInvoker(RepositoryMethodMetadata repositoryMethodMetadata, Invokable invokable) {
6371

64-
this.method = method;
72+
this.repositoryMethodMetadata = repositoryMethodMetadata;
73+
this.method = repositoryMethodMetadata.method().declaredMethod();
6574

6675
if (KotlinDetector.isKotlinReflectPresent()) {
6776

@@ -116,7 +125,7 @@ protected RepositoryMethodInvoker(Method method, Invokable invokable) {
116125
}
117126
}
118127

119-
static RepositoryQueryMethodInvoker forRepositoryQuery(Method declaredMethod, RepositoryQuery query) {
128+
static RepositoryQueryMethodInvoker forRepositoryQuery(RepositoryMethodMetadata declaredMethod, RepositoryQuery query) {
120129
return new RepositoryQueryMethodInvoker(declaredMethod, query);
121130
}
122131

@@ -128,7 +137,7 @@ static RepositoryQueryMethodInvoker forRepositoryQuery(Method declaredMethod, Re
128137
* @param baseMethod the base method to call on fragment {@code instance}.
129138
* @return {@link RepositoryMethodInvoker} to call a fragment {@link Method}.
130139
*/
131-
static RepositoryMethodInvoker forFragmentMethod(Method declaredMethod, Object instance, Method baseMethod) {
140+
static RepositoryMethodInvoker forFragmentMethod(RepositoryMethodMetadata declaredMethod, Object instance, Method baseMethod) {
132141
return new RepositoryFragmentMethodInvoker(declaredMethod, instance, baseMethod);
133142
}
134143

@@ -167,6 +176,10 @@ private Object doInvoke(Class<?> repositoryInterface, RepositoryInvocationMultic
167176

168177
try {
169178

179+
if(RepositoryMethodMetadata.get() == null && repositoryMethodMetadata != null) {
180+
DefaultRepositoryMethodMetadata.bind(repositoryMethodMetadata);
181+
}
182+
170183
Object result = invokable.invoke(args);
171184

172185
if (result != null && ReactiveWrappers.supports(result.getClass())) {
@@ -184,6 +197,8 @@ private Object doInvoke(Class<?> repositoryInterface, RepositoryInvocationMultic
184197
} catch (Exception e) {
185198
multicaster.notifyListeners(method, args, computeInvocationResult(invocationResultCaptor.error(e)));
186199
throw e;
200+
} finally {
201+
DefaultRepositoryMethodMetadata.unbind();
187202
}
188203
}
189204

@@ -202,7 +217,7 @@ interface Invokable {
202217
* Implementation to invoke query methods.
203218
*/
204219
private static class RepositoryQueryMethodInvoker extends RepositoryMethodInvoker {
205-
public RepositoryQueryMethodInvoker(Method method, RepositoryQuery repositoryQuery) {
220+
public RepositoryQueryMethodInvoker(RepositoryMethodMetadata method, RepositoryQuery repositoryQuery) {
206221
super(method, repositoryQuery::execute);
207222
}
208223
}
@@ -255,15 +270,19 @@ Publisher<Object> decorate(Class<?> repositoryInterface, RepositoryInvocationMul
255270
*/
256271
private static class RepositoryFragmentMethodInvoker extends RepositoryMethodInvoker {
257272

258-
public RepositoryFragmentMethodInvoker(Method declaredMethod, Object instance, Method baseClassMethod) {
259-
this(CoroutineAdapterInformation.create(declaredMethod, baseClassMethod), declaredMethod, instance,
273+
public RepositoryFragmentMethodInvoker(RepositoryMethodMetadata metadata, Object instance, Method baseClassMethod) {
274+
this(CoroutineAdapterInformation.create(metadata.method().declaredMethod(), baseClassMethod), metadata, instance,
260275
baseClassMethod);
261276
}
262277

263-
public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, Method declaredMethod,
278+
public RepositoryFragmentMethodInvoker(CoroutineAdapterInformation adapterInformation, RepositoryMethodMetadata declaredMethod,
264279
Object instance, Method baseClassMethod) {
265280
super(declaredMethod, args -> {
281+
266282
try {
283+
284+
285+
267286
if (adapterInformation.shouldAdaptReactiveToSuspended()) {
268287
/*
269288
* Kotlin suspended functions are invoked with a synthetic Continuation parameter that keeps track of the Coroutine context.

0 commit comments

Comments
 (0)