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}