Open Source Repository

Home /spring/spring-core-3.0.5 | Repository Home



org/springframework/util/MethodInvoker.java
/*
 * Copyright 2002-2009 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
 *
 *      http://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;

/**
 * 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.
 *
 <p>Typically not used directly but via its subclasses
 {@link org.springframework.beans.factory.config.MethodInvokingFactoryBean} and
 {@link org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean}.
 *
 @author Colin Sampaleanu
 @author Juergen Hoeller
 @since 19.02.2004
 @see #prepare
 @see #invoke
 */
public class MethodInvoker {

  private Class targetClass;

  private Object targetObject;

  private String targetMethod;

  private String staticMethod;

  private Object[] arguments = new Object[0];

  /** The method we will call */
  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(Class targetClass) {
    this.targetClass = targetClass;
  }

  /**
   * Return the target class on which to call the target method.
   */
  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(Object targetObject) {
    this.targetObject = targetObject;
    if (targetObject != null) {
      this.targetClass = targetObject.getClass();
    }
  }

  /**
   * Return the target object on which to call the target method.
   */
  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(String targetMethod) {
    this.targetMethod = targetMethod;
  }

  /**
   * Return the name of the method to be invoked.
   */
  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 != null ? arguments : new Object[0]);
  }

  /**
   * Return the arguments for the method invocation.
   */
  public Object[] getArguments() {
    return this.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 == -|| 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();
    if (targetClass == null) {
      throw new IllegalArgumentException("Either 'targetClass' or 'targetObject' is required");
    }
    if (targetMethod == null) {
      throw new IllegalArgumentException("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</code>,
   * 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 classNamethrows 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</code> if none
   @see #getTargetClass()
   @see #getTargetMethod()
   @see #getArguments()
   */
  protected Method findMatchingMethod() {
    String targetMethod = getTargetMethod();
    Object[] arguments = getArguments();
    int argCount = arguments.length;

    Method[] candidates = ReflectionUtils.getAllDeclaredMethods(getTargetClass());
    int minTypeDiffWeight = Integer.MAX_VALUE;
    Method matchingMethod = null;

    for (Method candidate : candidates) {
      if (candidate.getName().equals(targetMethod)) {
        Class[] paramTypes = candidate.getParameterTypes();
        if (paramTypes.length == argCount) {
          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</code>)
   @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</code> 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
   */
  public Object invoke() throws InvocationTargetException, IllegalAccessException {
    // In the static case, target will simply be <code>null</code>.
    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.
   @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;
  }

}