/*
* Copyright 2002-2017 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.
*
* @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;
/** 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;
}
/**
* Return the arguments for the method invocation.
*/
public
Object[]
getArguments() {
return (this.
arguments != null ? this.
arguments : new
Object[0]);
}
/**
* 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 == -1 ||
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();
Assert.
notNull(
targetClass, "Either 'targetClass' or 'targetObject' is required");
Assert.
notNull(
targetMethod, "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},
* 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 className) throws
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} 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})
* @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} 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}.
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.
* <p>Note: This is the algorithm used by MethodInvoker itself and also the algorithm
* used for constructor and factory method selection in Spring's bean container (in case
* of lenient constructor resolution which is the default for regular bean definitions).
* @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;
}
}