/*
 * Copyright 2002-2020 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
 *
 *      https://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.web.method;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseStatus;

Encapsulates information about a handler method consisting of a method and a bean. Provides convenient access to method parameters, the method return value, method annotations, etc.

The class may be created with a bean instance or with a bean name (e.g. lazy-init bean, prototype bean). Use createWithResolvedBean() to obtain a HandlerMethod instance with a bean instance resolved through the associated BeanFactory.

Author:Arjen Poutsma, Rossen Stoyanchev, Juergen Hoeller, Sam Brannen
Since:3.1
/** * Encapsulates information about a handler method consisting of a * {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}. * Provides convenient access to method parameters, the method return value, * method annotations, etc. * * <p>The class may be created with a bean instance or with a bean name * (e.g. lazy-init bean, prototype bean). Use {@link #createWithResolvedBean()} * to obtain a {@code HandlerMethod} instance with a bean instance resolved * through the associated {@link BeanFactory}. * * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sam Brannen * @since 3.1 */
public class HandlerMethod {
Logger that is available to subclasses.
/** Logger that is available to subclasses. */
protected static final Log logger = LogFactory.getLog(HandlerMethod.class); private final Object bean; @Nullable private final BeanFactory beanFactory; private final Class<?> beanType; private final Method method; private final Method bridgedMethod; private final MethodParameter[] parameters; @Nullable private HttpStatus responseStatus; @Nullable private String responseStatusReason; @Nullable private HandlerMethod resolvedFromHandlerMethod; @Nullable private volatile List<Annotation[][]> interfaceParameterAnnotations; private final String description;
Create an instance from a bean instance and a method.
/** * Create an instance from a bean instance and a method. */
public HandlerMethod(Object bean, Method method) { Assert.notNull(bean, "Bean is required"); Assert.notNull(method, "Method is required"); this.bean = bean; this.beanFactory = null; this.beanType = ClassUtils.getUserClass(bean); this.method = method; this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); this.parameters = initMethodParameters(); evaluateResponseStatus(); this.description = initDescription(this.beanType, this.method); }
Create an instance from a bean instance, method name, and parameter types.
Throws:
  • NoSuchMethodException – when the method cannot be found
