1 /*
2  * Copyright 2011-2020 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.jpa.repository.support;
17
18 import java.lang.reflect.Method;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.Optional;
24 import java.util.Set;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.ConcurrentMap;
27 import java.util.function.Predicate;
28
29 import javax.persistence.LockModeType;
30 import javax.persistence.QueryHint;
31
32 import org.aopalliance.intercept.MethodInterceptor;
33 import org.aopalliance.intercept.MethodInvocation;
34
35 import org.springframework.aop.TargetSource;
36 import org.springframework.aop.framework.ProxyFactory;
37 import org.springframework.beans.factory.BeanClassLoaderAware;
38 import org.springframework.core.NamedThreadLocal;
39 import org.springframework.core.annotation.AnnotatedElementUtils;
40 import org.springframework.core.annotation.AnnotationUtils;
41 import org.springframework.data.jpa.repository.EntityGraph;
42 import org.springframework.data.jpa.repository.Lock;
43 import org.springframework.data.jpa.repository.QueryHints;
44 import org.springframework.data.repository.core.RepositoryInformation;
45 import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor;
46 import org.springframework.lang.Nullable;
47 import org.springframework.transaction.support.TransactionSynchronizationManager;
48 import org.springframework.util.Assert;
49 import org.springframework.util.ClassUtils;
50 import org.springframework.util.ReflectionUtils;
51
52 /**
53  * {@link RepositoryProxyPostProcessor} that sets up interceptors to read metadata information from the invoked method.
54  * This is necessary to allow redeclaration of CRUD methods in repository interfaces and configure locking information
55  * or query hints on them.
56  *
57  * @author Oliver Gierke
58  * @author Thomas Darimont
59  * @author Christoph Strobl
60  * @author Mark Paluch
61  * @author Jens Schauder
62  */

63 class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, BeanClassLoaderAware {
64
65     private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
66
67     /*
68      * (non-Javadoc)
69      * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
70      */

71     @Override
72     public void setBeanClassLoader(ClassLoader classLoader) {
73         this.classLoader = classLoader;
74     }
75
76     /*
77      * (non-Javadoc)
78      * @see org.springframework.data.repository.core.support.RepositoryProxyPostProcessor#postProcess(org.springframework.aop.framework.ProxyFactory, org.springframework.data.repository.core.RepositoryInformation)
79      */

80     @Override
81     public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) {
82         factory.addAdvice(new CrudMethodMetadataPopulatingMethodInterceptor(repositoryInformation));
83     }
84
85     /**
86      * Returns a {@link CrudMethodMetadata} proxy that will lookup the actual target object by obtaining a thread bound
87      * instance from the {@link TransactionSynchronizationManager} later.
88      */

89     CrudMethodMetadata getCrudMethodMetadata() {
90
91         ProxyFactory factory = new ProxyFactory();
92
93         factory.addInterface(CrudMethodMetadata.class);
94         factory.setTargetSource(new ThreadBoundTargetSource());
95
96         return (CrudMethodMetadata) factory.getProxy(this.classLoader);
97     }
98
99     /**
100      * {@link MethodInterceptor} to build and cache {@link DefaultCrudMethodMetadata} instances for the invoked methods.
101      * Will bind the found information to a {@link TransactionSynchronizationManager} for later lookup.
102      *
103      * @see DefaultCrudMethodMetadata
104      * @author Oliver Gierke
105      * @author Thomas Darimont
106      */

107     static class CrudMethodMetadataPopulatingMethodInterceptor implements MethodInterceptor {
108
109         private static final ThreadLocal<MethodInvocation> currentInvocation = new NamedThreadLocal<>(
110                 "Current AOP method invocation");
111
112         private final ConcurrentMap<Method, CrudMethodMetadata> metadataCache = new ConcurrentHashMap<>();
113         private final Set<Method> implementations = new HashSet<>();
114
115         CrudMethodMetadataPopulatingMethodInterceptor(RepositoryInformation repositoryInformation) {
116
117             ReflectionUtils.doWithMethods(repositoryInformation.getRepositoryInterface(), implementations::add,
118                     method -> !repositoryInformation.isQueryMethod(method));
119         }
120
121         /**
122          * Return the AOP Alliance {@link MethodInvocation} object associated with the current invocation.
123          *
124          * @return the invocation object associated with the current invocation.
125          * @throws IllegalStateException if there is no AOP invocation in progress, or if the
126          *           {@link CrudMethodMetadataPopulatingMethodInterceptor} was not added to this interceptor chain.
127          */

128         static MethodInvocation currentInvocation() throws IllegalStateException {
129
130             MethodInvocation mi = currentInvocation.get();
131
132             if (mi == null)
133                 throw new IllegalStateException(
134                         "No MethodInvocation found: Check that an AOP invocation is in progress, and that the "
135                                 + "CrudMethodMetadataPopulatingMethodInterceptor is upfront in the interceptor chain.");
136             return mi;
137         }
138
139         /*
140          * (non-Javadoc)
141          * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
142          */

143         @Override
144         public Object invoke(MethodInvocation invocation) throws Throwable {
145
146             Method method = invocation.getMethod();
147
148             if (!implementations.contains(method)) {
149                 return invocation.proceed();
150             }
151
152             MethodInvocation oldInvocation = currentInvocation.get();
153             currentInvocation.set(invocation);
154
155             try {
156
157                 CrudMethodMetadata metadata = (CrudMethodMetadata) TransactionSynchronizationManager.getResource(method);
158
159                 if (metadata != null) {
160                     return invocation.proceed();
161                 }
162
163                 CrudMethodMetadata methodMetadata = metadataCache.get(method);
164
165                 if (methodMetadata == null) {
166
167                     methodMetadata = new DefaultCrudMethodMetadata(method);
168                     CrudMethodMetadata tmp = metadataCache.putIfAbsent(method, methodMetadata);
169
170                     if (tmp != null) {
171                         methodMetadata = tmp;
172                     }
173                 }
174
175                 TransactionSynchronizationManager.bindResource(method, methodMetadata);
176
177                 try {
178                     return invocation.proceed();
179                 } finally {
180                     TransactionSynchronizationManager.unbindResource(method);
181                 }
182             } finally {
183                 currentInvocation.set(oldInvocation);
184             }
185         }
186     }
187
188     /**
189      * Default implementation of {@link CrudMethodMetadata} that will inspect the backing method for annotations.
190      *
191      * @author Oliver Gierke
192      * @author Thomas Darimont
193      */

