Skip to content

Commit 5d25645

Browse files
mp911dechristophstrobl
authored andcommitted
Add CrudMethodMetadata to support ReadPreference annotations on overridden base repository methods.
See: #2971 Original Pull Request: #4503
1 parent 74b07e5 commit 5d25645

11 files changed

+632
-150
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2023 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.mongodb.repository.support;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.Optional;
20+
21+
import com.mongodb.ReadPreference;
22+
23+
/**
24+
* Interface to abstract {@link CrudMethodMetadata} that provide the {@link ReadPreference} to be used for query
25+
* execution.
26+
*
27+
* @author Mark Paluch
28+
* @since 4.2
29+
*/
30+
public interface CrudMethodMetadata {
31+
32+
/**
33+
* Returns the {@link ReadPreference} to be used.
34+
*
35+
* @return the {@link ReadPreference} to be used.
36+
*/
37+
Optional<ReadPreference> getReadPreference();
38+
39+
/**
40+
* Returns the {@link Method} that this metadata applies to.
41+
*
42+
* @return the {@link Method} that this metadata applies to.
43+
*/
44+
Method getMethod();
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
* Copyright 2023 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.mongodb.repository.support;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.HashSet;
20+
import java.util.Optional;
21+
import java.util.Set;
22+
import java.util.concurrent.ConcurrentHashMap;
23+
import java.util.concurrent.ConcurrentMap;
24+
25+
import org.aopalliance.intercept.MethodInterceptor;
26+
import org.aopalliance.intercept.MethodInvocation;
27+
import org.springframework.aop.TargetSource;
28+
import org.springframework.aop.framework.ProxyFactory;
29+
import org.springframework.beans.factory.BeanClassLoaderAware;
30+
import org.springframework.core.NamedThreadLocal;
31+
import org.springframework.core.annotation.AnnotatedElementUtils;
32+
import org.springframework.data.repository.core.RepositoryInformation;
33+
import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor;
34+
import org.springframework.lang.Nullable;
35+
import org.springframework.transaction.support.TransactionSynchronizationManager;
36+
import org.springframework.util.Assert;
37+
import org.springframework.util.ClassUtils;
38+
import org.springframework.util.ReflectionUtils;
39+
40+
import com.mongodb.ReadPreference;
41+
42+
/**
43+
* {@link RepositoryProxyPostProcessor} that sets up interceptors to read metadata information from the invoked method.
44+
* This is necessary to allow redeclaration of CRUD methods in repository interfaces and configure read preference
45+
* information or query hints on them.
46+
*
47+
* @author Mark Paluch
48+
*/
49+
class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, BeanClassLoaderAware {
50+
51+
private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
52+
53+
@Override
54+
public void setBeanClassLoader(ClassLoader classLoader) {
55+
this.classLoader = classLoader;
56+
}
57+
58+
@Override
59+
public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) {
60+
factory.addAdvice(new CrudMethodMetadataPopulatingMethodInterceptor(repositoryInformation));
61+
}
62+
63+
/**
64+
* Returns a {@link CrudMethodMetadata} proxy that will lookup the actual target object by obtaining a thread bound
65+
* instance from the {@link TransactionSynchronizationManager} later.
66+
*/
67+
CrudMethodMetadata getCrudMethodMetadata() {
68+
69+
ProxyFactory factory = new ProxyFactory();
70+
71+
factory.addInterface(CrudMethodMetadata.class);
72+
factory.setTargetSource(new ThreadBoundTargetSource());
73+
74+
return (CrudMethodMetadata) factory.getProxy(this.classLoader);
75+
}
76+
77+
/**
78+
* {@link MethodInterceptor} to build and cache {@link DefaultCrudMethodMetadata} instances for the invoked methods.
79+
* Will bind the found information to a {@link TransactionSynchronizationManager} for later lookup.
80+
*
81+
* @see DefaultCrudMethodMetadata
82+
*/
83+
static class CrudMethodMetadataPopulatingMethodInterceptor implements MethodInterceptor {
84+
85+
private static final ThreadLocal<MethodInvocation> currentInvocation = new NamedThreadLocal<>(
86+
"Current AOP method invocation");
87+
88+
private final ConcurrentMap<Method, CrudMethodMetadata> metadataCache = new ConcurrentHashMap<>();
89+
private final Set<Method> implementations = new HashSet<>();
90+
91+
CrudMethodMetadataPopulatingMethodInterceptor(RepositoryInformation repositoryInformation) {
92+
93+
ReflectionUtils.doWithMethods(repositoryInformation.getRepositoryInterface(), implementations::add,
94+
method -> !repositoryInformation.isQueryMethod(method));
95+
}
96+
97+
/**
98+
* Return the AOP Alliance {@link MethodInvocation} object associated with the current invocation.
99+
*
100+
* @return the invocation object associated with the current invocation.
101+
* @throws IllegalStateException if there is no AOP invocation in progress, or if the
102+
* {@link CrudMethodMetadataPopulatingMethodInterceptor} was not added to this interceptor chain.
103+
*/
104+
static MethodInvocation currentInvocation() throws IllegalStateException {
105+
106+
MethodInvocation mi = currentInvocation.get();
107+
108+
if (mi == null)
109+
throw new IllegalStateException(
110+
"No MethodInvocation found: Check that an AOP invocation is in progress, and that the "
111+
+ "CrudMethodMetadataPopulatingMethodInterceptor is upfront in the interceptor chain.");
112+
return mi;
113+
}
114+
115+
@Override
116+
public Object invoke(MethodInvocation invocation) throws Throwable {
117+
118+
Method method = invocation.getMethod();
119+
120+
if (!implementations.contains(method)) {
121+
return invocation.proceed();
122+
}
123+
124+
MethodInvocation oldInvocation = currentInvocation.get();
125+
currentInvocation.set(invocation);
126+
127+
try {
128+
129+
CrudMethodMetadata metadata = (CrudMethodMetadata) TransactionSynchronizationManager.getResource(method);
130+
131+
if (metadata != null) {
132+
return invocation.proceed();
133+
}
134+
135+
CrudMethodMetadata methodMetadata = metadataCache.get(method);
136+
137+
if (methodMetadata == null) {
138+
139+
methodMetadata = new DefaultCrudMethodMetadata(method);
140+
CrudMethodMetadata tmp = metadataCache.putIfAbsent(method, methodMetadata);
141+
142+
if (tmp != null) {
143+
methodMetadata = tmp;
144+
}
145+
}
146+
147+
TransactionSynchronizationManager.bindResource(method, methodMetadata);
148+
149+
try {
150+
return invocation.proceed();
151+
} finally {
152+
TransactionSynchronizationManager.unbindResource(method);
153+
}
154+
} finally {
155+
currentInvocation.set(oldInvocation);
156+
}
157+
}
158+
}
159+
160+
/**
161+
* Default implementation of {@link CrudMethodMetadata} that will inspect the backing method for annotations.
162+
*/
163+
static class DefaultCrudMethodMetadata implements CrudMethodMetadata {
164+
165+
private final Optional<ReadPreference> readPreference;
166+
private final Method method;
167+
168+
/**
169+
* Creates a new {@link DefaultCrudMethodMetadata} for the given {@link Method}.
170+
*
171+
* @param method must not be {@literal null}.
172+
*/
173+
DefaultCrudMethodMetadata(Method method) {
174+
175+
Assert.notNull(method, "Method must not be null");
176+
177+
this.readPreference = findReadPreference(method);
178+
this.method = method;
179+
}
180+
181+
private Optional<ReadPreference> findReadPreference(Method method) {
182+
183+
org.springframework.data.mongodb.repository.ReadPreference preference = AnnotatedElementUtils
184+
.findMergedAnnotation(method, org.springframework.data.mongodb.repository.ReadPreference.class);
185+
186+
if (preference == null) {
187+
188+
preference = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(),
189+
org.springframework.data.mongodb.repository.ReadPreference.class);
190+
}
191+
192+
if (preference == null) {
193+
return Optional.empty();
194+
}
195+
196+
return Optional.of(com.mongodb.ReadPreference.valueOf(preference.value()));
197+
198+
}
199+
200+
@Override
201+
public Optional<ReadPreference> getReadPreference() {
202+
return readPreference;
203+
}
204+
205+
@Override
206+
public Method getMethod() {
207+
return method;
208+
}
209+
}
210+
211+
private static class ThreadBoundTargetSource implements TargetSource {
212+
213+
@Override
214+
public Class<?> getTargetClass() {
215+
return CrudMethodMetadata.class;
216+
}
217+
218+
@Override
219+
public boolean isStatic() {
220+
return false;
221+
}
222+
223+
@Override
224+
public Object getTarget() {
225+
226+
MethodInvocation invocation = CrudMethodMetadataPopulatingMethodInterceptor.currentInvocation();
227+
return TransactionSynchronizationManager.getResource(invocation.getMethod());
228+
}
229+
230+
@Override
231+
public void releaseTarget(Object target) {}
232+
}
233+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public String getCollectionName() {
9494
}
9595

