001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.util;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.lang.reflect.Modifier;
022
023import org.springframework.lang.Nullable;
024
025/**
026 * Helper class that allows for specifying a method to invoke in a declarative
027 * fashion, be it static or non-static.
028 *
029 * <p>Usage: Specify "targetClass"/"targetMethod" or "targetObject"/"targetMethod",
030 * optionally specify arguments, prepare the invoker. Afterwards, you may
031 * invoke the method any number of times, obtaining the invocation result.
032 *
033 * @author Colin Sampaleanu
034 * @author Juergen Hoeller
035 * @since 19.02.2004
036 * @see #prepare
037 * @see #invoke
038 */
039public class MethodInvoker {
040
041        private static final Object[] EMPTY_ARGUMENTS = new Object[0];
042
043
044        @Nullable
045        protected Class<?> targetClass;
046
047        @Nullable
048        private Object targetObject;
049
050        @Nullable
051        private String targetMethod;
052
053        @Nullable
054        private String staticMethod;
055
056        @Nullable
057        private Object[] arguments;
058
059        /** The method we will call. */
060        @Nullable
061        private Method methodObject;
062
063
064        /**
065         * Set the target class on which to call the target method.
066         * Only necessary when the target method is static; else,
067         * a target object needs to be specified anyway.
068         * @see #setTargetObject
069         * @see #setTargetMethod
070         */
071        public void setTargetClass(@Nullable Class<?> targetClass) {
072                this.targetClass = targetClass;
073        }
074
075        /**
076         * Return the target class on which to call the target method.
077         */
078        @Nullable
079        public Class<?> getTargetClass() {
080                return this.targetClass;
081        }
082
083        /**
084         * Set the target object on which to call the target method.
085         * Only necessary when the target method is not static;
086         * else, a target class is sufficient.
087         * @see #setTargetClass
088         * @see #setTargetMethod
089         */
090        public void setTargetObject(@Nullable Object targetObject) {
091                this.targetObject = targetObject;
092                if (targetObject != null) {
093                        this.targetClass = targetObject.getClass();
094                }
095        }
096
097        /**
098         * Return the target object on which to call the target method.
099         */
100        @Nullable
101        public Object getTargetObject() {
102                return this.targetObject;
103        }
104
105        /**
106         * Set the name of the method to be invoked.
107         * Refers to either a static method or a non-static method,
108         * depending on a target object being set.
109         * @see #setTargetClass
110         * @see #setTargetObject
111         */
112        public void setTargetMethod(@Nullable String targetMethod) {
113                this.targetMethod = targetMethod;
114        }
115
116        /**
117         * Return the name of the method to be invoked.
118         */
119        @Nullable
120        public String getTargetMethod() {
121                return this.targetMethod;
122        }
123
124        /**
125         * Set a fully qualified static method name to invoke,
126         * e.g. "example.MyExampleClass.myExampleMethod".
127         * Convenient alternative to specifying targetClass and targetMethod.
128         * @see #setTargetClass
129         * @see #setTargetMethod
130         */
131        public void setStaticMethod(String staticMethod) {
132                this.staticMethod = staticMethod;
133        }
134
135        /**
136         * Set arguments for the method invocation. If this property is not set,
137         * or the Object array is of length 0, a method with no arguments is assumed.
138         */
139        public void setArguments(Object... arguments) {
140                this.arguments = arguments;
141        }
142
143        /**
144         * Return the arguments for the method invocation.
145         */
146        public Object[] getArguments() {
147                return (this.arguments != null ? this.arguments : EMPTY_ARGUMENTS);
148        }
149
150
151        /**
152         * Prepare the specified method.
153         * The method can be invoked any number of times afterwards.
154         * @see #getPreparedMethod
155         * @see #invoke
156         */
157        public void prepare() throws ClassNotFoundException, NoSuchMethodException {
158                if (this.staticMethod != null) {
159                        int lastDotIndex = this.staticMethod.lastIndexOf('.');
160                        if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length()) {
161                                throw new IllegalArgumentException(
162                                                "staticMethod must be a fully qualified class plus method name: " +
163                                                "e.g. 'example.MyExampleClass.myExampleMethod'");
164                        }
165                        String className = this.staticMethod.substring(0, lastDotIndex);
166                        String methodName = this.staticMethod.substring(lastDotIndex + 1);
167                        this.targetClass = resolveClassName(className);
168                        this.targetMethod = methodName;
169                }
170
171                Class<?> targetClass = getTargetClass();
172                String targetMethod = getTargetMethod();
173                Assert.notNull(targetClass, "Either 'targetClass' or 'targetObject' is required");
174                Assert.notNull(targetMethod, "Property 'targetMethod' is required");
175
176                Object[] arguments = getArguments();
177                Class<?>[] argTypes = new Class<?>[arguments.length];
178                for (int i = 0; i < arguments.length; ++i) {
179                        argTypes[i] = (arguments[i] != null ? arguments[i].getClass() : Object.class);
180                }
181
182                // Try to get the exact method first.
183                try {
184                        this.methodObject = targetClass.getMethod(targetMethod, argTypes);
185                }
186                catch (NoSuchMethodException ex) {
187                        // Just rethrow exception if we can't get any match.
188                        this.methodObject = findMatchingMethod();
189                        if (this.methodObject == null) {
190                                throw ex;
191                        }
192                }
193        }
194
195        /**
196         * Resolve the given class name into a Class.
197         * <p>The default implementations uses {@code ClassUtils.forName},
198         * using the thread context class loader.
199         * @param className the class name to resolve
200         * @return the resolved Class
201         * @throws ClassNotFoundException if the class name was invalid
202         */
203        protected Class<?> resolveClassName(String className) throws ClassNotFoundException {
204                return ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
205        }
206
207        /**
208         * Find a matching method with the specified name for the specified arguments.
209         * @return a matching method, or {@code null} if none
210         * @see #getTargetClass()
211         * @see #getTargetMethod()
212         * @see #getArguments()
213         */
214        @Nullable
215        protected Method findMatchingMethod() {
216                String targetMethod = getTargetMethod();
217                Object[] arguments = getArguments();
218                int argCount = arguments.length;
219
220                Class<?> targetClass = getTargetClass();
221                Assert.state(targetClass != null, "No target class set");
222                Method[] candidates = ReflectionUtils.getAllDeclaredMethods(targetClass);
223                int minTypeDiffWeight = Integer.MAX_VALUE;
224                Method matchingMethod = null;
225
226                for (Method candidate : candidates) {
227                        if (candidate.getName().equals(targetMethod)) {
228                                if (candidate.getParameterCount() == argCount) {
229                                        Class<?>[] paramTypes = candidate.getParameterTypes();
230                                        int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments);
231                                        if (typeDiffWeight < minTypeDiffWeight) {
232                                                minTypeDiffWeight = typeDiffWeight;
233                                                matchingMethod = candidate;
234                                        }
235                                }
236                        }
237                }
238
239                return matchingMethod;
240        }
241
242        /**
243         * Return the prepared Method object that will be invoked.
244         * <p>Can for example be used to determine the return type.
245         * @return the prepared Method object (never {@code null})
246         * @throws IllegalStateException if the invoker hasn't been prepared yet
247         * @see #prepare
248         * @see #invoke
249         */
250        public Method getPreparedMethod() throws IllegalStateException {
251                if (this.methodObject == null) {
252                        throw new IllegalStateException("prepare() must be called prior to invoke() on MethodInvoker");
253                }
254                return this.methodObject;
255        }
256
257        /**
258         * Return whether this invoker has been prepared already,
259         * i.e. whether it allows access to {@link #getPreparedMethod()} already.
260         */
261        public boolean isPrepared() {
262                return (this.methodObject != null);
263        }
264
265        /**
266         * Invoke the specified method.
267         * <p>The invoker needs to have been prepared before.
268         * @return the object (possibly null) returned by the method invocation,
269         * or {@code null} if the method has a void return type
270         * @throws InvocationTargetException if the target method threw an exception
271         * @throws IllegalAccessException if the target method couldn't be accessed
272         * @see #prepare
273         */
274        @Nullable
275        public Object invoke() throws InvocationTargetException, IllegalAccessException {
276                // In the static case, target will simply be {@code null}.
277                Object targetObject = getTargetObject();
278                Method preparedMethod = getPreparedMethod();
279                if (targetObject == null && !Modifier.isStatic(preparedMethod.getModifiers())) {
280                        throw new IllegalArgumentException("Target method must not be non-static without a target");
281                }
282                ReflectionUtils.makeAccessible(preparedMethod);
283                return preparedMethod.invoke(targetObject, getArguments());
284        }
285
286
287        /**
288         * Algorithm that judges the match between the declared parameter types of a candidate method
289         * and a specific list of arguments that this method is supposed to be invoked with.
290         * <p>Determines a weight that represents the class hierarchy difference between types and
291         * arguments. A direct match, i.e. type Integer -> arg of class Integer, does not increase
292         * the result - all direct matches means weight 0. A match between type Object and arg of
293         * class Integer would increase the weight by 2, due to the superclass 2 steps up in the
294         * hierarchy (i.e. Object) being the last one that still matches the required type Object.
295         * Type Number and class Integer would increase the weight by 1 accordingly, due to the
296         * superclass 1 step up the hierarchy (i.e. Number) still matching the required type Number.
297         * Therefore, with an arg of type Integer, a constructor (Integer) would be preferred to a
298         * constructor (Number) which would in turn be preferred to a constructor (Object).
299         * All argument weights get accumulated.
300         * <p>Note: This is the algorithm used by MethodInvoker itself and also the algorithm
301         * used for constructor and factory method selection in Spring's bean container (in case
302         * of lenient constructor resolution which is the default for regular bean definitions).
303         * @param paramTypes the parameter types to match
304         * @param args the arguments to match
305         * @return the accumulated weight for all arguments
306         */
307        public static int getTypeDifferenceWeight(Class<?>[] paramTypes, Object[] args) {
308                int result = 0;
309                for (int i = 0; i < paramTypes.length; i++) {
310                        if (!ClassUtils.isAssignableValue(paramTypes[i], args[i])) {
311                                return Integer.MAX_VALUE;
312                        }
313                        if (args[i] != null) {
314                                Class<?> paramType = paramTypes[i];
315                                Class<?> superClass = args[i].getClass().getSuperclass();
316                                while (superClass != null) {
317                                        if (paramType.equals(superClass)) {
318                                                result = result + 2;
319                                                superClass = null;
320                                        }
321                                        else if (ClassUtils.isAssignable(paramType, superClass)) {
322                                                result = result + 2;
323                                                superClass = superClass.getSuperclass();
324                                        }
325                                        else {
326                                                superClass = null;
327                                        }
328                                }
329                                if (paramType.isInterface()) {
330                                        result = result + 1;
331                                }
332                        }
333                }
334                return result;
335        }
336
337}