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