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