194     private static class DefaultCrudMethodMetadata implements CrudMethodMetadata {
195
196         private final @Nullable LockModeType lockModeType;
197         private final Map<String, Object> queryHints;
198         private final Map<String, Object> getQueryHintsForCount;
199         private final Optional<EntityGraph> entityGraph;
200         private final Method method;
201
202         /**
203          * Creates a new {@link DefaultCrudMethodMetadata} for the given {@link Method}.
204          *
205          * @param method must not be {@literal null}.
206          */

207         DefaultCrudMethodMetadata(Method method) {
208
209             Assert.notNull(method, "Method must not be null!");
210
211             this.lockModeType = findLockModeType(method);
212             this.queryHints = findQueryHints(method, it -> true);
213             this.getQueryHintsForCount = findQueryHints(method, QueryHints::forCounting);
214             this.entityGraph = findEntityGraph(method);
215             this.method = method;
216         }
217
218         private static Optional<EntityGraph> findEntityGraph(Method method) {
219             return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class));
220         }
221
222         @Nullable
223         private static LockModeType findLockModeType(Method method) {
224
225             Lock annotation = AnnotatedElementUtils.findMergedAnnotation(method, Lock.class);
226             return annotation == null ? null : (LockModeType) AnnotationUtils.getValue(annotation);
227         }
228
229         private static Map<String, Object> findQueryHints(Method method, Predicate<QueryHints> annotationFilter) {
230
231             Map<String, Object> queryHints = new HashMap<>();
232             QueryHints queryHintsAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, QueryHints.class);
233
234             if (queryHintsAnnotation != null && annotationFilter.test(queryHintsAnnotation)) {
235
236                 for (QueryHint hint : queryHintsAnnotation.value()) {
237                     queryHints.put(hint.name(), hint.value());
238                 }
239             }
240
241             QueryHint queryHintAnnotation = AnnotationUtils.findAnnotation(method, QueryHint.class);
242
243             if (queryHintAnnotation != null) {
244                 queryHints.put(queryHintAnnotation.name(), queryHintAnnotation.value());
245             }
246
247             return Collections.unmodifiableMap(queryHints);
248         }
249
250         /*
251          * (non-Javadoc)
252          * @see org.springframework.data.jpa.repository.support.CrudMethodMetadata#getLockModeType()
253          */

254         @Nullable
255         @Override
256         public LockModeType getLockModeType() {
257             return lockModeType;
258         }
259
260         /*
261          * (non-Javadoc)
262          * @see org.springframework.data.jpa.repository.support.CrudMethodMetadata#getQueryHints()
263          */

264         @Override
265         public Map<String, Object> getQueryHints() {
266             return queryHints;
267         }
268
269         /*
270          * (non-Javadoc)
271          * @see org.springframework.data.jpa.repository.support.CrudMethodMetadata#getQueryHintsForCount()
272          */

273         @Override
274         public Map<String, Object> getQueryHintsForCount() {
275             return getQueryHintsForCount;
276         }
277
278         /*
279          * (non-Javadoc)
280          * @see org.springframework.data.jpa.repository.support.CrudMethodMetadata#getEntityGraph()
281          */

282         @Override
283         public Optional<EntityGraph> getEntityGraph() {
284             return entityGraph;
285         }
286
287         /*
288          * (non-Javadoc)
289          * @see org.springframework.data.jpa.repository.support.CrudMethodMetadata#getMethod()
290          */

291         @Override
292         public Method getMethod() {
293             return method;
294         }
295     }
296
297     private static class ThreadBoundTargetSource implements TargetSource {
298
299         /*
300          * (non-Javadoc)
301          * @see org.springframework.aop.TargetSource#getTargetClass()
302          */

303         @Override
304         public Class<?> getTargetClass() {
305             return CrudMethodMetadata.class;
306         }
307
308         /*
309          * (non-Javadoc)
310          * @see org.springframework.aop.TargetSource#isStatic()
311          */

312         @Override
313         public boolean isStatic() {
314             return false;
315         }
316
317         /*
318          * (non-Javadoc)
319          * @see org.springframework.aop.TargetSource#getTarget()
320          */

321         @Override
322         public Object getTarget() {
323
324             MethodInvocation invocation = CrudMethodMetadataPopulatingMethodInterceptor.currentInvocation();
325             return TransactionSynchronizationManager.getResource(invocation.getMethod());
326         }
327
328         /*
329          * (non-Javadoc)
330          * @see org.springframework.aop.TargetSource#releaseTarget(java.lang.Object)
331          */

332         @Override
333         public void releaseTarget(Object target) {}
334     }
335 }
336