1 /*
2  * Copyright 2015-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.projection;
17
18 import java.lang.invoke.MethodHandle;
19 import java.lang.invoke.MethodHandles;
20 import java.lang.invoke.MethodHandles.Lookup;
21 import java.lang.invoke.MethodType;
22 import java.lang.reflect.Constructor;
23 import java.lang.reflect.Method;
24 import java.lang.reflect.Modifier;
25 import java.util.Map;
26
27 import org.aopalliance.intercept.MethodInterceptor;
28 import org.aopalliance.intercept.MethodInvocation;
29
30 import org.springframework.aop.ProxyMethodInvocation;
31 import org.springframework.data.util.Lazy;
32 import org.springframework.lang.Nullable;
33 import org.springframework.util.ConcurrentReferenceHashMap;
34 import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
35 import org.springframework.util.ReflectionUtils;
36
37 /**
38  * Method interceptor to invoke default methods on the repository proxy.
39  *
40  * @author Oliver Gierke
41  * @author Jens Schauder
42  * @author Mark Paluch
43  */

44 public class DefaultMethodInvokingMethodInterceptor implements MethodInterceptor {
45
46     private final MethodHandleLookup methodHandleLookup = MethodHandleLookup.getMethodHandleLookup();
47     private final Map<Method, MethodHandle> methodHandleCache = new ConcurrentReferenceHashMap<>(10, ReferenceType.WEAK);
48
49     /**
50      * Returns whether the {@code interfaceClass} declares {@link Method#isDefault() default methods}.
51      *
52      * @param interfaceClass the {@link Class} to inspect.
53      * @return {@literal trueif {@code interfaceClass} declares a default method.
54      * @since 2.2
55      */

56     public static boolean hasDefaultMethods(Class<?> interfaceClass) {
57
58         Method[] methods = ReflectionUtils.getAllDeclaredMethods(interfaceClass);
59
60         for (Method method : methods) {
61             if (method.isDefault()) {
62                 return true;
63             }
64         }
65
66         return false;
67     }
68
69     /*
70      * (non-Javadoc)
71      * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
72      */

73     @Nullable
74     @Override
75     public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
76
77         Method method = invocation.getMethod();
78
79         if (!method.isDefault()) {
80             return invocation.proceed();
81         }
82
83         Object[] arguments = invocation.getArguments();
84         Object proxy = ((ProxyMethodInvocation) invocation).getProxy();
85
86         return getMethodHandle(method).bindTo(proxy).invokeWithArguments(arguments);
87     }
88
89     private MethodHandle getMethodHandle(Method method) throws Exception {
90
91         MethodHandle handle = methodHandleCache.get(method);
92
93         if (handle == null) {
94
95             handle = methodHandleLookup.lookup(method);
96             methodHandleCache.put(method, handle);
97         }
98
99         return handle;
100     }
101
102     /**
103      * Strategies for {@link MethodHandle} lookup.
104      *
105      * @since 2.0
106      */

