View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.method.support;
18  
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.Method;
21  import java.util.Arrays;
22  
23  import org.springframework.core.DefaultParameterNameDiscoverer;
24  import org.springframework.core.GenericTypeResolver;
25  import org.springframework.core.MethodParameter;
26  import org.springframework.core.ParameterNameDiscoverer;
27  import org.springframework.util.ReflectionUtils;
28  import org.springframework.web.bind.WebDataBinder;
29  import org.springframework.web.bind.support.SessionStatus;
30  import org.springframework.web.bind.support.WebDataBinderFactory;
31  import org.springframework.web.context.request.NativeWebRequest;
32  import org.springframework.web.method.HandlerMethod;
33  
34  /**
35   * Provides a method for invoking the handler method for a given request after resolving its
36   * method argument values through registered {@link HandlerMethodArgumentResolver}s.
37   *
38   * <p>Argument resolution often requires a {@link WebDataBinder} for data binding or for type
39   * conversion. Use the {@link #setDataBinderFactory(WebDataBinderFactory)} property to supply
40   * a binder factory to pass to argument resolvers.
41   *
42   * <p>Use {@link #setHandlerMethodArgumentResolvers(HandlerMethodArgumentResolverComposite)}
43   * to customize the list of argument resolvers.
44   *
45   * @author Rossen Stoyanchev
46   * @author Juergen Hoeller
47   * @since 3.1
48   */
49  public class InvocableHandlerMethod extends HandlerMethod {
50  
51  	private WebDataBinderFactory dataBinderFactory;
52  
53  	private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
54  
55  	private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
56  
57  
58  	/**
59  	 * Create an instance from the given handler and method.
60  	 */
61  	public InvocableHandlerMethod(Object bean, Method method) {
62  		super(bean, method);
63  	}
64  
65  	/**
66  	 * Create an instance from a {@code HandlerMethod}.
67  	 */
68  	public InvocableHandlerMethod(HandlerMethod handlerMethod) {
69  		super(handlerMethod);
70  	}
71  
72  	/**
73  	 * Construct a new handler method with the given bean instance, method name and parameters.
74  	 * @param bean the object bean
75  	 * @param methodName the method name
76  	 * @param parameterTypes the method parameter types
77  	 * @throws NoSuchMethodException when the method cannot be found
78  	 */
79  	public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
80  			throws NoSuchMethodException {
81  
82  		super(bean, methodName, parameterTypes);
83  	}
84  
85  
86  	/**
87  	 * Set the {@link WebDataBinderFactory} to be passed to argument resolvers allowing them to create
88  	 * a {@link WebDataBinder} for data binding and type conversion purposes.
89  	 * @param dataBinderFactory the data binder factory.
90  	 */
91  	public void setDataBinderFactory(WebDataBinderFactory dataBinderFactory) {
92  		this.dataBinderFactory = dataBinderFactory;
93  	}
94  
95  	/**
96  	 * Set {@link HandlerMethodArgumentResolver}s to use to use for resolving method argument values.
97  	 */
98  	public void setHandlerMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
99  		this.argumentResolvers = argumentResolvers;
100 	}
101 
102 	/**
103 	 * Set the ParameterNameDiscoverer for resolving parameter names when needed
104 	 * (e.g. default request attribute name).
105 	 * <p>Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}.
106 	 */
107 	public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
108 		this.parameterNameDiscoverer = parameterNameDiscoverer;
109 	}
110 
111 
112 	/**
113 	 * Invoke the method after resolving its argument values in the context of the given request.
114 	 * <p>Argument values are commonly resolved through {@link HandlerMethodArgumentResolver}s.
115 	 * The {@code provideArgs} parameter however may supply argument values to be used directly,
116 	 * i.e. without argument resolution. Examples of provided argument values include a
117 	 * {@link WebDataBinder}, a {@link SessionStatus}, or a thrown exception instance.
118 	 * Provided argument values are checked before argument resolvers.
119 	 * @param request the current request
120 	 * @param mavContainer the ModelAndViewContainer for this request
121 	 * @param providedArgs "given" arguments matched by type, not resolved
122 	 * @return the raw value returned by the invoked method
123 	 * @exception Exception raised if no suitable argument resolver can be found,
124 	 * or if the method raised an exception
125 	 */
126 	public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,
127 			Object... providedArgs) throws Exception {
128 
129 		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
130 		if (logger.isTraceEnabled()) {
131 			StringBuilder sb = new StringBuilder("Invoking [");
132 			sb.append(getBeanType().getSimpleName()).append(".");
133 			sb.append(getMethod().getName()).append("] method with arguments ");
134 			sb.append(Arrays.asList(args));
135 			logger.trace(sb.toString());
136 		}
137 		Object returnValue = doInvoke(args);
138 		if (logger.isTraceEnabled()) {
139 			logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");
140 		}
141 		return returnValue;
142 	}
143 
144 	/**
145 	 * Get the method argument values for the current request.
146 	 */
147 	private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,
148 			Object... providedArgs) throws Exception {
149 
150 		MethodParameter[] parameters = getMethodParameters();
151 		Object[] args = new Object[parameters.length];
152 		for (int i = 0; i < parameters.length; i++) {
153 			MethodParameter parameter = parameters[i];
154 			parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
155 			GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
156 			args[i] = resolveProvidedArgument(parameter, providedArgs);
157 			if (args[i] != null) {
158 				continue;
159 			}
160 			if (this.argumentResolvers.supportsParameter(parameter)) {
161 				try {
162 					args[i] = this.argumentResolvers.resolveArgument(
163 							parameter, mavContainer, request, this.dataBinderFactory);
164 					continue;
165 				}
166 				catch (Exception ex) {
167 					if (logger.isTraceEnabled()) {
168 						logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
169 					}
170 					throw ex;
171 				}
172 			}
173 			if (args[i] == null) {
174 				String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
175 				throw new IllegalStateException(msg);
176 			}
177 		}
178 		return args;
179 	}
180 
181 	private String getArgumentResolutionErrorMessage(String message, int index) {
182 		MethodParameter param = getMethodParameters()[index];
183 		message += " [" + index + "] [type=" + param.getParameterType().getName() + "]";
184 		return getDetailedErrorMessage(message);
185 	}
186 
187 	/**
188 	 * Adds HandlerMethod details such as the controller type and method signature to the given error message.
189 	 * @param message error message to append the HandlerMethod details to
190 	 */
191 	protected String getDetailedErrorMessage(String message) {
192 		StringBuilder sb = new StringBuilder(message).append("\n");
193 		sb.append("HandlerMethod details: \n");
194 		sb.append("Controller [").append(getBeanType().getName()).append("]\n");
195 		sb.append("Method [").append(getBridgedMethod().toGenericString()).append("]\n");
196 		return sb.toString();
197 	}
198 
199 	/**
200 	 * Attempt to resolve a method parameter from the list of provided argument values.
201 	 */
202 	private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) {
203 		if (providedArgs == null) {
204 			return null;
205 		}
206 		for (Object providedArg : providedArgs) {
207 			if (parameter.getParameterType().isInstance(providedArg)) {
208 				return providedArg;
209 			}
210 		}
211 		return null;
212 	}
213 
214 
215 	/**
216 	 * Invoke the handler method with the given argument values.
217 	 */
218 	protected Object doInvoke(Object... args) throws Exception {
219 		ReflectionUtils.makeAccessible(getBridgedMethod());
220 		try {
221 			return getBridgedMethod().invoke(getBean(), args);
222 		}
223 		catch (IllegalArgumentException ex) {
224 			assertTargetBean(getBridgedMethod(), getBean(), args);
225 			throw new IllegalStateException(getInvocationErrorMessage(ex.getMessage(), args), ex);
226 		}
227 		catch (InvocationTargetException ex) {
228 			// Unwrap for HandlerExceptionResolvers ...
229 			Throwable targetException = ex.getTargetException();
230 			if (targetException instanceof RuntimeException) {
231 				throw (RuntimeException) targetException;
232 			}
233 			else if (targetException instanceof Error) {
234 				throw (Error) targetException;
235 			}
236 			else if (targetException instanceof Exception) {
237 				throw (Exception) targetException;
238 			}
239 			else {
240 				String msg = getInvocationErrorMessage("Failed to invoke controller method", args);
241 				throw new IllegalStateException(msg, targetException);
242 			}
243 		}
244 	}
245 
246 	/**
247 	 * Assert that the target bean class is an instance of the class where the given
248 	 * method is declared. In some cases the actual controller instance at request-
249 	 * processing time may be a JDK dynamic proxy (lazy initialization, prototype
250 	 * beans, and others). {@code @Controller}'s that require proxying should prefer
251 	 * class-based proxy mechanisms.
252 	 */
253 	private void assertTargetBean(Method method, Object targetBean, Object[] args) {
254 		Class<?> methodDeclaringClass = method.getDeclaringClass();
255 		Class<?> targetBeanClass = targetBean.getClass();
256 		if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
257 			String msg = "The mapped controller method class '" + methodDeclaringClass.getName() +
258 					"' is not an instance of the actual controller bean instance '" +
259 					targetBeanClass.getName() + "'. If the controller requires proxying " +
260 					"(e.g. due to @Transactional), please use class-based proxying.";
261 			throw new IllegalStateException(getInvocationErrorMessage(msg, args));
262 		}
263 	}
264 
265 	private String getInvocationErrorMessage(String message, Object[] resolvedArgs) {
266 		StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message));
267 		sb.append("Resolved arguments: \n");
268 		for (int i=0; i < resolvedArgs.length; i++) {
269 			sb.append("[").append(i).append("] ");
270 			if (resolvedArgs[i] == null) {
271 				sb.append("[null] \n");
272 			}
273 			else {
274 				sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
275 				sb.append("[value=").append(resolvedArgs[i]).append("]\n");
276 			}
277 		}
278 		return sb.toString();
279 	}
280 
281 }