Java tutorial
/* * Copyright 2002-2019 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.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 * {@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. */ protected final Log logger = LogFactory.getLog(getClass()); 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. */ 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 */ 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 {@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. */ 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. */ 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. */ public Object getBean() { return this.bean; } /** * 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. * <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 {@link #getMethod()}. */ protected Method getBridgedMethod() { return this.bridgedMethod; } /** * Return the method parameters for this handler method. */ public MethodParameter[] getMethodParameters() { return this.parameters; } /** * 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. * @since 4.3.8 * @see ResponseStatus#reason() */ @Nullable protected String getResponseStatusReason() { return this.responseStatusReason; } /** * Return the HandlerMethod return type. */ public MethodParameter getReturnType() { return new HandlerMethodParameter(-1); } /** * Return the actual return value type. */ public MethodParameter getReturnValueType(@Nullable Object returnValue) { return new ReturnValueMethodParameter(returnValue); } /** * 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. * <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. * @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 {@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 {@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 */ 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 : this.method.getDeclaringClass().getInterfaces()) { 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). {@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. */ 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 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. */ 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); } } }