/** * Create an instance from a bean instance, method name, and parameter types. * @throws NoSuchMethodException when the method cannot be found */
public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException { Assert.notNull(bean, "Bean is required"); Assert.notNull(methodName, "Method name is required"); this.bean = bean; this.beanFactory = null; this.beanType = ClassUtils.getUserClass(bean); this.method = bean.getClass().getMethod(methodName, parameterTypes); this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method); this.parameters = initMethodParameters(); evaluateResponseStatus(); this.description = initDescription(this.beanType, this.method); }
Create an instance from a bean name, a method, and a BeanFactory. The method createWithResolvedBean() may be used later to re-create the HandlerMethod with an initialized bean.
/** * Create an instance from a bean name, a method, and a {@code BeanFactory}. * The method {@link #createWithResolvedBean()} may be used later to * re-create the {@code HandlerMethod} with an initialized bean. */
public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) { Assert.hasText(beanName, "Bean name is required"); Assert.notNull(beanFactory, "BeanFactory is required"); Assert.notNull(method, "Method is required"); this.bean = beanName; this.beanFactory = beanFactory; Class<?> beanType = beanFactory.getType(beanName); if (beanType == null) { throw new IllegalStateException("Cannot resolve bean type for bean with name '" + beanName + "'"); } this.beanType = ClassUtils.getUserClass(beanType); this.method = method; this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); this.parameters = initMethodParameters(); evaluateResponseStatus(); this.description = initDescription(this.beanType, this.method); }
Copy constructor for use in subclasses.
/** * Copy constructor for use in subclasses. */
protected HandlerMethod(HandlerMethod handlerMethod) { Assert.notNull(handlerMethod, "HandlerMethod is required"); this.bean = handlerMethod.bean; this.beanFactory = handlerMethod.beanFactory; this.beanType = handlerMethod.beanType; this.method = handlerMethod.method; this.bridgedMethod = handlerMethod.bridgedMethod; this.parameters = handlerMethod.parameters; this.responseStatus = handlerMethod.responseStatus; this.responseStatusReason = handlerMethod.responseStatusReason; this.description = handlerMethod.description; this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod; }
Re-create HandlerMethod with the resolved handler.
/** * Re-create HandlerMethod with the resolved handler. */
private HandlerMethod(HandlerMethod handlerMethod, Object handler) { Assert.notNull(handlerMethod, "HandlerMethod is required"); Assert.notNull(handler, "Handler object is required"); this.bean = handler; this.beanFactory = handlerMethod.beanFactory; this.beanType = handlerMethod.beanType; this.method = handlerMethod.method; this.bridgedMethod = handlerMethod.bridgedMethod; this.parameters = handlerMethod.parameters; this.responseStatus = handlerMethod.responseStatus; this.responseStatusReason = handlerMethod.responseStatusReason; this.resolvedFromHandlerMethod = handlerMethod; this.description = handlerMethod.description; } private MethodParameter[] initMethodParameters() { int count = this.bridgedMethod.getParameterCount(); MethodParameter[] result = new MethodParameter[count]; for (int i = 0; i < count; i++) { result[i] = new HandlerMethodParameter(i); } return result; } private void evaluateResponseStatus() { ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class); if (annotation == null) { annotation = AnnotatedElementUtils.findMergedAnnotation(getBeanType(), ResponseStatus.class); } if (annotation != null) { this.responseStatus = annotation.code(); this.responseStatusReason = annotation.reason(); } } private static String initDescription(Class<?> beanType, Method method) { StringJoiner joiner = new StringJoiner(", ", "(", ")"); for (Class<?> paramType : method.getParameterTypes()) { joiner.add(paramType.getSimpleName()); } return beanType.getName() + "#" + method.getName() + joiner.toString(); }
Return the bean for this handler method.
/** * Return the bean for this handler method. */
public Object getBean() { return this.bean; }
Return the method for this handler method.
/** * Return the method for this handler method. */
public Method getMethod() { return this.method; }
This method returns the type of the handler for this handler method.

Note that if the bean type is a CGLIB-generated class, the original user-defined class is returned.

/** * This method returns the type of the handler for this handler method. * <p>Note that if the bean type is a CGLIB-generated class, the original * user-defined class is returned. */
public Class<?> getBeanType() { return this.beanType; }
If the bean method is a bridge method, this method returns the bridged (user-defined) method. Otherwise it returns the same method as getMethod().
/** * If the bean method is a bridge method, this method returns the bridged * (user-defined) method. Otherwise it returns the same method as {@link #getMethod()}. */
protected Method getBridgedMethod() { return this.bridgedMethod; }
Return the method parameters for this handler method.
/** * Return the method parameters for this handler method. */
public MethodParameter[] getMethodParameters() { return this.parameters; }
Return the specified response status, if any.
See Also:
Since:4.3.8
/** * Return the specified response status, if any. * @since 4.3.8 * @see ResponseStatus#code() */
@Nullable protected HttpStatus getResponseStatus() { return this.responseStatus; }
Return the associated response status reason, if any.
See Also:
Since:4.3.8
/** * Return the associated response status reason, if any. * @since 4.3.8 * @see ResponseStatus#reason() */
@Nullable protected String getResponseStatusReason() { return this.responseStatusReason; }
Return the HandlerMethod return type.
/** * Return the HandlerMethod return type. */
public MethodParameter getReturnType() { return new HandlerMethodParameter(-1); }
Return the actual return value type.
/** * Return the actual return value type. */
public MethodParameter getReturnValueType(@Nullable Object returnValue) { return new ReturnValueMethodParameter(returnValue); }
Return true if the method return type is void, false otherwise.
/** * Return {@code true} if the method return type is void, {@code false} otherwise. */
public boolean isVoid() { return Void.TYPE.equals(getReturnType().getParameterType()); }
Return a single annotation on the underlying method traversing its super methods if no annotation can be found on the given method itself.

