All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.method.HandlerMethod Maven / Gradle / Ivy

There is a newer version: 6.1.10
Show newest version
/*
 * Copyright 2002-2024 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.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.Predicate;
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.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotatedMethod;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.http.HttpStatusCode;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
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.
 *
 * 

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 extends AnnotatedMethod { /** Logger that is available to subclasses. */ protected static final Log logger = LogFactory.getLog(HandlerMethod.class); private final Object bean; @Nullable private final BeanFactory beanFactory; @Nullable private final MessageSource messageSource; private final Class beanType; private final boolean validateArguments; private final boolean validateReturnValue; @Nullable private HttpStatusCode responseStatus; @Nullable private String responseStatusReason; @Nullable private HandlerMethod resolvedFromHandlerMethod; private final String description; /** * Create an instance from a bean instance and a method. */ public HandlerMethod(Object bean, Method method) { this(bean, method, null); } /** * Variant of {@link #HandlerMethod(Object, Method)} that * also accepts a {@link MessageSource} for use from subclasses. * @since 5.3.10 */ protected HandlerMethod(Object bean, Method method, @Nullable MessageSource messageSource) { super(method); this.bean = bean; this.beanFactory = null; this.messageSource = messageSource; this.beanType = ClassUtils.getUserClass(bean); this.validateArguments = false; this.validateReturnValue = false; evaluateResponseStatus(); this.description = initDescription(this.beanType, 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 { super(bean.getClass().getMethod(methodName, parameterTypes)); this.bean = bean; this.beanFactory = null; this.messageSource = null; this.beanType = ClassUtils.getUserClass(bean); this.validateArguments = false; this.validateReturnValue = false; evaluateResponseStatus(); this.description = initDescription(this.beanType, getMethod()); } /** * 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) { this(beanName, beanFactory, null, method); } /** * Variant of {@link #HandlerMethod(String, BeanFactory, Method)} that * also accepts a {@link MessageSource}. */ public HandlerMethod( String beanName, BeanFactory beanFactory, @Nullable MessageSource messageSource, Method method) { super(method); Assert.hasText(beanName, "Bean name is required"); Assert.notNull(beanFactory, "BeanFactory is required"); this.bean = beanName; this.beanFactory = beanFactory; this.messageSource = messageSource; 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.validateArguments = false; this.validateReturnValue = false; evaluateResponseStatus(); this.description = initDescription(this.beanType, method); } /** * Copy constructor for use in subclasses. */ protected HandlerMethod(HandlerMethod handlerMethod) { this(handlerMethod, null, false); } /** * Re-create HandlerMethod with additional input. */ private HandlerMethod(HandlerMethod handlerMethod, @Nullable Object handler, boolean initValidateFlags) { super(handlerMethod); this.bean = (handler != null ? handler : handlerMethod.bean); this.beanFactory = handlerMethod.beanFactory; this.messageSource = handlerMethod.messageSource; this.beanType = handlerMethod.beanType; this.validateArguments = (initValidateFlags ? MethodValidationInitializer.checkArguments(this.beanType, getMethodParameters()) : handlerMethod.validateArguments); this.validateReturnValue = (initValidateFlags ? MethodValidationInitializer.checkReturnValue(this.beanType, getBridgedMethod()) : handlerMethod.validateReturnValue); this.responseStatus = handlerMethod.responseStatus; this.responseStatusReason = handlerMethod.responseStatusReason; this.resolvedFromHandlerMethod = handlerMethod; this.description = handlerMethod.toString(); } private void evaluateResponseStatus() { ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class); if (annotation == null) { annotation = AnnotatedElementUtils.findMergedAnnotation(getBeanType(), ResponseStatus.class); } if (annotation != null) { String reason = annotation.reason(); String resolvedReason = (StringUtils.hasText(reason) && this.messageSource != null ? this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) : reason); this.responseStatus = annotation.code(); this.responseStatusReason = resolvedReason; if (StringUtils.hasText(this.responseStatusReason) && getMethod().getReturnType() != void.class) { logger.warn("Return value of [" + getMethod() + "] will be ignored since @ResponseStatus 'reason' attribute is set."); } } } 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; } /** * Return the bean for this handler method. */ public Object getBean() { return this.bean; } /** * 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. */ public Class getBeanType() { return this.beanType; } @Override protected Class getContainingClass() { return this.beanType; } /** * Whether the method arguments are a candidate for method validation, which * is the case when there are parameter {@code jakarta.validation.Constraint} * annotations. *

The presence of {@code jakarta.validation.Valid} by itself does not * trigger method validation since such parameters are already validated at * the level of argument resolvers. *

Note: if the class is annotated with {@link Validated}, * this method returns false, deferring to method validation via AOP proxy. * @since 6.1 */ public boolean shouldValidateArguments() { return this.validateArguments; } /** * Whether the method return value is a candidate for method validation, which * is the case when there are method {@code jakarta.validation.Constraint} * or {@code jakarta.validation.Valid} annotations. *

Note: if the class is annotated with {@link Validated}, * this method returns false, deferring to method validation via AOP proxy. * @since 6.1 */ public boolean shouldValidateReturnValue() { return this.validateReturnValue; } /** * Return the specified response status, if any. * @since 4.3.8 * @see ResponseStatus#code() */ @Nullable protected HttpStatusCode 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 from which this HandlerMethod instance was * resolved via {@link #createWithResolvedBean()}. */ @Nullable public HandlerMethod getResolvedFromHandlerMethod() { return this.resolvedFromHandlerMethod; } /** * Re-create the HandlerMethod and initialize * {@link #shouldValidateArguments()} and {@link #shouldValidateReturnValue()}. * @since 6.1.3 */ public HandlerMethod createWithValidateFlags() { return new HandlerMethod(this, null, true); } /** * 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 beanName) { Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory"); handler = this.beanFactory.getBean(beanName); } Assert.notNull(handler, "No handler instance"); return new HandlerMethod(this, handler, false); } /** * Return a short representation of this handler method for log message purposes. * @since 4.3 */ public String getShortLogMessage() { return getBeanType().getName() + "#" + getMethod().getName() + "[" + getMethod().getParameterCount() + " args]"; } @Override public boolean equals(@Nullable Object other) { return (this == other || (super.equals(other) && this.bean.equals(((HandlerMethod) other).bean))); } @Override public int hashCode() { return (this.bean.hashCode() * 31 + super.hashCode()); } @Override public String toString() { return this.description; } // Support methods for use in subclass variants /** * 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; } /** * Checks for the presence of {@code @Constraint} and {@code @Valid} * annotations on the method and method parameters. */ private static class MethodValidationInitializer { private static final boolean BEAN_VALIDATION_PRESENT = ClassUtils.isPresent("jakarta.validation.Validator", HandlerMethod.class.getClassLoader()); private static final Predicate> CONSTRAINT_PREDICATE = MergedAnnotationPredicates.typeIn("jakarta.validation.Constraint"); private static final Predicate> VALID_PREDICATE = MergedAnnotationPredicates.typeIn("jakarta.validation.Valid"); public static boolean checkArguments(Class beanType, MethodParameter[] parameters) { if (BEAN_VALIDATION_PRESENT && AnnotationUtils.findAnnotation(beanType, Validated.class) == null) { for (MethodParameter param : parameters) { MergedAnnotations merged = MergedAnnotations.from(param.getParameterAnnotations()); if (merged.stream().anyMatch(CONSTRAINT_PREDICATE)) { return true; } Class type = param.getParameterType(); if (merged.stream().anyMatch(VALID_PREDICATE) && isIndexOrKeyBasedContainer(type)) { return true; } merged = MergedAnnotations.from(getContainerElementAnnotations(param)); if (merged.stream().anyMatch(CONSTRAINT_PREDICATE)) { return true; } } } return false; } public static boolean checkReturnValue(Class beanType, Method method) { if (BEAN_VALIDATION_PRESENT && AnnotationUtils.findAnnotation(beanType, Validated.class) == null) { MergedAnnotations merged = MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); return merged.stream().anyMatch(CONSTRAINT_PREDICATE.or(VALID_PREDICATE)); } return false; } private static boolean isIndexOrKeyBasedContainer(Class type) { // Index or key-based containers only, or MethodValidationAdapter cannot access // the element given what is exposed in ConstraintViolation. return (List.class.isAssignableFrom(type) || Object[].class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)); } /** * There may be constraints on elements of a container (list, map). */ private static Annotation[] getContainerElementAnnotations(MethodParameter param) { List result = null; int i = param.getParameterIndex(); Method method = param.getMethod(); if (method != null && method.getAnnotatedParameterTypes()[i] instanceof AnnotatedParameterizedType apt) { for (AnnotatedType type : apt.getAnnotatedActualTypeArguments()) { for (Annotation annot : type.getAnnotations()) { result = (result != null ? result : new ArrayList<>()); result.add(annot); } } } result = (result != null ? result : Collections.emptyList()); return result.toArray(new Annotation[0]); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy