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}