001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.method;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.Method;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.List;
024import java.util.StringJoiner;
025import java.util.stream.Collectors;
026import java.util.stream.IntStream;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.beans.factory.BeanFactory;
032import org.springframework.core.BridgeMethodResolver;
033import org.springframework.core.MethodParameter;
034import org.springframework.core.ResolvableType;
035import org.springframework.core.annotation.AnnotatedElementUtils;
036import org.springframework.core.annotation.SynthesizingMethodParameter;
037import org.springframework.http.HttpStatus;
038import org.springframework.lang.Nullable;
039import org.springframework.util.Assert;
040import org.springframework.util.ClassUtils;
041import org.springframework.util.ObjectUtils;
042import org.springframework.util.StringUtils;
043import org.springframework.web.bind.annotation.ResponseStatus;
044
045/**
046 * Encapsulates information about a handler method consisting of a
047 * {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
048 * Provides convenient access to method parameters, the method return value,
049 * method annotations, etc.
050 *
051 * <p>The class may be created with a bean instance or with a bean name
052 * (e.g. lazy-init bean, prototype bean). Use {@link #createWithResolvedBean()}
053 * to obtain a {@code HandlerMethod} instance with a bean instance resolved
054 * through the associated {@link BeanFactory}.
055 *
056 * @author Arjen Poutsma
057 * @author Rossen Stoyanchev
058 * @author Juergen Hoeller
059 * @author Sam Brannen
060 * @since 3.1
061 */
062public class HandlerMethod {
063
064        /** Logger that is available to subclasses. */
065        protected final Log logger = LogFactory.getLog(getClass());
066
067        private final Object bean;
068
069        @Nullable
070        private final BeanFactory beanFactory;
071
072        private final Class<?> beanType;
073
074        private final Method method;
075
076        private final Method bridgedMethod;
077
078        private final MethodParameter[] parameters;
079
080        @Nullable
081        private HttpStatus responseStatus;
082
083        @Nullable
084        private String responseStatusReason;
085
086        @Nullable
087        private HandlerMethod resolvedFromHandlerMethod;
088
089        @Nullable
090        private volatile List<Annotation[][]> interfaceParameterAnnotations;
091
092        private final String description;
093
094
095        /**
096         * Create an instance from a bean instance and a method.
097         */
098        public HandlerMethod(Object bean, Method method) {
099                Assert.notNull(bean, "Bean is required");
100                Assert.notNull(method, "Method is required");
101                this.bean = bean;
102                this.beanFactory = null;
103                this.beanType = ClassUtils.getUserClass(bean);
104                this.method = method;
105                this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
106                this.parameters = initMethodParameters();
107                evaluateResponseStatus();
108                this.description = initDescription(this.beanType, this.method);
109        }
110
111        /**
112         * Create an instance from a bean instance, method name, and parameter types.
113         * @throws NoSuchMethodException when the method cannot be found
114         */
115        public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
116                Assert.notNull(bean, "Bean is required");
117                Assert.notNull(methodName, "Method name is required");
118                this.bean = bean;
119                this.beanFactory = null;
120                this.beanType = ClassUtils.getUserClass(bean);
121                this.method = bean.getClass().getMethod(methodName, parameterTypes);
122                this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method);
123                this.parameters = initMethodParameters();
124                evaluateResponseStatus();
125                this.description = initDescription(this.beanType, this.method);
126        }
127
128        /**
129         * Create an instance from a bean name, a method, and a {@code BeanFactory}.
130         * The method {@link #createWithResolvedBean()} may be used later to
131         * re-create the {@code HandlerMethod} with an initialized bean.
132         */
133        public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
134                Assert.hasText(beanName, "Bean name is required");
135                Assert.notNull(beanFactory, "BeanFactory is required");
136                Assert.notNull(method, "Method is required");
137                this.bean = beanName;
138                this.beanFactory = beanFactory;
139                Class<?> beanType = beanFactory.getType(beanName);
140                if (beanType == null) {
141                        throw new IllegalStateException("Cannot resolve bean type for bean with name '" + beanName + "'");
142                }
143                this.beanType = ClassUtils.getUserClass(beanType);
144                this.method = method;
145                this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
146                this.parameters = initMethodParameters();
147                evaluateResponseStatus();
148                this.description = initDescription(this.beanType, this.method);
149        }
150
151        /**
152         * Copy constructor for use in subclasses.
153         */
154        protected HandlerMethod(HandlerMethod handlerMethod) {
155                Assert.notNull(handlerMethod, "HandlerMethod is required");
156                this.bean = handlerMethod.bean;
157                this.beanFactory = handlerMethod.beanFactory;
158                this.beanType = handlerMethod.beanType;
159                this.method = handlerMethod.method;
160                this.bridgedMethod = handlerMethod.bridgedMethod;
161                this.parameters = handlerMethod.parameters;
162                this.responseStatus = handlerMethod.responseStatus;
163                this.responseStatusReason = handlerMethod.responseStatusReason;
164                this.description = handlerMethod.description;
165                this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod;
166        }
167
168        /**
169         * Re-create HandlerMethod with the resolved handler.
170         */
171        private HandlerMethod(HandlerMethod handlerMethod, Object handler) {
172                Assert.notNull(handlerMethod, "HandlerMethod is required");
173                Assert.notNull(handler, "Handler object is required");
174                this.bean = handler;
175                this.beanFactory = handlerMethod.beanFactory;
176                this.beanType = handlerMethod.beanType;
177                this.method = handlerMethod.method;
178                this.bridgedMethod = handlerMethod.bridgedMethod;
179                this.parameters = handlerMethod.parameters;
180                this.responseStatus = handlerMethod.responseStatus;
181                this.responseStatusReason = handlerMethod.responseStatusReason;
182                this.resolvedFromHandlerMethod = handlerMethod;
183                this.description = handlerMethod.description;
184        }
185
186        private MethodParameter[] initMethodParameters() {
187                int count = this.bridgedMethod.getParameterCount();
188                MethodParameter[] result = new MethodParameter[count];
189                for (int i = 0; i < count; i++) {
190                        result[i] = new HandlerMethodParameter(i);
191                }
192                return result;
193        }
194
195        private void evaluateResponseStatus() {
196                ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class);
197                if (annotation == null) {
198                        annotation = AnnotatedElementUtils.findMergedAnnotation(getBeanType(), ResponseStatus.class);
199                }
200                if (annotation != null) {
201                        this.responseStatus = annotation.code();
202                        this.responseStatusReason = annotation.reason();
203                }
204        }
205
206        private static String initDescription(Class<?> beanType, Method method) {
207                StringJoiner joiner = new StringJoiner(", ", "(", ")");
208                for (Class<?> paramType : method.getParameterTypes()) {
209                        joiner.add(paramType.getSimpleName());
210                }
211                return beanType.getName() + "#" + method.getName() + joiner.toString();
212        }
213
214
215        /**
216         * Return the bean for this handler method.
217         */
218        public Object getBean() {
219                return this.bean;
220        }
221
222        /**
223         * Return the method for this handler method.
224         */
225        public Method getMethod() {
226                return this.method;
227        }
228
229        /**
230         * This method returns the type of the handler for this handler method.
231         * <p>Note that if the bean type is a CGLIB-generated class, the original
232         * user-defined class is returned.
233         */
234        public Class<?> getBeanType() {
235                return this.beanType;
236        }
237
238        /**
239         * If the bean method is a bridge method, this method returns the bridged
240         * (user-defined) method. Otherwise it returns the same method as {@link #getMethod()}.
241         */
242        protected Method getBridgedMethod() {
243                return this.bridgedMethod;
244        }
245
246        /**
247         * Return the method parameters for this handler method.
248         */
249        public MethodParameter[] getMethodParameters() {
250                return this.parameters;
251        }
252
253        /**
254         * Return the specified response status, if any.
255         * @since 4.3.8
256         * @see ResponseStatus#code()
257         */
258        @Nullable
259        protected HttpStatus getResponseStatus() {
260                return this.responseStatus;
261        }
262
263        /**
264         * Return the associated response status reason, if any.
265         * @since 4.3.8
266         * @see ResponseStatus#reason()
267         */
268        @Nullable
269        protected String getResponseStatusReason() {
270                return this.responseStatusReason;
271        }
272
273        /**
274         * Return the HandlerMethod return type.
275         */
276        public MethodParameter getReturnType() {
277                return new HandlerMethodParameter(-1);
278        }
279
280        /**
281         * Return the actual return value type.
282         */
283        public MethodParameter getReturnValueType(@Nullable Object returnValue) {
284                return new ReturnValueMethodParameter(returnValue);
285        }
286
287        /**
288         * Return {@code true} if the method return type is void, {@code false} otherwise.
289         */
290        public boolean isVoid() {
291                return Void.TYPE.equals(getReturnType().getParameterType());
292        }
293
294        /**
295         * Return a single annotation on the underlying method traversing its super methods
296         * if no annotation can be found on the given method itself.
297         * <p>Also supports <em>merged</em> composed annotations with attribute
298         * overrides as of Spring Framework 4.2.2.
299         * @param annotationType the type of annotation to introspect the method for
300         * @return the annotation, or {@code null} if none found
301         * @see AnnotatedElementUtils#findMergedAnnotation
302         */
303        @Nullable
304        public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
305                return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
306        }
307
308        /**
309         * Return whether the parameter is declared with the given annotation type.
310         * @param annotationType the annotation type to look for
311         * @since 4.3
312         * @see AnnotatedElementUtils#hasAnnotation
313         */
314        public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
315                return AnnotatedElementUtils.hasAnnotation(this.method, annotationType);
316        }
317
318        /**
319         * Return the HandlerMethod from which this HandlerMethod instance was
320         * resolved via {@link #createWithResolvedBean()}.
321         */
322        @Nullable
323        public HandlerMethod getResolvedFromHandlerMethod() {
324                return this.resolvedFromHandlerMethod;
325        }
326
327        /**
328         * If the provided instance contains a bean name rather than an object instance,
329         * the bean name is resolved before a {@link HandlerMethod} is created and returned.
330         */
331        public HandlerMethod createWithResolvedBean() {
332                Object handler = this.bean;
333                if (this.bean instanceof String) {
334                        Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
335                        String beanName = (String) this.bean;
336                        handler = this.beanFactory.getBean(beanName);
337                }
338                return new HandlerMethod(this, handler);
339        }
340
341        /**
342         * Return a short representation of this handler method for log message purposes.
343         * @since 4.3
344         */
345        public String getShortLogMessage() {
346                return getBeanType().getName() + "#" + this.method.getName() +
347                                "[" + this.method.getParameterCount() + " args]";
348        }
349
350
351        private List<Annotation[][]> getInterfaceParameterAnnotations() {
352                List<Annotation[][]> parameterAnnotations = this.interfaceParameterAnnotations;
353                if (parameterAnnotations == null) {
354                        parameterAnnotations = new ArrayList<>();
355                        for (Class<?> ifc : ClassUtils.getAllInterfacesForClassAsSet(this.method.getDeclaringClass())) {
356                                for (Method candidate : ifc.getMethods()) {
357                                        if (isOverrideFor(candidate)) {
358                                                parameterAnnotations.add(candidate.getParameterAnnotations());
359                                        }
360                                }
361                        }
362                        this.interfaceParameterAnnotations = parameterAnnotations;
363                }
364                return parameterAnnotations;
365        }
366
367        private boolean isOverrideFor(Method candidate) {
368                if (!candidate.getName().equals(this.method.getName()) ||
369                                candidate.getParameterCount() != this.method.getParameterCount()) {
370                        return false;
371                }
372                Class<?>[] paramTypes = this.method.getParameterTypes();
373                if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) {
374                        return true;
375                }
376                for (int i = 0; i < paramTypes.length; i++) {
377                        if (paramTypes[i] !=
378                                        ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) {
379                                return false;
380                        }
381                }
382                return true;
383        }
384
385
386        @Override
387        public boolean equals(@Nullable Object other) {
388                if (this == other) {
389                        return true;
390                }
391                if (!(other instanceof HandlerMethod)) {
392                        return false;
393                }
394                HandlerMethod otherMethod = (HandlerMethod) other;
395                return (this.bean.equals(otherMethod.bean) && this.method.equals(otherMethod.method));
396        }
397
398        @Override
399        public int hashCode() {
400                return (this.bean.hashCode() * 31 + this.method.hashCode());
401        }
402
403        @Override
404        public String toString() {
405                return this.description;
406        }
407
408
409        // Support methods for use in "InvocableHandlerMethod" sub-class variants..
410
411        @Nullable
412        protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) {
413                if (!ObjectUtils.isEmpty(providedArgs)) {
414                        for (Object providedArg : providedArgs) {
415                                if (parameter.getParameterType().isInstance(providedArg)) {
416                                        return providedArg;
417                                }
418                        }
419                }
420                return null;
421        }
422
423        protected static String formatArgumentError(MethodParameter param, String message) {
424                return "Could not resolve parameter [" + param.getParameterIndex() + "] in " +
425                                param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : "");
426        }
427
428        /**
429         * Assert that the target bean class is an instance of the class where the given
430         * method is declared. In some cases the actual controller instance at request-
431         * processing time may be a JDK dynamic proxy (lazy initialization, prototype
432         * beans, and others). {@code @Controller}'s that require proxying should prefer
433         * class-based proxy mechanisms.
434         */
435        protected void assertTargetBean(Method method, Object targetBean, Object[] args) {
436                Class<?> methodDeclaringClass = method.getDeclaringClass();
437                Class<?> targetBeanClass = targetBean.getClass();
438                if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
439                        String text = "The mapped handler method class '" + methodDeclaringClass.getName() +
440                                        "' is not an instance of the actual controller bean class '" +
441                                        targetBeanClass.getName() + "'. If the controller requires proxying " +
442                                        "(e.g. due to @Transactional), please use class-based proxying.";
443                        throw new IllegalStateException(formatInvokeError(text, args));
444                }
445        }
446
447        protected String formatInvokeError(String text, Object[] args) {
448                String formattedArgs = IntStream.range(0, args.length)
449                                .mapToObj(i -> (args[i] != null ?
450                                                "[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" :
451                                                "[" + i + "] [null]"))
452                                .collect(Collectors.joining(",\n", " ", " "));
453                return text + "\n" +
454                                "Controller [" + getBeanType().getName() + "]\n" +
455                                "Method [" + getBridgedMethod().toGenericString() + "] " +
456                                "with argument values:\n" + formattedArgs;
457        }
458
459
460        /**
461         * A MethodParameter with HandlerMethod-specific behavior.
462         */
463        protected class HandlerMethodParameter extends SynthesizingMethodParameter {
464
465                @Nullable
466                private volatile Annotation[] combinedAnnotations;
467
468                public HandlerMethodParameter(int index) {
469                        super(HandlerMethod.this.bridgedMethod, index);
470                }
471
472                protected HandlerMethodParameter(HandlerMethodParameter original) {
473                        super(original);
474                }
475
476                @Override
477                public Class<?> getContainingClass() {
478                        return HandlerMethod.this.getBeanType();
479                }
480
481                @Override
482                public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) {
483                        return HandlerMethod.this.getMethodAnnotation(annotationType);
484                }
485
486                @Override
487                public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) {
488                        return HandlerMethod.this.hasMethodAnnotation(annotationType);
489                }
490
491                @Override
492                public Annotation[] getParameterAnnotations() {
493                        Annotation[] anns = this.combinedAnnotations;
494                        if (anns == null) {
495                                anns = super.getParameterAnnotations();
496                                int index = getParameterIndex();
497                                if (index >= 0) {
498                                        for (Annotation[][] ifcAnns : getInterfaceParameterAnnotations()) {
499                                                if (index < ifcAnns.length) {
500                                                        Annotation[] paramAnns = ifcAnns[index];
501                                                        if (paramAnns.length > 0) {
502                                                                List<Annotation> merged = new ArrayList<>(anns.length + paramAnns.length);
503                                                                merged.addAll(Arrays.asList(anns));
504                                                                for (Annotation paramAnn : paramAnns) {
505                                                                        boolean existingType = false;
506                                                                        for (Annotation ann : anns) {
507                                                                                if (ann.annotationType() == paramAnn.annotationType()) {
508                                                                                        existingType = true;
509                                                                                        break;
510                                                                                }
511                                                                        }
512                                                                        if (!existingType) {
513                                                                                merged.add(adaptAnnotation(paramAnn));
514                                                                        }
515                                                                }
516                                                                anns = merged.toArray(new Annotation[0]);
517                                                        }
518                                                }
519                                        }
520                                }
521                                this.combinedAnnotations = anns;
522                        }
523                        return anns;
524                }
525
526                @Override
527                public HandlerMethodParameter clone() {
528                        return new HandlerMethodParameter(this);
529                }
530        }
531
532
533        /**
534         * A MethodParameter for a HandlerMethod return type based on an actual return value.
535         */
536        private class ReturnValueMethodParameter extends HandlerMethodParameter {
537
538                @Nullable
539                private final Object returnValue;
540
541                public ReturnValueMethodParameter(@Nullable Object returnValue) {
542                        super(-1);
543                        this.returnValue = returnValue;
544                }
545
546                protected ReturnValueMethodParameter(ReturnValueMethodParameter original) {
547                        super(original);
548                        this.returnValue = original.returnValue;
549                }
550
551                @Override
552                public Class<?> getParameterType() {
553                        return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType());
554                }
555
556                @Override
557                public ReturnValueMethodParameter clone() {
558                        return new ReturnValueMethodParameter(this);
559                }
560        }
561
562}