107     enum MethodHandleLookup {
108
109         /**
110          * Encapsulated {@link MethodHandle} lookup working on Java 9.
111          */

112         ENCAPSULATED {
113
114             private final @Nullable Method privateLookupIn = ReflectionUtils.findMethod(MethodHandles.class,
115                     "privateLookupIn", Class.class, Lookup.class);
116
117             /*
118              * (non-Javadoc)
119              * @see org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.MethodHandleLookup#lookup(java.lang.reflect.Method)
120              */

121             @Override
122             MethodHandle lookup(Method method) throws ReflectiveOperationException {
123
124                 if (privateLookupIn == null) {
125                     throw new IllegalStateException("Could not obtain MethodHandles.privateLookupIn!");
126                 }
127
128                 return doLookup(method, getLookup(method.getDeclaringClass(), privateLookupIn));
129             }
130
131             /*
132              * (non-Javadoc)
133              * @see org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.MethodHandleLookup#isAvailable()
134              */

135             @Override
136             boolean isAvailable() {
137                 return privateLookupIn != null;
138             }
139
140             private Lookup getLookup(Class<?> declaringClass, Method privateLookupIn) {
141
142                 Lookup lookup = MethodHandles.lookup();
143
144                 try {
145                     return (Lookup) privateLookupIn.invoke(MethodHandles.class, declaringClass, lookup);
146                 } catch (ReflectiveOperationException e) {
147                     return lookup;
148                 }
149             }
150         },
151
152         /**
153          * Open (via reflection construction of {@link MethodHandles.Lookup}) method handle lookup. Works with Java 8 and
154          * with Java 9 permitting illegal access.
155          */

156         OPEN {
157
158             private final Lazy<Constructor<Lookup>> constructor = Lazy.of(MethodHandleLookup::getLookupConstructor);
159
160             /*
161              * (non-Javadoc)
162              * @see org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.MethodHandleLookup#lookup(java.lang.reflect.Method)
163              */

164             @Override
165             MethodHandle lookup(Method method) throws ReflectiveOperationException {
166
167                 if (!isAvailable()) {
168                     throw new IllegalStateException("Could not obtain MethodHandles.lookup constructor!");
169                 }
170
171                 Constructor<Lookup> constructor = this.constructor.get();
172
173                 return constructor.newInstance(method.getDeclaringClass()).unreflectSpecial(method, method.getDeclaringClass());
174             }
175
176             /*
177              * (non-Javadoc)
178              * @see org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.MethodHandleLookup#isAvailable()
179              */

180             @Override
181             boolean isAvailable() {
182                 return constructor.orElse(null) != null;
183             }
184         },
185
186         /**
187          * Fallback {@link MethodHandle} lookup using {@link MethodHandles#lookup() public lookup}.
188          *
189          * @since 2.1
190          */

191         FALLBACK {
192
193             /*
194              * (non-Javadoc)
195              * @see org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.MethodHandleLookup#lookup(java.lang.reflect.Method)
196              */

197             @Override
198             MethodHandle lookup(Method method) throws ReflectiveOperationException {
199                 return doLookup(method, MethodHandles.lookup());
200             }
201
202             /*
203              * (non-Javadoc)
204              * @see org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.MethodHandleLookup#isAvailable()
205              */

206             @Override
207             boolean isAvailable() {
208                 return true;
209             }
210         };
211
212         private static MethodHandle doLookup(Method method, Lookup lookup)
213                 throws NoSuchMethodException, IllegalAccessException {
214
215             MethodType methodType = MethodType.methodType(method.getReturnType(), method.getParameterTypes());
216
217             if (Modifier.isStatic(method.getModifiers())) {
218                 return lookup.findStatic(method.getDeclaringClass(), method.getName(), methodType);
219             }
220
221             return lookup.findSpecial(method.getDeclaringClass(), method.getName(), methodType, method.getDeclaringClass());
222         }
223
224         /**
225          * Lookup a {@link MethodHandle} given {@link Method} to look up.
226          *
227          * @param method must not be {@literal null}.
228          * @return the method handle.
229          * @throws ReflectiveOperationException
230          */

231         abstract MethodHandle lookup(Method method) throws ReflectiveOperationException;
232
233         /**
234          * @return {@literal trueif the lookup is available.
235          */

236         abstract boolean isAvailable();
237
238         /**
239          * Obtain the first available {@link MethodHandleLookup}.
240          *
241          * @return the {@link MethodHandleLookup}
242          * @throws IllegalStateException if no {@link MethodHandleLookup} is available.
243          */

244         public static MethodHandleLookup getMethodHandleLookup() {
245
246             for (MethodHandleLookup it : MethodHandleLookup.values()) {
247
248                 if (it.isAvailable()) {
249                     return it;
250                 }
251             }
252
253             throw new IllegalStateException("No MethodHandleLookup available!");
254         }
255
256         @Nullable
257         private static Constructor<Lookup> getLookupConstructor() {
258
259             try {
260
261                 Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class);
262                 ReflectionUtils.makeAccessible(constructor);
263
264                 return constructor;
265             } catch (Exception ex) {
266
267                 // this is the signal that we are on Java 9 (encapsulated) and can't use the accessible constructor approach.
268                 if (ex.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
269                     return null;
270                 }
271
272                 throw new IllegalStateException(ex);
273             }
274         }
275     }
276 }
277