001/*
002 * Copyright 2002-2018 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.invocation;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.lang.reflect.Type;
022import java.util.Arrays;
023
024import org.springframework.core.DefaultParameterNameDiscoverer;
025import org.springframework.core.MethodParameter;
026import org.springframework.core.ParameterNameDiscoverer;
027import org.springframework.core.ResolvableType;
028import org.springframework.messaging.Message;
029import org.springframework.messaging.handler.HandlerMethod;
030import org.springframework.util.ClassUtils;
031import org.springframework.util.ReflectionUtils;
032
033/**
034 * Provides a method for invoking the handler method for a given message after resolving its
035 * method argument values through registered {@link HandlerMethodArgumentResolver}s.
036 *
037 * <p>Use {@link #setMessageMethodArgumentResolvers} to customize the list of argument resolvers.
038 *
039 * @author Rossen Stoyanchev
040 * @author Juergen Hoeller
041 * @since 4.0
042 */
043public class InvocableHandlerMethod extends HandlerMethod {
044
045        private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
046
047        private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
048
049
050        /**
051         * Create an instance from a {@code HandlerMethod}.
052         */
053        public InvocableHandlerMethod(HandlerMethod handlerMethod) {
054                super(handlerMethod);
055        }
056
057        /**
058         * Create an instance from a bean instance and a method.
059         */
060        public InvocableHandlerMethod(Object bean, Method method) {
061                super(bean, method);
062        }
063
064        /**
065         * Construct a new handler method with the given bean instance, method name and parameters.
066         * @param bean the object bean
067         * @param methodName the method name
068         * @param parameterTypes the method parameter types
069         * @throws NoSuchMethodException when the method cannot be found
070         */
071        public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
072                        throws NoSuchMethodException {
073
074                super(bean, methodName, parameterTypes);
075        }
076
077
078        /**
079         * Set {@link HandlerMethodArgumentResolver}s to use to use for resolving method argument values.
080         */
081        public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
082                this.argumentResolvers = argumentResolvers;
083        }
084
085        /**
086         * Set the ParameterNameDiscoverer for resolving parameter names when needed
087         * (e.g. default request attribute name).
088         * <p>Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}.
089         */
090        public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
091                this.parameterNameDiscoverer = parameterNameDiscoverer;
092        }
093
094
095        /**
096         * Invoke the method after resolving its argument values in the context of the given message.
097         * <p>Argument values are commonly resolved through {@link HandlerMethodArgumentResolver}s.
098         * The {@code providedArgs} parameter however may supply argument values to be used directly,
099         * i.e. without argument resolution.
100         * @param message the current message being processed
101         * @param providedArgs "given" arguments matched by type, not resolved
102         * @return the raw value returned by the invoked method
103         * @throws Exception raised if no suitable argument resolver can be found,
104         * or if the method raised an exception
105         */
106        public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
107                Object[] args = getMethodArgumentValues(message, providedArgs);
108                if (logger.isTraceEnabled()) {
109                        logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) +
110                                        "' with arguments " + Arrays.toString(args));
111                }
112                Object returnValue = doInvoke(args);
113                if (logger.isTraceEnabled()) {
114                        logger.trace("Method [" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) +
115                                        "] returned [" + returnValue + "]");
116                }
117                return returnValue;
118        }
119
120        /**
121         * Get the method argument values for the current message.
122         */
123        private Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
124                MethodParameter[] parameters = getMethodParameters();
125                Object[] args = new Object[parameters.length];
126                for (int i = 0; i < parameters.length; i++) {
127                        MethodParameter parameter = parameters[i];
128                        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
129                        args[i] = resolveProvidedArgument(parameter, providedArgs);
130                        if (args[i] != null) {
131                                continue;
132                        }
133                        if (this.argumentResolvers.supportsParameter(parameter)) {
134                                try {
135                                        args[i] = this.argumentResolvers.resolveArgument(parameter, message);
136                                        continue;
137                                }
138                                catch (Exception ex) {
139                                        if (logger.isDebugEnabled()) {
140                                                logger.debug(getArgumentResolutionErrorMessage("Failed to resolve", i), ex);
141                                        }
142                                        throw ex;
143                                }
144                        }
145                        if (args[i] == null) {
146                                throw new MethodArgumentResolutionException(message, parameter,
147                                                getArgumentResolutionErrorMessage("No suitable resolver for", i));
148                        }
149                }
150                return args;
151        }
152
153        private String getArgumentResolutionErrorMessage(String text, int index) {
154                Class<?> paramType = getMethodParameters()[index].getParameterType();
155                return text + " argument " + index + " of type '" + paramType.getName() + "'";
156        }
157
158        /**
159         * Attempt to resolve a method parameter from the list of provided argument values.
160         */
161        private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) {
162                if (providedArgs == null) {
163                        return null;
164                }
165                for (Object providedArg : providedArgs) {
166                        if (parameter.getParameterType().isInstance(providedArg)) {
167                                return providedArg;
168                        }
169                }
170                return null;
171        }
172
173
174        /**
175         * Invoke the handler method with the given argument values.
176         */
177        protected Object doInvoke(Object... args) throws Exception {
178                ReflectionUtils.makeAccessible(getBridgedMethod());
179                try {
180                        return getBridgedMethod().invoke(getBean(), args);
181                }
182                catch (IllegalArgumentException ex) {
183                        assertTargetBean(getBridgedMethod(), getBean(), args);
184                        String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
185                        throw new IllegalStateException(getInvocationErrorMessage(text, args), ex);
186                }
187                catch (InvocationTargetException ex) {
188                        // Unwrap for HandlerExceptionResolvers ...
189                        Throwable targetException = ex.getTargetException();
190                        if (targetException instanceof RuntimeException) {
191                                throw (RuntimeException) targetException;
192                        }
193                        else if (targetException instanceof Error) {
194                                throw (Error) targetException;
195                        }
196                        else if (targetException instanceof Exception) {
197                                throw (Exception) targetException;
198                        }
199                        else {
200                                String text = getInvocationErrorMessage("Failed to invoke handler method", args);
201                                throw new IllegalStateException(text, targetException);
202                        }
203                }
204        }
205
206        /**
207         * Assert that the target bean class is an instance of the class where the given
208         * method is declared. In some cases the actual endpoint instance at request-
209         * processing time may be a JDK dynamic proxy (lazy initialization, prototype
210         * beans, and others). Endpoint classes that require proxying should prefer
211         * class-based proxy mechanisms.
212         */
213        private void assertTargetBean(Method method, Object targetBean, Object[] args) {
214                Class<?> methodDeclaringClass = method.getDeclaringClass();
215                Class<?> targetBeanClass = targetBean.getClass();
216                if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
217                        String text = "The mapped handler method class '" + methodDeclaringClass.getName() +
218                                        "' is not an instance of the actual endpoint bean class '" +
219                                        targetBeanClass.getName() + "'. If the endpoint requires proxying " +
220                                        "(e.g. due to @Transactional), please use class-based proxying.";
221                        throw new IllegalStateException(getInvocationErrorMessage(text, args));
222                }
223        }
224
225        private String getInvocationErrorMessage(String text, Object[] resolvedArgs) {
226                StringBuilder sb = new StringBuilder(getDetailedErrorMessage(text));
227                sb.append("Resolved arguments: \n");
228                for (int i = 0; i < resolvedArgs.length; i++) {
229                        sb.append("[").append(i).append("] ");
230                        if (resolvedArgs[i] == null) {
231                                sb.append("[null] \n");
232                        }
233                        else {
234                                sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
235                                sb.append("[value=").append(resolvedArgs[i]).append("]\n");
236                        }
237                }
238                return sb.toString();
239        }
240
241        /**
242         * Adds HandlerMethod details such as the bean type and method signature to the message.
243         * @param text error message to append the HandlerMethod details to
244         */
245        protected String getDetailedErrorMessage(String text) {
246                StringBuilder sb = new StringBuilder(text).append("\n");
247                sb.append("HandlerMethod details: \n");
248                sb.append("Endpoint [").append(getBeanType().getName()).append("]\n");
249                sb.append("Method [").append(getBridgedMethod().toGenericString()).append("]\n");
250                return sb.toString();
251        }
252
253
254        MethodParameter getAsyncReturnValueType(Object returnValue) {
255                return new AsyncResultMethodParameter(returnValue);
256        }
257
258
259        private class AsyncResultMethodParameter extends HandlerMethodParameter {
260
261                private final Object returnValue;
262
263                private final ResolvableType returnType;
264
265                public AsyncResultMethodParameter(Object returnValue) {
266                        super(-1);
267                        this.returnValue = returnValue;
268                        this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
269                }
270
271                protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
272                        super(original);
273                        this.returnValue = original.returnValue;
274                        this.returnType = original.returnType;
275                }
276
277                @Override
278                public Class<?> getParameterType() {
279                        if (this.returnValue != null) {
280                                return this.returnValue.getClass();
281                        }
282                        if (!ResolvableType.NONE.equals(this.returnType)) {
283                                return this.returnType.resolve(Object.class);
284                        }
285                        return super.getParameterType();
286                }
287
288                @Override
289                public Type getGenericParameterType() {
290                        return this.returnType.getType();
291                }
292
293                @Override
294                public AsyncResultMethodParameter clone() {
295                        return new AsyncResultMethodParameter(this);
296                }
297        }
298
299}