/
MethodInvoker.java
337 lines (299 loc) · 10.8 KB
/
MethodInvoker.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.util;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.springframework.lang.Nullable;
/**
* Helper class that allows for specifying a method to invoke in a declarative
* fashion, be it static or non-static.
*
* <p>Usage: Specify "targetClass"/"targetMethod" or "targetObject"/"targetMethod",
* optionally specify arguments, prepare the invoker. Afterwards, you may
* invoke the method any number of times, obtaining the invocation result.
*
* @author Colin Sampaleanu
* @author Juergen Hoeller
* @since 19.02.2004
* @see #prepare
* @see #invoke
*/
public class MethodInvoker {
private static final Object[] EMPTY_ARGUMENTS = new Object[0];
@Nullable
protected Class<?> targetClass;
@Nullable
private Object targetObject;
@Nullable
private String targetMethod;
@Nullable
private String staticMethod;
@Nullable
private Object[] arguments;
/** The method we will call. */
@Nullable
private Method methodObject;
/**
* Set the target class on which to call the target method.
* Only necessary when the target method is static; else,
* a target object needs to be specified anyway.
* @see #setTargetObject
* @see #setTargetMethod
*/
public void setTargetClass(@Nullable Class<?> targetClass) {
this.targetClass = targetClass;
}
/**
* Return the target class on which to call the target method.
*/
@Nullable
public Class<?> getTargetClass() {
return this.targetClass;
}
/**
* Set the target object on which to call the target method.
* Only necessary when the target method is not static;
* else, a target class is sufficient.
* @see #setTargetClass
* @see #setTargetMethod
*/
public void setTargetObject(@Nullable Object targetObject) {
this.targetObject = targetObject;
if (targetObject != null) {
this.targetClass = targetObject.getClass();
}
}
/**
* Return the target object on which to call the target method.
*/
@Nullable
public Object getTargetObject() {
return this.targetObject;
}
/**
* Set the name of the method to be invoked.
* Refers to either a static method or a non-static method,
* depending on a target object being set.
* @see #setTargetClass
* @see #setTargetObject
*/
public void setTargetMethod(@Nullable String targetMethod) {
this.targetMethod = targetMethod;
}
/**
* Return the name of the method to be invoked.
*/
@Nullable
public String getTargetMethod() {
return this.targetMethod;
}
/**
* Set a fully qualified static method name to invoke,
* e.g. "example.MyExampleClass.myExampleMethod".
* Convenient alternative to specifying targetClass and targetMethod.
* @see #setTargetClass
* @see #setTargetMethod
*/
public void setStaticMethod(String staticMethod) {
this.staticMethod = staticMethod;
}
/**
* Set arguments for the method invocation. If this property is not set,
* or the Object array is of length 0, a method with no arguments is assumed.
*/
public void setArguments(Object... arguments) {
this.arguments = arguments;
}
/**
* Return the arguments for the method invocation.
*/
public Object[] getArguments() {
return (this.arguments != null ? this.arguments : EMPTY_ARGUMENTS);
}
/**
* Prepare the specified method.
* The method can be invoked any number of times afterwards.
* @see #getPreparedMethod
* @see #invoke
*/
public void prepare() throws ClassNotFoundException, NoSuchMethodException {
if (this.staticMethod != null) {
int lastDotIndex = this.staticMethod.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length()) {
throw new IllegalArgumentException(
"staticMethod must be a fully qualified class plus method name: " +
"e.g. 'example.MyExampleClass.myExampleMethod'");
}
String className = this.staticMethod.substring(0, lastDotIndex);
String methodName = this.staticMethod.substring(lastDotIndex + 1);
this.targetClass = resolveClassName(className);
this.targetMethod = methodName;
}
Class<?> targetClass = getTargetClass();
String targetMethod = getTargetMethod();
Assert.notNull(targetClass, "Either 'targetClass' or 'targetObject' is required");
Assert.notNull(targetMethod, "Property 'targetMethod' is required");
Object[] arguments = getArguments();
Class<?>[] argTypes = new Class<?>[arguments.length];
for (int i = 0; i < arguments.length; ++i) {
argTypes[i] = (arguments[i] != null ? arguments[i].getClass() : Object.class);
}
// Try to get the exact method first.
try {
this.methodObject = targetClass.getMethod(targetMethod, argTypes);
}
catch (NoSuchMethodException ex) {
// Just rethrow exception if we can't get any match.
this.methodObject = findMatchingMethod();
if (this.methodObject == null) {
throw ex;
}
}
}
/**
* Resolve the given class name into a Class.
* <p>The default implementations uses {@code ClassUtils.forName},
* using the thread context class loader.
* @param className the class name to resolve
* @return the resolved Class
* @throws ClassNotFoundException if the class name was invalid
*/
protected Class<?> resolveClassName(String className) throws ClassNotFoundException {
return ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
}
/**
* Find a matching method with the specified name for the specified arguments.
* @return a matching method, or {@code null} if none
* @see #getTargetClass()
* @see #getTargetMethod()
* @see #getArguments()
*/
@Nullable
protected Method findMatchingMethod() {
String targetMethod = getTargetMethod();
Object[] arguments = getArguments();
int argCount = arguments.length;
Class<?> targetClass = getTargetClass();
Assert.state(targetClass != null, "No target class set");
Method[] candidates = ReflectionUtils.getAllDeclaredMethods(targetClass);
int minTypeDiffWeight = Integer.MAX_VALUE;
Method matchingMethod = null;
for (Method candidate : candidates) {
if (candidate.getName().equals(targetMethod)) {
if (candidate.getParameterCount() == argCount) {
Class<?>[] paramTypes = candidate.getParameterTypes();
int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments);
if (typeDiffWeight < minTypeDiffWeight) {
minTypeDiffWeight = typeDiffWeight;
matchingMethod = candidate;
}
}
}
}
return matchingMethod;
}
/**
* Return the prepared Method object that will be invoked.
* <p>Can for example be used to determine the return type.
* @return the prepared Method object (never {@code null})
* @throws IllegalStateException if the invoker hasn't been prepared yet
* @see #prepare
* @see #invoke
*/
public Method getPreparedMethod() throws IllegalStateException {
if (this.methodObject == null) {
throw new IllegalStateException("prepare() must be called prior to invoke() on MethodInvoker");
}
return this.methodObject;
}
/**
* Return whether this invoker has been prepared already,
* i.e. whether it allows access to {@link #getPreparedMethod()} already.
*/
public boolean isPrepared() {
return (this.methodObject != null);
}
/**
* Invoke the specified method.
* <p>The invoker needs to have been prepared before.
* @return the object (possibly null) returned by the method invocation,
* or {@code null} if the method has a void return type
* @throws InvocationTargetException if the target method threw an exception
* @throws IllegalAccessException if the target method couldn't be accessed
* @see #prepare
*/
@Nullable
public Object invoke() throws InvocationTargetException, IllegalAccessException {
// In the static case, target will simply be {@code null}.
Object targetObject = getTargetObject();
Method preparedMethod = getPreparedMethod();
if (targetObject == null && !Modifier.isStatic(preparedMethod.getModifiers())) {
throw new IllegalArgumentException("Target method must not be non-static without a target");
}
ReflectionUtils.makeAccessible(preparedMethod);
return preparedMethod.invoke(targetObject, getArguments());
}
/**
* Algorithm that judges the match between the declared parameter types of a candidate method
* and a specific list of arguments that this method is supposed to be invoked with.
* <p>Determines a weight that represents the class hierarchy difference between types and
* arguments. A direct match, i.e. type Integer -> arg of class Integer, does not increase
* the result - all direct matches means weight 0. A match between type Object and arg of
* class Integer would increase the weight by 2, due to the superclass 2 steps up in the
* hierarchy (i.e. Object) being the last one that still matches the required type Object.
* Type Number and class Integer would increase the weight by 1 accordingly, due to the
* superclass 1 step up the hierarchy (i.e. Number) still matching the required type Number.
* Therefore, with an arg of type Integer, a constructor (Integer) would be preferred to a
* constructor (Number) which would in turn be preferred to a constructor (Object).
* All argument weights get accumulated.
* <p>Note: This is the algorithm used by MethodInvoker itself and also the algorithm
* used for constructor and factory method selection in Spring's bean container (in case
* of lenient constructor resolution which is the default for regular bean definitions).
* @param paramTypes the parameter types to match
* @param args the arguments to match
* @return the accumulated weight for all arguments
*/
public static int getTypeDifferenceWeight(Class<?>[] paramTypes, Object[] args) {
int result = 0;
for (int i = 0; i < paramTypes.length; i++) {
if (!ClassUtils.isAssignableValue(paramTypes[i], args[i])) {
return Integer.MAX_VALUE;
}
if (args[i] != null) {
Class<?> paramType = paramTypes[i];
Class<?> superClass = args[i].getClass().getSuperclass();
while (superClass != null) {
if (paramType.equals(superClass)) {
result = result + 2;
superClass = null;
}
else if (ClassUtils.isAssignable(paramType, superClass)) {
result = result + 2;
superClass = superClass.getSuperclass();
}
else {
superClass = null;
}
}
if (paramType.isInterface()) {
result = result + 1;
}
}
}
return result;
}
}