001/* 002 * Copyright 2002-2015 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.servlet.mvc.annotation; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.OutputStream; 022import java.io.Reader; 023import java.io.Writer; 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.security.Principal; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Locale; 033import java.util.Map; 034import java.util.concurrent.ConcurrentHashMap; 035import javax.servlet.ServletException; 036import javax.servlet.ServletRequest; 037import javax.servlet.ServletResponse; 038import javax.servlet.http.HttpServletRequest; 039import javax.servlet.http.HttpServletResponse; 040import javax.servlet.http.HttpSession; 041import javax.xml.transform.Source; 042 043import org.springframework.core.ExceptionDepthComparator; 044import org.springframework.core.GenericTypeResolver; 045import org.springframework.core.MethodParameter; 046import org.springframework.core.annotation.AnnotatedElementUtils; 047import org.springframework.core.annotation.AnnotationUtils; 048import org.springframework.core.annotation.SynthesizingMethodParameter; 049import org.springframework.http.HttpInputMessage; 050import org.springframework.http.HttpOutputMessage; 051import org.springframework.http.HttpStatus; 052import org.springframework.http.MediaType; 053import org.springframework.http.converter.ByteArrayHttpMessageConverter; 054import org.springframework.http.converter.HttpMessageConverter; 055import org.springframework.http.converter.StringHttpMessageConverter; 056import org.springframework.http.converter.xml.SourceHttpMessageConverter; 057import org.springframework.http.server.ServletServerHttpRequest; 058import org.springframework.http.server.ServletServerHttpResponse; 059import org.springframework.ui.Model; 060import org.springframework.util.ClassUtils; 061import org.springframework.util.ObjectUtils; 062import org.springframework.util.ReflectionUtils; 063import org.springframework.util.StringUtils; 064import org.springframework.web.bind.annotation.ExceptionHandler; 065import org.springframework.web.bind.annotation.ResponseBody; 066import org.springframework.web.bind.annotation.ResponseStatus; 067import org.springframework.web.bind.support.WebArgumentResolver; 068import org.springframework.web.context.request.NativeWebRequest; 069import org.springframework.web.context.request.ServletWebRequest; 070import org.springframework.web.context.request.WebRequest; 071import org.springframework.web.servlet.ModelAndView; 072import org.springframework.web.servlet.View; 073import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; 074import org.springframework.web.servlet.support.RequestContextUtils; 075 076/** 077 * Implementation of the {@link org.springframework.web.servlet.HandlerExceptionResolver} interface that handles 078 * exceptions through the {@link ExceptionHandler} annotation. 079 * 080 * <p>This exception resolver is enabled by default in the {@link org.springframework.web.servlet.DispatcherServlet}. 081 * 082 * @author Arjen Poutsma 083 * @author Juergen Hoeller 084 * @since 3.0 085 * @deprecated as of Spring 3.2, in favor of 086 * {@link org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver ExceptionHandlerExceptionResolver} 087 */ 088@Deprecated 089public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExceptionResolver { 090 091 /** 092 * Arbitrary {@link Method} reference, indicating no method found in the cache. 093 */ 094 private static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis"); 095 096 097 private final Map<Class<?>, Map<Class<? extends Throwable>, Method>> exceptionHandlerCache = 098 new ConcurrentHashMap<Class<?>, Map<Class<? extends Throwable>, Method>>(64); 099 100 private WebArgumentResolver[] customArgumentResolvers; 101 102 private HttpMessageConverter<?>[] messageConverters = 103 new HttpMessageConverter<?>[] {new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), 104 new SourceHttpMessageConverter<Source>(), 105 new org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter()}; 106 107 108 /** 109 * Set a custom ArgumentResolvers to use for special method parameter types. 110 * <p>Such a custom ArgumentResolver will kick in first, having a chance to resolve 111 * an argument value before the standard argument handling kicks in. 112 */ 113 public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { 114 this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver}; 115 } 116 117 /** 118 * Set one or more custom ArgumentResolvers to use for special method parameter types. 119 * <p>Any such custom ArgumentResolver will kick in first, having a chance to resolve 120 * an argument value before the standard argument handling kicks in. 121 */ 122 public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) { 123 this.customArgumentResolvers = argumentResolvers; 124 } 125 126 /** 127 * Set the message body converters to use. 128 * <p>These converters are used to convert from and to HTTP requests and responses. 129 */ 130 public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) { 131 this.messageConverters = messageConverters; 132 } 133 134 135 @Override 136 protected ModelAndView doResolveException( 137 HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { 138 139 if (handler != null) { 140 Method handlerMethod = findBestExceptionHandlerMethod(handler, ex); 141 if (handlerMethod != null) { 142 ServletWebRequest webRequest = new ServletWebRequest(request, response); 143 try { 144 Object[] args = resolveHandlerArguments(handlerMethod, handler, webRequest, ex); 145 if (logger.isDebugEnabled()) { 146 logger.debug("Invoking request handler method: " + handlerMethod); 147 } 148 Object retVal = doInvokeMethod(handlerMethod, handler, args); 149 return getModelAndView(handlerMethod, retVal, webRequest); 150 } 151 catch (Exception invocationEx) { 152 logger.error("Invoking request method resulted in exception : " + handlerMethod, invocationEx); 153 } 154 } 155 } 156 return null; 157 } 158 159 /** 160 * Finds the handler method that matches the thrown exception best. 161 * @param handler the handler object 162 * @param thrownException the exception to be handled 163 * @return the best matching method; or {@code null} if none is found 164 */ 165 private Method findBestExceptionHandlerMethod(Object handler, final Exception thrownException) { 166 final Class<?> handlerType = ClassUtils.getUserClass(handler); 167 final Class<? extends Throwable> thrownExceptionType = thrownException.getClass(); 168 Method handlerMethod = null; 169 170 Map<Class<? extends Throwable>, Method> handlers = this.exceptionHandlerCache.get(handlerType); 171 if (handlers != null) { 172 handlerMethod = handlers.get(thrownExceptionType); 173 if (handlerMethod != null) { 174 return (handlerMethod == NO_METHOD_FOUND ? null : handlerMethod); 175 } 176 } 177 else { 178 handlers = new ConcurrentHashMap<Class<? extends Throwable>, Method>(16); 179 this.exceptionHandlerCache.put(handlerType, handlers); 180 } 181 182 final Map<Class<? extends Throwable>, Method> matchedHandlers = new HashMap<Class<? extends Throwable>, Method>(); 183 184 ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() { 185 @Override 186 public void doWith(Method method) { 187 method = ClassUtils.getMostSpecificMethod(method, handlerType); 188 List<Class<? extends Throwable>> handledExceptions = getHandledExceptions(method); 189 for (Class<? extends Throwable> handledException : handledExceptions) { 190 if (handledException.isAssignableFrom(thrownExceptionType)) { 191 if (!matchedHandlers.containsKey(handledException)) { 192 matchedHandlers.put(handledException, method); 193 } 194 else { 195 Method oldMappedMethod = matchedHandlers.get(handledException); 196 if (!oldMappedMethod.equals(method)) { 197 throw new IllegalStateException( 198 "Ambiguous exception handler mapped for " + handledException + "]: {" + 199 oldMappedMethod + ", " + method + "}."); 200 } 201 } 202 } 203 } 204 } 205 }); 206 207 handlerMethod = getBestMatchingMethod(matchedHandlers, thrownException); 208 handlers.put(thrownExceptionType, (handlerMethod == null ? NO_METHOD_FOUND : handlerMethod)); 209 return handlerMethod; 210 } 211 212 /** 213 * Returns all the exception classes handled by the given method. 214 * <p>The default implementation looks for exceptions in the annotation, 215 * or - if that annotation element is empty - any exceptions listed in the method parameters if the method 216 * is annotated with {@code @ExceptionHandler}. 217 * @param method the method 218 * @return the handled exceptions 219 */ 220 @SuppressWarnings("unchecked") 221 protected List<Class<? extends Throwable>> getHandledExceptions(Method method) { 222 List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>(); 223 ExceptionHandler exceptionHandler = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); 224 if (exceptionHandler != null) { 225 if (!ObjectUtils.isEmpty(exceptionHandler.value())) { 226 result.addAll(Arrays.asList(exceptionHandler.value())); 227 } 228 else { 229 for (Class<?> param : method.getParameterTypes()) { 230 if (Throwable.class.isAssignableFrom(param)) { 231 result.add((Class<? extends Throwable>) param); 232 } 233 } 234 } 235 } 236 return result; 237 } 238 239 /** 240 * Uses the {@link ExceptionDepthComparator} to find the best matching method. 241 * @return the best matching method, or {@code null} if none found 242 */ 243 private Method getBestMatchingMethod( 244 Map<Class<? extends Throwable>, Method> resolverMethods, Exception thrownException) { 245 246 if (resolverMethods.isEmpty()) { 247 return null; 248 } 249 Class<? extends Throwable> closestMatch = 250 ExceptionDepthComparator.findClosestMatch(resolverMethods.keySet(), thrownException); 251 Method method = resolverMethods.get(closestMatch); 252 return ((method == null) || (NO_METHOD_FOUND == method)) ? null : method; 253 } 254 255 /** 256 * Resolves the arguments for the given method. Delegates to {@link #resolveCommonArgument}. 257 */ 258 private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, 259 NativeWebRequest webRequest, Exception thrownException) throws Exception { 260 261 Class<?>[] paramTypes = handlerMethod.getParameterTypes(); 262 Object[] args = new Object[paramTypes.length]; 263 Class<?> handlerType = handler.getClass(); 264 for (int i = 0; i < args.length; i++) { 265 MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i); 266 GenericTypeResolver.resolveParameterType(methodParam, handlerType); 267 Class<?> paramType = methodParam.getParameterType(); 268 Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException); 269 if (argValue != WebArgumentResolver.UNRESOLVED) { 270 args[i] = argValue; 271 } 272 else { 273 throw new IllegalStateException("Unsupported argument [" + paramType.getName() + 274 "] for @ExceptionHandler method: " + handlerMethod); 275 } 276 } 277 return args; 278 } 279 280 /** 281 * Resolves common method arguments. Delegates to registered {@link #setCustomArgumentResolver(WebArgumentResolver) 282 * argumentResolvers} first, then checking {@link #resolveStandardArgument}. 283 * @param methodParameter the method parameter 284 * @param webRequest the request 285 * @param thrownException the exception thrown 286 * @return the argument value, or {@link WebArgumentResolver#UNRESOLVED} 287 */ 288 protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest, 289 Exception thrownException) throws Exception { 290 291 // Invoke custom argument resolvers if present... 292 if (this.customArgumentResolvers != null) { 293 for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) { 294 Object value = argumentResolver.resolveArgument(methodParameter, webRequest); 295 if (value != WebArgumentResolver.UNRESOLVED) { 296 return value; 297 } 298 } 299 } 300 301 // Resolution of standard parameter types... 302 Class<?> paramType = methodParameter.getParameterType(); 303 Object value = resolveStandardArgument(paramType, webRequest, thrownException); 304 if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) { 305 throw new IllegalStateException( 306 "Standard argument type [" + paramType.getName() + "] resolved to incompatible value of type [" + 307 (value != null ? value.getClass() : null) + 308 "]. Consider declaring the argument type in a less specific fashion."); 309 } 310 return value; 311 } 312 313 /** 314 * Resolves standard method arguments. The default implementation handles {@link NativeWebRequest}, 315 * {@link ServletRequest}, {@link ServletResponse}, {@link HttpSession}, {@link Principal}, 316 * {@link Locale}, request {@link InputStream}, request {@link Reader}, response {@link OutputStream}, 317 * response {@link Writer}, and the given {@code thrownException}. 318 * @param parameterType the method parameter type 319 * @param webRequest the request 320 * @param thrownException the exception thrown 321 * @return the argument value, or {@link WebArgumentResolver#UNRESOLVED} 322 */ 323 protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest, 324 Exception thrownException) throws Exception { 325 326 if (parameterType.isInstance(thrownException)) { 327 return thrownException; 328 } 329 else if (WebRequest.class.isAssignableFrom(parameterType)) { 330 return webRequest; 331 } 332 333 HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); 334 HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); 335 336 if (ServletRequest.class.isAssignableFrom(parameterType)) { 337 return request; 338 } 339 else if (ServletResponse.class.isAssignableFrom(parameterType)) { 340 return response; 341 } 342 else if (HttpSession.class.isAssignableFrom(parameterType)) { 343 return request.getSession(); 344 } 345 else if (Principal.class.isAssignableFrom(parameterType)) { 346 return request.getUserPrincipal(); 347 } 348 else if (Locale.class == parameterType) { 349 return RequestContextUtils.getLocale(request); 350 } 351 else if (InputStream.class.isAssignableFrom(parameterType)) { 352 return request.getInputStream(); 353 } 354 else if (Reader.class.isAssignableFrom(parameterType)) { 355 return request.getReader(); 356 } 357 else if (OutputStream.class.isAssignableFrom(parameterType)) { 358 return response.getOutputStream(); 359 } 360 else if (Writer.class.isAssignableFrom(parameterType)) { 361 return response.getWriter(); 362 } 363 else { 364 return WebArgumentResolver.UNRESOLVED; 365 366 } 367 } 368 369 private Object doInvokeMethod(Method method, Object target, Object[] args) throws Exception { 370 ReflectionUtils.makeAccessible(method); 371 try { 372 return method.invoke(target, args); 373 } 374 catch (InvocationTargetException ex) { 375 ReflectionUtils.rethrowException(ex.getTargetException()); 376 } 377 throw new IllegalStateException("Should never get here"); 378 } 379 380 @SuppressWarnings("unchecked") 381 private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest) 382 throws Exception { 383 384 ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class); 385 if (responseStatus != null) { 386 HttpStatus statusCode = responseStatus.code(); 387 String reason = responseStatus.reason(); 388 if (!StringUtils.hasText(reason)) { 389 webRequest.getResponse().setStatus(statusCode.value()); 390 } 391 else { 392 webRequest.getResponse().sendError(statusCode.value(), reason); 393 } 394 } 395 396 if (returnValue != null && AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class) != null) { 397 return handleResponseBody(returnValue, webRequest); 398 } 399 400 if (returnValue instanceof ModelAndView) { 401 return (ModelAndView) returnValue; 402 } 403 else if (returnValue instanceof Model) { 404 return new ModelAndView().addAllObjects(((Model) returnValue).asMap()); 405 } 406 else if (returnValue instanceof Map) { 407 return new ModelAndView().addAllObjects((Map<String, Object>) returnValue); 408 } 409 else if (returnValue instanceof View) { 410 return new ModelAndView((View) returnValue); 411 } 412 else if (returnValue instanceof String) { 413 return new ModelAndView((String) returnValue); 414 } 415 else if (returnValue == null) { 416 return new ModelAndView(); 417 } 418 else { 419 throw new IllegalArgumentException("Invalid handler method return value: " + returnValue); 420 } 421 } 422 423 @SuppressWarnings({ "unchecked", "rawtypes", "resource" }) 424 private ModelAndView handleResponseBody(Object returnValue, ServletWebRequest webRequest) 425 throws ServletException, IOException { 426 427 HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest()); 428 List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept(); 429 if (acceptedMediaTypes.isEmpty()) { 430 acceptedMediaTypes = Collections.singletonList(MediaType.ALL); 431 } 432 MediaType.sortByQualityValue(acceptedMediaTypes); 433 HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse()); 434 Class<?> returnValueType = returnValue.getClass(); 435 if (this.messageConverters != null) { 436 for (MediaType acceptedMediaType : acceptedMediaTypes) { 437 for (HttpMessageConverter messageConverter : this.messageConverters) { 438 if (messageConverter.canWrite(returnValueType, acceptedMediaType)) { 439 messageConverter.write(returnValue, acceptedMediaType, outputMessage); 440 return new ModelAndView(); 441 } 442 } 443 } 444 } 445 if (logger.isWarnEnabled()) { 446 logger.warn("Could not find HttpMessageConverter that supports return type [" + returnValueType + "] and " + 447 acceptedMediaTypes); 448 } 449 return null; 450 } 451 452}