9696
public String getIdAttribute() {
97-
return entityMetadata.getRequiredIdProperty().getName();
97+
return entityMetadata.hasIdProperty() ? entityMetadata.getRequiredIdProperty().getName() : "_id";
9898
}
9999

100100
@Override

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport {
6161

6262
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
6363

64+
private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
6465
private final MongoOperations operations;
6566
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
6667

@@ -75,6 +76,15 @@ public MongoRepositoryFactory(MongoOperations mongoOperations) {
7576

7677
this.operations = mongoOperations;
7778
this.mappingContext = mongoOperations.getConverter().getMappingContext();
79+
80+
addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
81+
}
82+
83+
@Override
84+
public void setBeanClassLoader(ClassLoader classLoader) {
85+
86+
super.setBeanClassLoader(classLoader);
87+
crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader);
7888
}
7989

8090
@Override
@@ -127,7 +137,13 @@ protected Object getTargetRepository(RepositoryInformation information) {
127137

128138
MongoEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType(),
129139
information);
130-
return getTargetRepositoryViaReflection(information, information, entityInformation, operations);
140+
Object targetRepository = getTargetRepositoryViaReflection(information, entityInformation, operations);
141+
142+
if (targetRepository instanceof SimpleMongoRepository<?, ?> repository) {
143+
repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata());
144+
}
145+
146+
return targetRepository;
131147
}
132148

133149
@Override

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup
6262

6363
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
6464

65+
private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
6566
private final ReactiveMongoOperations operations;
6667
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
6768

@@ -76,7 +77,16 @@ public ReactiveMongoRepositoryFactory(ReactiveMongoOperations mongoOperations) {
7677

7778
this.operations = mongoOperations;
7879
this.mappingContext = mongoOperations.getConverter().getMappingContext();
80+
7981
setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
82+
addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
83+
}
84+
85+
@Override
86+
public void setBeanClassLoader(ClassLoader classLoader) {
87+
88+
super.setBeanClassLoader(classLoader);
89+
crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader);
8090
}
8191

8292
@Override
@@ -114,7 +124,13 @@ protected Object getTargetRepository(RepositoryInformation information) {
114124

115125
MongoEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType(),
116126
information);
117-
return getTargetRepositoryViaReflection(information, information, entityInformation, operations);
127+
Object targetRepository = getTargetRepositoryViaReflection(information, entityInformation, operations);
128+
129+
if (targetRepository instanceof SimpleReactiveMongoRepository<?, ?> repository) {
130+
repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata());
131+
}
132+
133+
return targetRepository;
118134
}
119135

120136
@Override

0 commit comments

Comments
 (0)