From 318d04cd2b519b539a1acfff2ee7507d7aff6873 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 May 2018 22:30:26 +0200 Subject: [PATCH] Explicit coverage of root vs cause exception matching in MVC ref docs Issue: SPR-16743 (cherry picked from commit a200df6) --- .../AnnotationDrivenBeanDefinitionParser.java | 109 +++++++----------- .../WebMvcConfigurationSupport.java | 23 ++-- src/asciidoc/web-mvc.adoc | 51 +++++--- 3 files changed, 83 insertions(+), 100 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index 8289e043aa..fc65da93b8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; @@ -53,13 +52,10 @@ import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConvert import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.springframework.web.HttpRequestHandler; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManagerFactoryBean; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.WebArgumentResolver; import org.springframework.web.method.support.CompositeUriComponentsContributor; @@ -110,10 +106,10 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv * *

This class registers the following {@link HandlerExceptionResolver}s: *

@@ -155,6 +151,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { public static final String CONTENT_NEGOTIATION_MANAGER_BEAN_NAME = "mvcContentNegotiationManager"; + private static final boolean javaxValidationPresent = ClassUtils.isPresent("javax.validation.Validator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); @@ -210,8 +207,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { configurePathMatchingProperties(handlerMappingDef, element, parserContext); readerContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME , handlerMappingDef); - RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); - handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); + RuntimeBeanReference corsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); + handlerMappingDef.getPropertyValues().add("corsConfigurations", corsRef); RuntimeBeanReference conversionService = getConversionService(element, source, parserContext); RuntimeBeanReference validator = getValidator(element, source, parserContext); @@ -285,44 +282,40 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef); String mappedInterceptorName = readerContext.registerWithGeneratedName(mappedCsInterceptorDef); - RootBeanDefinition exceptionHandlerExceptionResolver = new RootBeanDefinition(ExceptionHandlerExceptionResolver.class); - exceptionHandlerExceptionResolver.setSource(source); - exceptionHandlerExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - exceptionHandlerExceptionResolver.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); - exceptionHandlerExceptionResolver.getPropertyValues().add("messageConverters", messageConverters); - exceptionHandlerExceptionResolver.getPropertyValues().add("order", 0); - addResponseBodyAdvice(exceptionHandlerExceptionResolver); - + RootBeanDefinition methodExceptionResolver = new RootBeanDefinition(ExceptionHandlerExceptionResolver.class); + methodExceptionResolver.setSource(source); + methodExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + methodExceptionResolver.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); + methodExceptionResolver.getPropertyValues().add("messageConverters", messageConverters); + methodExceptionResolver.getPropertyValues().add("order", 0); + addResponseBodyAdvice(methodExceptionResolver); if (argumentResolvers != null) { - exceptionHandlerExceptionResolver.getPropertyValues().add("customArgumentResolvers", argumentResolvers); + methodExceptionResolver.getPropertyValues().add("customArgumentResolvers", argumentResolvers); } if (returnValueHandlers != null) { - exceptionHandlerExceptionResolver.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers); + methodExceptionResolver.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers); } + String methodExResolverName = readerContext.registerWithGeneratedName(methodExceptionResolver); - String methodExceptionResolverName = readerContext.registerWithGeneratedName(exceptionHandlerExceptionResolver); - - RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class); - responseStatusExceptionResolver.setSource(source); - responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - responseStatusExceptionResolver.getPropertyValues().add("order", 1); - String responseStatusExceptionResolverName = - readerContext.registerWithGeneratedName(responseStatusExceptionResolver); + RootBeanDefinition statusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class); + statusExceptionResolver.setSource(source); + statusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + statusExceptionResolver.getPropertyValues().add("order", 1); + String statusExResolverName = readerContext.registerWithGeneratedName(statusExceptionResolver); RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class); defaultExceptionResolver.setSource(source); defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); defaultExceptionResolver.getPropertyValues().add("order", 2); - String defaultExceptionResolverName = - readerContext.registerWithGeneratedName(defaultExceptionResolver); + String defaultExResolverName = readerContext.registerWithGeneratedName(defaultExceptionResolver); parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME)); parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); - parserContext.registerComponent(new BeanComponentDefinition(uriCompContribDef, uriCompContribName)); - parserContext.registerComponent(new BeanComponentDefinition(exceptionHandlerExceptionResolver, methodExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName)); + parserContext.registerComponent(new BeanComponentDefinition(uriContributorDef, uriContributorName)); + parserContext.registerComponent(new BeanComponentDefinition(mappedInterceptorDef, mappedInterceptorName)); + parserContext.registerComponent(new BeanComponentDefinition(methodExceptionResolver, methodExResolverName)); + parserContext.registerComponent(new BeanComponentDefinition(statusExceptionResolver, statusExResolverName)); + parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExResolverName)); // Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off" MvcNamespaceUtils.registerDefaultComponents(parserContext, source); @@ -380,8 +373,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { } } - private RuntimeBeanReference getContentNegotiationManager(Element element, Object source, - ParserContext parserContext) { + private RuntimeBeanReference getContentNegotiationManager( + Element element, Object source, ParserContext parserContext) { RuntimeBeanReference beanRef; if (element.hasAttribute("content-negotiation-manager")) { @@ -402,8 +395,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { return beanRef; } - private void configurePathMatchingProperties(RootBeanDefinition handlerMappingDef, Element element, - ParserContext parserContext) { + private void configurePathMatchingProperties( + RootBeanDefinition handlerMappingDef, Element element, ParserContext parserContext) { Element pathMatchingElement = DomUtils.getChildElementByTagName(element, "path-matching"); if (pathMatchingElement != null) { @@ -462,15 +455,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { private String getAsyncTimeout(Element element) { Element asyncElement = DomUtils.getChildElementByTagName(element, "async-support"); - return (asyncElement != null) ? asyncElement.getAttribute("default-timeout") : null; + return (asyncElement != null ? asyncElement.getAttribute("default-timeout") : null); } private RuntimeBeanReference getAsyncExecutor(Element element) { Element asyncElement = DomUtils.getChildElementByTagName(element, "async-support"); - if (asyncElement != null) { - if (asyncElement.hasAttribute("task-executor")) { - return new RuntimeBeanReference(asyncElement.getAttribute("task-executor")); - } + if (asyncElement != null && asyncElement.hasAttribute("task-executor")) { + return new RuntimeBeanReference(asyncElement.getAttribute("task-executor")); } return null; } @@ -571,7 +562,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { } if (jackson2XmlPresent) { - RootBeanDefinition jacksonConverterDef = createConverterDefinition(MappingJackson2XmlHttpMessageConverter.class, source); + Class type = MappingJackson2XmlHttpMessageConverter.class; + RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source); GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source); jacksonFactoryDef.getPropertyValues().add("createXmlMapper", true); jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef); @@ -582,7 +574,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { } if (jackson2Present) { - RootBeanDefinition jacksonConverterDef = createConverterDefinition(MappingJackson2HttpMessageConverter.class, source); + Class type = MappingJackson2HttpMessageConverter.class; + RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source); GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source); jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef); messageConverters.add(jacksonConverterDef); @@ -609,7 +602,6 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { return beanDefinition; } - private ManagedList extractBeanSubElements(Element parentElement, ParserContext parserContext) { ManagedList list = new ManagedList(); list.setSource(parserContext.extractSource(parentElement)); @@ -620,27 +612,6 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { return list; } - private ManagedList extractBeanRefSubElements(Element parentElement, ParserContext parserContext){ - ManagedList list = new ManagedList(); - list.setSource(parserContext.extractSource(parentElement)); - for (Element refElement : DomUtils.getChildElementsByTagName(parentElement, "ref")) { - BeanReference reference; - if (StringUtils.hasText("bean")) { - reference = new RuntimeBeanReference(refElement.getAttribute("bean"),false); - list.add(reference); - } - else if (StringUtils.hasText("parent")){ - reference = new RuntimeBeanReference(refElement.getAttribute("parent"),true); - list.add(reference); - } - else { - parserContext.getReaderContext().error("'bean' or 'parent' attribute is required for element", - parserContext.extractSource(parentElement)); - } - } - return list; - } - /** * A FactoryBean for a CompositeUriComponentsContributor that obtains the @@ -671,7 +642,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { } @Override - public CompositeUriComponentsContributor getObject() throws Exception { + public CompositeUriComponentsContributor getObject() { return this.uriComponentsContributor; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 61b4f5a2c3..5e329a0707 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,8 +63,6 @@ import org.springframework.validation.Validator; import org.springframework.web.HttpRequestHandler; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.ServletContextAware; import org.springframework.web.cors.CorsConfiguration; @@ -134,10 +132,10 @@ import org.springframework.web.util.UrlPathHelper; *

Registers a {@link HandlerExceptionResolverComposite} with this chain of * exception resolvers: *

    - *
  • {@link ExceptionHandlerExceptionResolver} for handling exceptions - * through @{@link ExceptionHandler} methods. - *
  • {@link ResponseStatusExceptionResolver} for exceptions annotated - * with @{@link ResponseStatus}. + *
  • {@link ExceptionHandlerExceptionResolver} for handling exceptions through + * {@link org.springframework.web.bind.annotation.ExceptionHandler} methods. + *
  • {@link ResponseStatusExceptionResolver} for exceptions annotated with + * {@link org.springframework.web.bind.annotation.ResponseStatus}. *
  • {@link DefaultHandlerExceptionResolver} for resolving known Spring * exception types *
@@ -865,12 +863,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv * A method available to subclasses for adding default {@link HandlerExceptionResolver}s. *

Adds the following exception resolvers: *

    - *
  • {@link ExceptionHandlerExceptionResolver} - * for handling exceptions through @{@link ExceptionHandler} methods. - *
  • {@link ResponseStatusExceptionResolver} - * for exceptions annotated with @{@link ResponseStatus}. - *
  • {@link DefaultHandlerExceptionResolver} - * for resolving known Spring exception types + *
  • {@link ExceptionHandlerExceptionResolver} for handling exceptions through + * {@link org.springframework.web.bind.annotation.ExceptionHandler} methods. + *
  • {@link ResponseStatusExceptionResolver} for exceptions annotated with + * {@link org.springframework.web.bind.annotation.ResponseStatus}. + *
  • {@link DefaultHandlerExceptionResolver} for resolving known Spring exception types *
*/ protected final void addDefaultHandlerExceptionResolvers(List exceptionResolvers) { diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc index cc82277717..1a86876bbb 100644 --- a/src/asciidoc/web-mvc.adoc +++ b/src/asciidoc/web-mvc.adoc @@ -3975,11 +3975,11 @@ especially when relying on `@ResponseBody` methods rather than on view resolutio may be more convenient to directly set the status of the response and optionally write error content to the body of the response. -You can do that with `@ExceptionHandler` methods. When declared within a controller such -methods apply to exceptions raised by `@RequestMapping` methods of that controller (or -any of its subclasses). You can also declare an `@ExceptionHandler` method within an -`@ControllerAdvice` class in which case it handles exceptions from `@RequestMapping` -methods from many controllers. Below is an example of a controller-local +You can do that with `@ExceptionHandler` methods. When declared within a controller, +such methods apply to exceptions raised by `@RequestMapping` methods of that controller +(or any of its subclasses). You can also declare an `@ExceptionHandler` method within +an `@ControllerAdvice` class in which case it handles exceptions from `@RequestMapping` +methods from many controllers. Below is an example for a controller-local `@ExceptionHandler` method: [source,java,indent=0] @@ -3988,21 +3988,28 @@ methods from many controllers. Below is an example of a controller-local @Controller public class SimpleController { - // @RequestMapping methods omitted ... + // ... - @ExceptionHandler(IOException.class) - public ResponseEntity handleIOException(IOException ex) { - // prepare responseEntity - return responseEntity; + @ExceptionHandler + public ResponseEntity handle(IOException ex) { + // ... } - } ---- -The `@ExceptionHandler` value can be set to an array of Exception types. If an exception -is thrown that matches one of the types in the list, then the method annotated with the -matching `@ExceptionHandler` will be invoked. If the annotation value is not set then -the exception types listed as method arguments are used. +The exception may match against a top-level exception being propagated (i.e. a direct +`IOException` thrown), or against the immediate cause within a top-level wrapper exception +(e.g. an `IOException` wrapped inside an `IllegalStateException`). + +For matching exception types, preferably declare the target exception as a method argument +as shown above. When multiple exception methods match, a root exception match is generally +preferred to a cause exception match. More specifically, the `ExceptionDepthComparator` is +used to sort exceptions based on their depth from the thrown exception type. + +Alternatively, the `@ExceptionHandler` value can be set to an array of exception types. +If an exception is thrown that matches one of the types in the list, then the method +annotated with the matching `@ExceptionHandler` will be invoked. If the annotation value +is not set, then the declared method parameter type will be used for matching. [TIP] ==== @@ -4014,18 +4021,26 @@ advice bean. As a consequence, when using a multi-advice arrangement, please dec primary root exception mappings on a prioritized advice bean with a corresponding order! ==== -Much like standard controller methods annotated with a `@RequestMapping` annotation, the -method arguments and return values of `@ExceptionHandler` methods can be flexible. For -example, the `HttpServletRequest` can be accessed in Servlet environments and the +Much like standard controller methods annotated with a `@RequestMapping` annotation, +the method arguments and return values of `@ExceptionHandler` methods can be flexible. +For example, the `HttpServletRequest` can be accessed in Servlet environments and the `PortletRequest` in Portlet environments. The return type can be a `String`, which is interpreted as a view name, a `ModelAndView` object, a `ResponseEntity`, or you can also add the `@ResponseBody` to have the method return value converted with message converters and written to the response stream. +Last but not least, an `@ExceptionHandler` method implementation may choose to back +out of dealing with a given exception instance by rethrowing it in its original form. +This is useful in scenarios where you are only interested in root-level matches or in +matches within a specific context that cannot be statically determined. A rethrown +exception will be propagated through the remaining resolution chain, just like if +the given `@ExceptionHandler` method would not have matched in the first place. + [[mvc-ann-rest-spring-mvc-exceptions]] === Handling Standard Spring MVC Exceptions + Spring MVC may raise a number of exceptions while processing a request. The `SimpleMappingExceptionResolver` can easily map any exception to a default error view as needed. However, when working with clients that interpret responses in an automated way