001/* 002 * Copyright 2002-2019 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.method.annotation; 018 019import java.lang.reflect.Method; 020import java.lang.reflect.Proxy; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.concurrent.ConcurrentHashMap; 027import javax.servlet.http.HttpServletRequest; 028import javax.servlet.http.HttpServletResponse; 029import javax.xml.transform.Source; 030 031import org.springframework.aop.support.AopUtils; 032import org.springframework.beans.factory.InitializingBean; 033import org.springframework.context.ApplicationContext; 034import org.springframework.context.ApplicationContextAware; 035import org.springframework.core.annotation.AnnotationAwareOrderComparator; 036import org.springframework.http.HttpStatus; 037import org.springframework.http.converter.ByteArrayHttpMessageConverter; 038import org.springframework.http.converter.HttpMessageConverter; 039import org.springframework.http.converter.StringHttpMessageConverter; 040import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; 041import org.springframework.http.converter.xml.SourceHttpMessageConverter; 042import org.springframework.ui.ModelMap; 043import org.springframework.web.accept.ContentNegotiationManager; 044import org.springframework.web.bind.annotation.ControllerAdvice; 045import org.springframework.web.context.request.ServletWebRequest; 046import org.springframework.web.method.ControllerAdviceBean; 047import org.springframework.web.method.HandlerMethod; 048import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; 049import org.springframework.web.method.annotation.MapMethodProcessor; 050import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; 051import org.springframework.web.method.annotation.ModelMethodProcessor; 052import org.springframework.web.method.support.HandlerMethodArgumentResolver; 053import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; 054import org.springframework.web.method.support.HandlerMethodReturnValueHandler; 055import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite; 056import org.springframework.web.method.support.ModelAndViewContainer; 057import org.springframework.web.servlet.ModelAndView; 058import org.springframework.web.servlet.View; 059import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver; 060import org.springframework.web.servlet.mvc.support.RedirectAttributes; 061import org.springframework.web.servlet.support.RequestContextUtils; 062 063/** 064 * An {@link AbstractHandlerMethodExceptionResolver} that resolves exceptions 065 * through {@code @ExceptionHandler} methods. 066 * 067 * <p>Support for custom argument and return value types can be added via 068 * {@link #setCustomArgumentResolvers} and {@link #setCustomReturnValueHandlers}. 069 * Or alternatively to re-configure all argument and return value types use 070 * {@link #setArgumentResolvers} and {@link #setReturnValueHandlers(List)}. 071 * 072 * @author Rossen Stoyanchev 073 * @author Juergen Hoeller 074 * @since 3.1 075 */ 076public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver 077 implements ApplicationContextAware, InitializingBean { 078 079 private List<HandlerMethodArgumentResolver> customArgumentResolvers; 080 081 private HandlerMethodArgumentResolverComposite argumentResolvers; 082 083 private List<HandlerMethodReturnValueHandler> customReturnValueHandlers; 084 085 private HandlerMethodReturnValueHandlerComposite returnValueHandlers; 086 087 private List<HttpMessageConverter<?>> messageConverters; 088 089 private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); 090 091 private final List<Object> responseBodyAdvice = new ArrayList<Object>(); 092 093 private ApplicationContext applicationContext; 094 095 private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache = 096 new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>(64); 097 098 private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = 099 new LinkedHashMap<ControllerAdviceBean, ExceptionHandlerMethodResolver>(); 100 101 102 public ExceptionHandlerExceptionResolver() { 103 StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); 104 stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316 105 106 this.messageConverters = new ArrayList<HttpMessageConverter<?>>(); 107 this.messageConverters.add(new ByteArrayHttpMessageConverter()); 108 this.messageConverters.add(stringHttpMessageConverter); 109 this.messageConverters.add(new SourceHttpMessageConverter<Source>()); 110 this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); 111 } 112 113 114 /** 115 * Provide resolvers for custom argument types. Custom resolvers are ordered 116 * after built-in ones. To override the built-in support for argument 117 * resolution use {@link #setArgumentResolvers} instead. 118 */ 119 public void setCustomArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { 120 this.customArgumentResolvers = argumentResolvers; 121 } 122 123 /** 124 * Return the custom argument resolvers, or {@code null}. 125 */ 126 public List<HandlerMethodArgumentResolver> getCustomArgumentResolvers() { 127 return this.customArgumentResolvers; 128 } 129 130 /** 131 * Configure the complete list of supported argument types thus overriding 132 * the resolvers that would otherwise be configured by default. 133 */ 134 public void setArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { 135 if (argumentResolvers == null) { 136 this.argumentResolvers = null; 137 } 138 else { 139 this.argumentResolvers = new HandlerMethodArgumentResolverComposite(); 140 this.argumentResolvers.addResolvers(argumentResolvers); 141 } 142 } 143 144 /** 145 * Return the configured argument resolvers, or possibly {@code null} if 146 * not initialized yet via {@link #afterPropertiesSet()}. 147 */ 148 public HandlerMethodArgumentResolverComposite getArgumentResolvers() { 149 return this.argumentResolvers; 150 } 151 152 /** 153 * Provide handlers for custom return value types. Custom handlers are 154 * ordered after built-in ones. To override the built-in support for 155 * return value handling use {@link #setReturnValueHandlers}. 156 */ 157 public void setCustomReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { 158 this.customReturnValueHandlers = returnValueHandlers; 159 } 160 161 /** 162 * Return the custom return value handlers, or {@code null}. 163 */ 164 public List<HandlerMethodReturnValueHandler> getCustomReturnValueHandlers() { 165 return this.customReturnValueHandlers; 166 } 167 168 /** 169 * Configure the complete list of supported return value types thus 170 * overriding handlers that would otherwise be configured by default. 171 */ 172 public void setReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { 173 if (returnValueHandlers == null) { 174 this.returnValueHandlers = null; 175 } 176 else { 177 this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite(); 178 this.returnValueHandlers.addHandlers(returnValueHandlers); 179 } 180 } 181 182 /** 183 * Return the configured handlers, or possibly {@code null} if not 184 * initialized yet via {@link #afterPropertiesSet()}. 185 */ 186 public HandlerMethodReturnValueHandlerComposite getReturnValueHandlers() { 187 return this.returnValueHandlers; 188 } 189 190 /** 191 * Set the message body converters to use. 192 * <p>These converters are used to convert from and to HTTP requests and responses. 193 */ 194 public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) { 195 this.messageConverters = messageConverters; 196 } 197 198 /** 199 * Return the configured message body converters. 200 */ 201 public List<HttpMessageConverter<?>> getMessageConverters() { 202 return this.messageConverters; 203 } 204 205 /** 206 * Set the {@link ContentNegotiationManager} to use to determine requested media types. 207 * If not set, the default constructor is used. 208 */ 209 public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { 210 this.contentNegotiationManager = contentNegotiationManager; 211 } 212 213 /** 214 * Return the configured {@link ContentNegotiationManager}. 215 */ 216 public ContentNegotiationManager getContentNegotiationManager() { 217 return this.contentNegotiationManager; 218 } 219 220 /** 221 * Add one or more components to be invoked after the execution of a controller 222 * method annotated with {@code @ResponseBody} or returning {@code ResponseEntity} 223 * but before the body is written to the response with the selected 224 * {@code HttpMessageConverter}. 225 */ 226 public void setResponseBodyAdvice(List<ResponseBodyAdvice<?>> responseBodyAdvice) { 227 this.responseBodyAdvice.clear(); 228 if (responseBodyAdvice != null) { 229 this.responseBodyAdvice.addAll(responseBodyAdvice); 230 } 231 } 232 233 @Override 234 public void setApplicationContext(ApplicationContext applicationContext) { 235 this.applicationContext = applicationContext; 236 } 237 238 public ApplicationContext getApplicationContext() { 239 return this.applicationContext; 240 } 241 242 243 @Override 244 public void afterPropertiesSet() { 245 // Do this first, it may add ResponseBodyAdvice beans 246 initExceptionHandlerAdviceCache(); 247 248 if (this.argumentResolvers == null) { 249 List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); 250 this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); 251 } 252 if (this.returnValueHandlers == null) { 253 List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); 254 this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); 255 } 256 } 257 258 private void initExceptionHandlerAdviceCache() { 259 if (getApplicationContext() == null) { 260 return; 261 } 262 if (logger.isDebugEnabled()) { 263 logger.debug("Looking for exception mappings: " + getApplicationContext()); 264 } 265 266 List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); 267 AnnotationAwareOrderComparator.sort(adviceBeans); 268 269 for (ControllerAdviceBean adviceBean : adviceBeans) { 270 ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType()); 271 if (resolver.hasExceptionMappings()) { 272 this.exceptionHandlerAdviceCache.put(adviceBean, resolver); 273 if (logger.isInfoEnabled()) { 274 logger.info("Detected @ExceptionHandler methods in " + adviceBean); 275 } 276 } 277 if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) { 278 this.responseBodyAdvice.add(adviceBean); 279 if (logger.isInfoEnabled()) { 280 logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean); 281 } 282 } 283 } 284 } 285 286 /** 287 * Return an unmodifiable Map with the {@link ControllerAdvice @ControllerAdvice} 288 * beans discovered in the ApplicationContext. The returned map will be empty if 289 * the method is invoked before the bean has been initialized via 290 * {@link #afterPropertiesSet()}. 291 */ 292 public Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> getExceptionHandlerAdviceCache() { 293 return Collections.unmodifiableMap(this.exceptionHandlerAdviceCache); 294 } 295 296 /** 297 * Return the list of argument resolvers to use including built-in resolvers 298 * and custom resolvers provided via {@link #setCustomArgumentResolvers}. 299 */ 300 protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { 301lerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>(); 302 303 // Annotation-based argument resolution 304 resolvers.add(new SessionAttributeMethodArgumentResolver()); 305 resolvers.add(new RequestAttributeMethodArgumentResolver()); 306 307 // Type-based argument resolution 308 resolvers.add(new ServletRequestMethodArgumentResolver()); 309 resolvers.add(new ServletResponseMethodArgumentResolver()); 310 resolvers.add(new RedirectAttributesMethodArgumentResolver()); 311 resolvers.add(new ModelMethodProcessor()); 312 313 // Custom arguments 314 if (getCustomArgumentResolvers() != null) { 315 resolvers.addAll(getCustomArgumentResolvers()); 316 } 317 318 return resolvers; 319 } 320 321 /** 322 * Return the list of return value handlers to use including built-in and 323 * custom handlers provided via {@link #setReturnValueHandlers}. 324 */ 325 protected List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() { 326 List<HandlerMethodReturnValueHandler> handlers = new ArrayList<HandlerMethodReturnValueHandler>(); 327 328 // Single-purpose return value types 329 handlers.add(new ModelAndViewMethodReturnValueHandler()); 330 handlers.add(new ModelMethodProcessor()); 331 handlers.add(new ViewMethodReturnValueHandler()); 332 handlers.add(new HttpEntityMethodProcessor( 333 getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); 334 335 // Annotation-based return value types 336 handlers.add(new ModelAttributeMethodProcessor(false)); 337 handlers.add(new RequestResponseBodyMethodProcessor( 338 getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); 339 340 // Multi-purpose return value types 341 handlers.add(new ViewNameMethodReturnValueHandler()); 342 handlers.add(new MapMethodProcessor()); 343 344 // Custom return value types 345 if (getCustomReturnValueHandlers() != null) { 346 handlers.addAll(getCustomReturnValueHandlers()); 347 } 348 349 // Catch-all 350 handlers.add(new ModelAttributeMethodProcessor(true)); 351 352 return handlers; 353 } 354 355 356 /** 357 * Find an {@code @ExceptionHandler} method and invoke it to handle the raised exception. 358 */ 359 @Override 360 protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, 361 HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) { 362 363 ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); 364 if (exceptionHandlerMethod == null) { 365 return null; 366 } 367 368 exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); 369 exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); 370 371 ServletWebRequest webRequest = new ServletWebRequest(request, response); 372 ModelAndViewContainer mavContainer = new ModelAndViewContainer(); 373 374 try { 375 if (logger.isDebugEnabled()) { 376 logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod); 377 } 378 Throwable cause = exception.getCause(); 379 if (cause != null) { 380 // Expose cause as provided argument as well 381 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod); 382 } 383 else { 384 // Otherwise, just the given exception as-is 385 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod); 386 } 387 } 388 catch (Throwable invocationEx) { 389 // Any other than the original exception is unintended here, 390 // probably an accident (e.g. failed assertion or the like). 391 if (invocationEx != exception && logger.isWarnEnabled()) { 392 logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx); 393 } 394 // Continue with default processing of the original exception... 395 return null; 396 } 397 398 if (mavContainer.isRequestHandled()) { 399 return new ModelAndView(); 400 } 401 else { 402 ModelMap model = mavContainer.getModel(); 403 HttpStatus status = mavContainer.getStatus(); 404 ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); 405 mav.setViewName(mavContainer.getViewName()); 406 if (!mavContainer.isViewReference()) { 407 mav.setView((View) mavContainer.getView()); 408 } 409 if (model instanceof RedirectAttributes) { 410 Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); 411 RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); 412 } 413 return mav; 414 } 415 } 416 417 /** 418 * Find an {@code @ExceptionHandler} method for the given exception. The default 419 * implementation searches methods in the class hierarchy of the controller first 420 * and if not found, it continues searching for additional {@code @ExceptionHandler} 421 * methods assuming some {@linkplain ControllerAdvice @ControllerAdvice} 422 * Spring-managed beans were detected. 423 * @param handlerMethod the method where the exception was raised (may be {@code null}) 424 * @param exception the raised exception 425 * @return a method to handle the exception, or {@code null} if none 426 */ 427 protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) { 428 Class<?> handlerType = null; 429 430 if (handlerMethod != null) { 431 // Local exception handler methods on the controller class itself. 432 // To be invoked through the proxy, even in case of an interface-based proxy. 433 handlerType = handlerMethod.getBeanType(); 434 ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); 435 if (resolver == null) { 436 resolver = new ExceptionHandlerMethodResolver(handlerType); 437 this.exceptionHandlerCache.put(handlerType, resolver); 438 } 439 Method method = resolver.resolveMethod(exception); 440 if (method != null) { 441 return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method); 442 } 443 // For advice applicability check below (involving base packages, assignable types 444 // and annotation presence), use target class instead of interface-based proxy. 445 if (Proxy.isProxyClass(handlerType)) { 446 handlerType = AopUtils.getTargetClass(handlerMethod.getBean()); 447 } 448 } 449 450 for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { 451 ControllerAdviceBean advice = entry.getKey(); 452 if (advice.isApplicableToBeanType(handlerType)) { 453 ExceptionHandlerMethodResolver resolver = entry.getValue(); 454 Method method = resolver.resolveMethod(exception); 455 if (method != null) { 456 return new ServletInvocableHandlerMethod(advice.resolveBean(), method); 457 } 458 } 459 } 460 461 return null; 462 } 463 464}