001/*
002 * Copyright 2002-2019 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.messaging.handler;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.Method;
021import java.util.stream.Collectors;
022import java.util.stream.IntStream;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026
027import org.springframework.beans.factory.BeanFactory;
028import org.springframework.core.BridgeMethodResolver;
029import org.springframework.core.MethodParameter;
030import org.springframework.core.annotation.AnnotatedElementUtils;
031import org.springframework.core.annotation.SynthesizingMethodParameter;
032import org.springframework.lang.Nullable;
033import org.springframework.util.Assert;
034import org.springframework.util.ClassUtils;
035import org.springframework.util.ObjectUtils;
036import org.springframework.util.StringUtils;
037
038/**
039 * Encapsulates information about a handler method consisting of a
040 * {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
041 * Provides convenient access to method parameters, the method return value,
042 * method annotations, etc.
043 *
044 * <p>The class may be created with a bean instance or with a bean name
045 * (e.g. lazy-init bean, prototype bean). Use {@link #createWithResolvedBean()}
046 * to obtain a {@code HandlerMethod} instance with a bean instance resolved
047 * through the associated {@link BeanFactory}.
048 *
049 * @author Arjen Poutsma
050 * @author Rossen Stoyanchev
051 * @author Juergen Hoeller
052 * @since 4.0
053 */
054public class HandlerMethod {
055
056        /** Public for wrapping with fallback logger. */
057        public static final Log defaultLogger = LogFactory.getLog(HandlerMethod.class);
058
059
060        private final Object bean;
061
062        @Nullable
063        private final BeanFactory beanFactory;
064
065        private final Class<?> beanType;
066
067        private final Method method;
068
069        private final Method bridgedMethod;
070
071        private final MethodParameter[] parameters;
072
073        @Nullable
074        private HandlerMethod resolvedFromHandlerMethod;
075
076        protected Log logger = defaultLogger;
077
078
079        /**
080         * Create an instance from a bean instance and a method.
081         */
082        public HandlerMethod(Object bean, Method method) {
083                Assert.notNull(bean, "Bean is required");
084                Assert.notNull(method, "Method is required");
085                this.bean = bean;
086                this.beanFactory = null;
087                this.beanType = ClassUtils.getUserClass(bean);
088                this.method = method;
089                this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
090                this.parameters = initMethodParameters();
091        }
092
093        /**
094         * Create an instance from a bean instance, method name, and parameter types.
095         * @throws NoSuchMethodException when the method cannot be found
096         */
097        public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
098                Assert.notNull(bean, "Bean is required");
099                Assert.notNull(methodName, "Method name is required");
100                this.bean = bean;
101                this.beanFactory = null;
102                this.beanType = ClassUtils.getUserClass(bean);
103                this.method = bean.getClass().getMethod(methodName, parameterTypes);
104                this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method);
105                this.parameters = initMethodParameters();
106        }
107
108        /**
109         * Create an instance from a bean name, a method, and a {@code BeanFactory}.
110         * The method {@link #createWithResolvedBean()} may be used later to
111         * re-create the {@code HandlerMethod} with an initialized bean.
112         */
113        public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
114                Assert.hasText(beanName, "Bean name is required");
115                Assert.notNull(beanFactory, "BeanFactory is required");
116                Assert.notNull(method, "Method is required");
117                this.bean = beanName;
118                this.beanFactory = beanFactory;
119                Class<?> beanType = beanFactory.getType(beanName);
120                if (beanType == null) {
121                        throw new IllegalStateException("Cannot resolve bean type for bean with name '" + beanName + "'");
122                }
123                this.beanType = ClassUtils.getUserClass(beanType);
124                this.method = method;
125                this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
126                this.parameters = initMethodParameters();
127        }
128
129        /**
130         * Copy constructor for use in subclasses.
131         */
132        protected HandlerMethod(HandlerMethod handlerMethod) {
133                Assert.notNull(handlerMethod, "HandlerMethod is required");
134                this.bean = handlerMethod.bean;
135                this.beanFactory = handlerMethod.beanFactory;
136                this.beanType = handlerMethod.beanType;
137                this.method = handlerMethod.method;
138                this.bridgedMethod = handlerMethod.bridgedMethod;
139                this.parameters = handlerMethod.parameters;
140                this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod;
141        }
142
143        /**
144         * Re-create HandlerMethod with the resolved handler.
145         */
146        private HandlerMethod(HandlerMethod handlerMethod, Object handler) {
147                Assert.notNull(handlerMethod, "HandlerMethod is required");
148                Assert.notNull(handler, "Handler object is required");
149                this.bean = handler;
150                this.beanFactory = handlerMethod.beanFactory;
151                this.beanType = handlerMethod.beanType;
152                this.method = handlerMethod.method;
153                this.bridgedMethod = handlerMethod.bridgedMethod;
154                this.parameters = handlerMethod.parameters;
155                this.resolvedFromHandlerMethod = handlerMethod;
156        }
157
158
159        private MethodParameter[] initMethodParameters() {
160                int count = this.bridgedMethod.getParameterCount();
161                MethodParameter[] result = new MethodParameter[count];
162                for (int i = 0; i < count; i++) {
163                        result[i] = new HandlerMethodParameter(i);
164                }
165                return result;
166        }
167
168
169        /**
170         * Set an alternative logger to use than the one based on the class name.
171         * @param logger the logger to use
172         * @since 5.1
173         */
174        public void setLogger(Log logger) {
175                this.logger = logger;
176        }
177
178        /**
179         * Return the currently configured Logger.
180         * @since 5.1
181         */
182        public Log getLogger() {
183                return logger;
184        }
185
186        /**
187         * Return the bean for this handler method.
188         */
189        public Object getBean() {
190                return this.bean;
191        }
192
193        /**
194         * Return the method for this handler method.
195         */
196        public Method getMethod() {
197                return this.method;
198        }
199
200        /**
201         * This method returns the type of the handler for this handler method.
202         * <p>Note that if the bean type is a CGLIB-generated class, the original
203         * user-defined class is returned.
204         */
205        public Class<?> getBeanType() {
206                return this.beanType;
207        }
208
209        /**
210         * If the bean method is a bridge method, this method returns the bridged
211         * (user-defined) method. Otherwise it returns the same method as {@link #getMethod()}.
212         */
213        protected Method getBridgedMethod() {
214                return this.bridgedMethod;
215        }
216
217        /**
218         * Return the method parameters for this handler method.
219         */
220        public MethodParameter[] getMethodParameters() {
221                return this.parameters;
222        }
223
224        /**
225         * Return the HandlerMethod return type.
226         */
227        public MethodParameter getReturnType() {
228                return new HandlerMethodParameter(-1);
229        }
230
231        /**
232         * Return the actual return value type.
233         */
234        public MethodParameter getReturnValueType(@Nullable Object returnValue) {
235                return new ReturnValueMethodParameter(returnValue);
236        }
237
238        /**
239         * Return {@code true} if the method return type is void, {@code false} otherwise.
240         */
241        public boolean isVoid() {
242                return Void.TYPE.equals(getReturnType().getParameterType());
243        }
244
245        /**
246         * Return a single annotation on the underlying method traversing its super methods
247         * if no annotation can be found on the given method itself.
248         * <p>Also supports <em>merged</em> composed annotations with attribute
249         * overrides as of Spring Framework 4.3.
250         * @param annotationType the type of annotation to introspect the method for
251         * @return the annotation, or {@code null} if none found
252         * @see AnnotatedElementUtils#findMergedAnnotation
253         */
254        @Nullable
255        public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
256                return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
257        }
258
259        /**
260         * Return whether the parameter is declared with the given annotation type.
261         * @param annotationType the annotation type to look for
262         * @since 4.3
263         * @see AnnotatedElementUtils#hasAnnotation
264         */
265        public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
266                return AnnotatedElementUtils.hasAnnotation(this.method, annotationType);
267        }
268
269        /**
270         * Return the HandlerMethod from which this HandlerMethod instance was
271         * resolved via {@link #createWithResolvedBean()}.
272         * @since 4.3
273         */
274        @Nullable
275        public HandlerMethod getResolvedFromHandlerMethod() {
276                return this.resolvedFromHandlerMethod;
277        }
278
279        /**
280         * If the provided instance contains a bean name rather than an object instance,
281         * the bean name is resolved before a {@link HandlerMethod} is created and returned.
282         */
283        public HandlerMethod createWithResolvedBean() {
284                Object handler = this.bean;
285                if (this.bean instanceof String) {
286                        Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
287                        String beanName = (String) this.bean;
288                        handler = this.beanFactory.getBean(beanName);
289                }
290                return new HandlerMethod(this, handler);
291        }
292
293        /**
294         * Return a short representation of this handler method for log message purposes.
295         */
296        public String getShortLogMessage() {
297                int args = this.method.getParameterCount();
298                return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]";
299        }
300
301
302        @Override
303        public boolean equals(@Nullable Object other) {
304                if (this == other) {
305                        return true;
306                }
307                if (!(other instanceof HandlerMethod)) {
308                        return false;
309                }
310                HandlerMethod otherMethod = (HandlerMethod) other;
311                return (this.bean.equals(otherMethod.bean) && this.method.equals(otherMethod.method));
312        }
313
314        @Override
315        public int hashCode() {
316                return (this.bean.hashCode() * 31 + this.method.hashCode());
317        }
318
319        @Override
320        public String toString() {
321                return this.method.toGenericString();
322        }
323
324
325        // Support methods for use in "InvocableHandlerMethod" sub-class variants..
326
327        @Nullable
328        protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) {
329                if (!ObjectUtils.isEmpty(providedArgs)) {
330                        for (Object providedArg : providedArgs) {
331                                if (parameter.getParameterType().isInstance(providedArg)) {
332                                        return providedArg;
333                                }
334                        }
335                }
336                return null;
337        }
338
339        protected static String formatArgumentError(MethodParameter param, String message) {
340                return "Could not resolve parameter [" + param.getParameterIndex() + "] in " +
341                                param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : "");
342        }
343
344        /**
345         * Assert that the target bean class is an instance of the class where the given
346         * method is declared. In some cases the actual endpoint instance at request-
347         * processing time may be a JDK dynamic proxy (lazy initialization, prototype
348         * beans, and others). Endpoint classes that require proxying should prefer
349         * class-based proxy mechanisms.
350         */
351        protected void assertTargetBean(Method method, Object targetBean, Object[] args) {
352                Class<?> methodDeclaringClass = method.getDeclaringClass();
353                Class<?> targetBeanClass = targetBean.getClass();
354                if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
355                        String text = "The mapped handler method class '" + methodDeclaringClass.getName() +
356                                        "' is not an instance of the actual endpoint bean class '" +
357                                        targetBeanClass.getName() + "'. If the endpoint requires proxying " +
358                                        "(e.g. due to @Transactional), please use class-based proxying.";
359                        throw new IllegalStateException(formatInvokeError(text, args));
360                }
361        }
362
363        protected String formatInvokeError(String text, Object[] args) {
364
365                String formattedArgs = IntStream.range(0, args.length)
366                                .mapToObj(i -> (args[i] != null ?
367                                                "[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" :
368                                                "[" + i + "] [null]"))
369                                .collect(Collectors.joining(",\n", " ", " "));
370
371                return text + "\n" +
372                                "Endpoint [" + getBeanType().getName() + "]\n" +
373                                "Method [" + getBridgedMethod().toGenericString() + "] " +
374                                "with argument values:\n" + formattedArgs;
375        }
376
377
378        /**
379         * A MethodParameter with HandlerMethod-specific behavior.
380         */
381        protected class HandlerMethodParameter extends SynthesizingMethodParameter {
382
383                public HandlerMethodParameter(int index) {
384                        super(HandlerMethod.this.bridgedMethod, index);
385                }
386
387                protected HandlerMethodParameter(HandlerMethodParameter original) {
388                        super(original);
389                }
390
391                @Override
392                public Class<?> getContainingClass() {
393                        return HandlerMethod.this.getBeanType();
394                }
395
396                @Override
397                public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) {
398                        return HandlerMethod.this.getMethodAnnotation(annotationType);
399                }
400
401                @Override
402                public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) {
403                        return HandlerMethod.this.hasMethodAnnotation(annotationType);
404                }
405
406                @Override
407                public HandlerMethodParameter clone() {
408                        return new HandlerMethodParameter(this);
409                }
410        }
411
412
413        /**
414         * A MethodParameter for a HandlerMethod return type based on an actual return value.
415         */
416        private class ReturnValueMethodParameter extends HandlerMethodParameter {
417
418                @Nullable
419                private final Object returnValue;
420
421                public ReturnValueMethodParameter(@Nullable Object returnValue) {
422                        super(-1);
423                        this.returnValue = returnValue;
424                }
425
426                protected ReturnValueMethodParameter(ReturnValueMethodParameter original) {
427                        super(original);
428                        this.returnValue = original.returnValue;
429                }
430
431                @Override
432                public Class<?> getParameterType() {
433                        return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType());
434                }
435
436                @Override
437                public ReturnValueMethodParameter clone() {
438                        return new ReturnValueMethodParameter(this);
439                }
440        }
441
442}