Also supports merged composed annotations with attribute overrides as of Spring Framework 4.2.2.

Params:
  • annotationType – the type of annotation to introspect the method for
See Also:
Returns:the annotation, or null if none found
/** * Return a single annotation on the underlying method traversing its super methods * if no annotation can be found on the given method itself. * <p>Also supports <em>merged</em> composed annotations with attribute * overrides as of Spring Framework 4.2.2. * @param annotationType the type of annotation to introspect the method for * @return the annotation, or {@code null} if none found * @see AnnotatedElementUtils#findMergedAnnotation */
@Nullable public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) { return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); }
Return whether the parameter is declared with the given annotation type.
Params:
  • annotationType – the annotation type to look for
See Also:
Since:4.3
/** * Return whether the parameter is declared with the given annotation type. * @param annotationType the annotation type to look for * @since 4.3 * @see AnnotatedElementUtils#hasAnnotation */
public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) { return AnnotatedElementUtils.hasAnnotation(this.method, annotationType); }
Return the HandlerMethod from which this HandlerMethod instance was resolved via createWithResolvedBean().
/** * Return the HandlerMethod from which this HandlerMethod instance was * resolved via {@link #createWithResolvedBean()}. */
@Nullable public HandlerMethod getResolvedFromHandlerMethod() { return this.resolvedFromHandlerMethod; }
If the provided instance contains a bean name rather than an object instance, the bean name is resolved before a HandlerMethod is created and returned.
/** * If the provided instance contains a bean name rather than an object instance, * the bean name is resolved before a {@link HandlerMethod} is created and returned. */
public HandlerMethod createWithResolvedBean() { Object handler = this.bean; if (this.bean instanceof String) { Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory"); String beanName = (String) this.bean; handler = this.beanFactory.getBean(beanName); } return new HandlerMethod(this, handler); }
Return a short representation of this handler method for log message purposes.
Since:4.3
/** * Return a short representation of this handler method for log message purposes. * @since 4.3 */
public String getShortLogMessage() { return getBeanType().getName() + "#" + this.method.getName() + "[" + this.method.getParameterCount() + " args]"; } private List<Annotation[][]> getInterfaceParameterAnnotations() { List<Annotation[][]> parameterAnnotations = this.interfaceParameterAnnotations; if (parameterAnnotations == null) { parameterAnnotations = new ArrayList<>(); for (Class<?> ifc : ClassUtils.getAllInterfacesForClassAsSet(this.method.getDeclaringClass())) { for (Method candidate : ifc.getMethods()) { if (isOverrideFor(candidate)) { parameterAnnotations.add(candidate.getParameterAnnotations()); } } } this.interfaceParameterAnnotations = parameterAnnotations; } return parameterAnnotations; } private boolean isOverrideFor(Method candidate) { if (!candidate.getName().equals(this.method.getName()) || candidate.getParameterCount() != this.method.getParameterCount()) { return false; } Class<?>[] paramTypes = this.method.getParameterTypes(); if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) { return true; } for (int i = 0; i < paramTypes.length; i++) { if (paramTypes[i] != ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) { return false; } } return true; } @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } if (!(other instanceof HandlerMethod)) { return false; } HandlerMethod otherMethod = (HandlerMethod) other; return (this.bean.equals(otherMethod.bean) && this.method.equals(otherMethod.method)); } @Override public int hashCode() { return (this.bean.hashCode() * 31 + this.method.hashCode()); } @Override public String toString() { return this.description; } // Support methods for use in "InvocableHandlerMethod" sub-class variants.. @Nullable protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { if (!ObjectUtils.isEmpty(providedArgs)) { for (Object providedArg : providedArgs) { if (parameter.getParameterType().isInstance(providedArg)) { return providedArg; } } } return null; } protected static String formatArgumentError(MethodParameter param, String message) { return "Could not resolve parameter [" + param.getParameterIndex() + "] in " + param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : ""); }
Assert that the target bean class is an instance of the class where the given method is declared. In some cases the actual controller instance at request- processing time may be a JDK dynamic proxy (lazy initialization, prototype beans, and others). @Controller's that require proxying should prefer class-based proxy mechanisms.
/** * Assert that the target bean class is an instance of the class where the given * method is declared. In some cases the actual controller instance at request- * processing time may be a JDK dynamic proxy (lazy initialization, prototype * beans, and others). {@code @Controller}'s that require proxying should prefer * class-based proxy mechanisms. */
protected void assertTargetBean(Method method, Object targetBean, Object[] args) { Class<?> methodDeclaringClass = method.getDeclaringClass(); Class<?> targetBeanClass = targetBean.getClass(); if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { String text = "The mapped handler method class '" + methodDeclaringClass.getName() + "' is not an instance of the actual controller bean class '" + targetBeanClass.getName() + "'. If the controller requires proxying " + "(e.g. due to @Transactional), please use class-based proxying."; throw new IllegalStateException(formatInvokeError(text, args)); } } protected String formatInvokeError(String text, Object[] args) { String formattedArgs = IntStream.range(0, args.length) .mapToObj(i -> (args[i] != null ? "[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" : "[" + i + "] [null]")) .collect(Collectors.joining(",\n", " ", " ")); return text + "\n" + "Controller [" + getBeanType().getName() + "]\n" + "Method [" + getBridgedMethod().toGenericString() + "] " + "with argument values:\n" + formattedArgs; }
A MethodParameter with HandlerMethod-specific behavior.
/** * A MethodParameter with HandlerMethod-specific behavior. */
protected class HandlerMethodParameter extends SynthesizingMethodParameter { @Nullable private volatile Annotation[] combinedAnnotations; public HandlerMethodParameter(int index) { super(HandlerMethod.this.bridgedMethod, index); } protected HandlerMethodParameter(HandlerMethodParameter original) { super(original); } @Override @NonNull public Method getMethod() { return HandlerMethod.this.bridgedMethod; } @Override public Class<?> getContainingClass() { return HandlerMethod.this.getBeanType(); } @Override public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) { return HandlerMethod.this.getMethodAnnotation(annotationType); } @Override public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) { return HandlerMethod.this.hasMethodAnnotation(annotationType); } @Override public Annotation[] getParameterAnnotations() { Annotation[] anns = this.combinedAnnotations; if (anns == null) { anns = super.getParameterAnnotations(); int index = getParameterIndex(); if (index >= 0) { for (Annotation[][] ifcAnns : getInterfaceParameterAnnotations()) { if (index < ifcAnns.length) { Annotation[] paramAnns = ifcAnns[index]; if (paramAnns.length > 0) { List<Annotation> merged = new ArrayList<>(anns.length + paramAnns.length); merged.addAll(Arrays.asList(anns)); for (Annotation paramAnn : paramAnns) { boolean existingType = false; for (Annotation ann : anns) { if (ann.annotationType() == paramAnn.annotationType()) { existingType = true; break; } } if (!existingType) { merged.add(adaptAnnotation(paramAnn)); } } anns = merged.toArray(new Annotation[0]); } } } } this.combinedAnnotations = anns; } return anns; } @Override public HandlerMethodParameter clone() { return new HandlerMethodParameter(this); } }
A MethodParameter for a HandlerMethod return type based on an actual return value.
/** * A MethodParameter for a HandlerMethod return type based on an actual return value. */
private class ReturnValueMethodParameter extends HandlerMethodParameter { @Nullable private final Object returnValue; public ReturnValueMethodParameter(@Nullable Object returnValue) { super(-1); this.returnValue = returnValue; } protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { super(original); this.returnValue = original.returnValue; } @Override public Class<?> getParameterType() { return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType()); } @Override public ReturnValueMethodParameter clone() { return new ReturnValueMethodParameter(this); } } }