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