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 true} if {@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 true} if 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