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