From e65b930e7ad63b909bd2977bff806322477f8a91 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 Jul 2012 13:37:49 -0400 Subject: [PATCH] Introduced ControllerAdvice annotation Classes with this annotation can contain @ExceptionHandler, @InitBinder, and @ModelAttribute methods that apply to all controllers. The new annotation is also a component annotation allowing implementations to be discovered through component scanning. Issue: SPR-9112 --- .../web/bind/annotation/ControllerAdvice.java | 46 ++++++ .../bind/annotation/ExceptionResolver.java | 57 -------- .../bind/support/ControllerAdviceBean.java | 136 ++++++++++++++++++ .../ExceptionHandlerMethodResolver.java | 64 ++++----- ...bstractHandlerMethodExceptionResolver.java | 12 +- .../ExceptionHandlerExceptionResolver.java | 96 +++++-------- .../RequestMappingHandlerAdapter.java | 99 ++++++++++--- ...xceptionHandlerExceptionResolverTests.java | 55 +++---- ...HandlerMethodAnnotationDetectionTests.java | 9 +- .../RequestMappingHandlerAdapterTests.java | 61 +++++--- src/reference/docbook/mvc.xml | 44 ++++-- 11 files changed, 440 insertions(+), 239 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java delete mode 100644 spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/bind/support/ControllerAdviceBean.java diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java new file mode 100644 index 0000000000..71aeac2110 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +/** + * Indicates the annotated class assists a "Controller". + * + *

Serves as a specialization of {@link Component @Component}, allowing for + * implementation classes to be autodetected through classpath scanning. + * + *

It is typically used to define {@link ExceptionHandler @ExceptionHandler}, + * {@link InitBinder @InitBinder}, and {@link ModelAttribute @ModelAttribute} + * methods that apply across controller class hierarchies. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface ControllerAdvice { + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionResolver.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionResolver.java deleted file mode 100644 index e0c07c2a6d..0000000000 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionResolver.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2002-2012 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.bind.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; - -/** - * An {@linkplain Component @Component} annotation that indicates the annotated class - * contains {@linkplain ExceptionHandler @ExceptionHandler} methods. Such methods - * will be used in addition to {@code @ExceptionHandler} methods in - * {@code @Controller}-annotated classes. - * - *

In order for the the annotation to detected, an instance of - * {@code org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver} - * is configured. - * - *

Classes with this annotation may use the {@linkplain Order @Order} annotation - * or implement the {@link Ordered} interface to indicate the order in which they - * should be used relative to other such annotated components. However, note that - * the order is only for components registered through {@code @ExceptionResolver}, - * i.e. within an - * {@code org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver}. - * - * @author Rossen Stoyanchev - * @since 3.2 - * - * @see org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Component -public @interface ExceptionResolver { - -} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/ControllerAdviceBean.java b/spring-web/src/main/java/org/springframework/web/bind/support/ControllerAdviceBean.java new file mode 100644 index 0000000000..fd1b204ed4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/support/ControllerAdviceBean.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.bind.support; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.ControllerAdvice; + +/** + * Encapsulates information about an {@linkplain ControllerAdvice @ControllerAdvice} + * bean without requiring the bean to be instantiated. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class ControllerAdviceBean implements Ordered { + + private final Object bean; + + private final int order; + + private final BeanFactory beanFactory; + + + /** + * Create an instance using the given bean name. + * @param beanName the name of the bean + * @param beanFactory a BeanFactory that can be used later to resolve the bean + */ + public ControllerAdviceBean(String beanName, BeanFactory beanFactory) { + Assert.hasText(beanName, "'beanName' must not be null"); + Assert.notNull(beanFactory, "'beanFactory' must not be null"); + Assert.isTrue(beanFactory.containsBean(beanName), + "Bean factory [" + beanFactory + "] does not contain bean " + "with name [" + beanName + "]"); + this.bean = beanName; + this.beanFactory = beanFactory; + this.order = initOrder(this.beanFactory.getType(beanName)); + } + + /** + * Create an instance using the given bean instance. + * @param bean the bean + */ + public ControllerAdviceBean(Object bean) { + Assert.notNull(bean, "'bean' must not be null"); + this.bean = bean; + this.order = initOrder(bean.getClass()); + this.beanFactory = null; + } + + private static int initOrder(Class beanType) { + Order orderAnnot = AnnotationUtils.findAnnotation(beanType, Order.class); + return (orderAnnot != null) ? orderAnnot.value() : Ordered.LOWEST_PRECEDENCE; + } + + /** + * Find the names of beans annotated with + * {@linkplain ControllerAdvice @ControllerAdvice} in the given + * ApplicationContext and wrap them as {@code ControllerAdviceBean} instances. + */ + public static List findBeans(ApplicationContext applicationContext) { + List beans = new ArrayList(); + for (String name : applicationContext.getBeanDefinitionNames()) { + if (applicationContext.findAnnotationOnBean(name, ControllerAdvice.class) != null) { + beans.add(new ControllerAdviceBean(name, applicationContext)); + } + } + return beans; + } + + /** + * Return a bean instance if necessary resolving the bean name through the BeanFactory. + */ + public Object resolveBean() { + return (this.bean instanceof String) ? this.beanFactory.getBean((String) this.bean) : this.bean; + } + + public int getOrder() { + return this.order; + } + + /** + * Returns the type of the contained bean. + * If the bean type is a CGLIB-generated class, the original, user-defined class is returned. + */ + public Class getBeanType() { + Class clazz = (this.bean instanceof String) + ? this.beanFactory.getType((String) this.bean) : this.bean.getClass(); + + return ClassUtils.getUserClass(clazz); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o != null && o instanceof ControllerAdviceBean) { + ControllerAdviceBean other = (ControllerAdviceBean) o; + return this.bean.equals(other.bean); + } + return false; + } + + @Override + public int hashCode() { + return 31 * this.bean.hashCode(); + } + + @Override + public String toString() { + return bean.toString(); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java index 1445c5b695..22dd998229 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java @@ -22,7 +22,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.springframework.core.ExceptionDepthComparator; @@ -34,57 +33,47 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.method.HandlerMethodSelector; /** - * Given a set of @{@link ExceptionHandler} methods at initialization, finds - * the best matching method mapped to an exception at runtime. - * - *

Exception mappings are extracted from the method @{@link ExceptionHandler} - * annotation or by looking for {@link Throwable} method arguments. - * + * Discovers {@linkplain ExceptionHandler @ExceptionHandler} methods in a given class + * type, including all super types, and helps to resolve an Exception to the method + * its mapped to. Exception mappings are defined through {@code @ExceptionHandler} + * annotation or by looking at the signature of an {@code @ExceptionHandler} method. + * * @author Rossen Stoyanchev * @since 3.1 */ public class ExceptionHandlerMethodResolver { private static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis"); - - private final Map, Method> mappedMethods = + + private final Map, Method> mappedMethods = new ConcurrentHashMap, Method>(); private final Map, Method> exceptionLookupCache = new ConcurrentHashMap, Method>(); /** - * A constructor that finds {@link ExceptionHandler} methods in a handler. - * @param handlerType the handler to inspect for exception handler methods. - * @throws IllegalStateException - * If an exception type is mapped to two methods. - * @throws IllegalArgumentException - * If an @{@link ExceptionHandler} method is not mapped to any exceptions. + * A constructor that finds {@link ExceptionHandler} methods in the given type. + * @param handlerType the type to introspect */ public ExceptionHandlerMethodResolver(Class handlerType) { - init(HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)); - } - - private void init(Set exceptionHandlerMethods) { - for (Method method : exceptionHandlerMethods) { - for (Class exceptionType : detectMappedExceptions(method)) { + for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { + for (Class exceptionType : detectExceptionMappings(method)) { addExceptionMapping(exceptionType, method); } } } /** - * Detect the exceptions an @{@link ExceptionHandler} method is mapped to. - * If the method @{@link ExceptionHandler} annotation doesn't have any, - * scan the method signature for all arguments of type {@link Throwable}. + * Extract exception mappings from the {@code @ExceptionHandler} annotation + * first and as a fall-back from the method signature. */ @SuppressWarnings("unchecked") - private List> detectMappedExceptions(Method method) { + private List> detectExceptionMappings(Method method) { List> result = new ArrayList>(); + ExceptionHandler annotation = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); - if (annotation != null) { - result.addAll(Arrays.asList(annotation.value())); - } + result.addAll(Arrays.asList(annotation.value())); + if (result.isEmpty()) { for (Class paramType : method.getParameterTypes()) { if (Throwable.class.isAssignableFrom(paramType)) { @@ -92,7 +81,9 @@ public class ExceptionHandlerMethodResolver { } } } + Assert.notEmpty(result, "No exception types mapped to {" + method + "}"); + return result; } @@ -106,10 +97,17 @@ public class ExceptionHandlerMethodResolver { } /** - * Find a method to handle the given exception. If more than one match is - * found, the best match is selected via {@link ExceptionDepthComparator}. + * Whether the contained type has any exception mappings. + */ + public boolean hasExceptionMappings() { + return (this.mappedMethods.size() > 0); + } + + /** + * Find a method to handle the given exception. + * Use {@link ExceptionDepthComparator} if more than one match is found. * @param exception the exception - * @return an @{@link ExceptionHandler} method, or {@code null} + * @return a method to handle the exception or {@code null} */ public Method resolveMethod(Exception exception) { Class exceptionType = exception.getClass(); @@ -122,7 +120,7 @@ public class ExceptionHandlerMethodResolver { } /** - * Return the method mapped to the exception type, or {@code null}. + * Return the method mapped to the given exception type or {@code null}. */ private Method getMappedMethod(Class exceptionType) { List> matches = new ArrayList>(); @@ -141,7 +139,7 @@ public class ExceptionHandlerMethodResolver { } /** - * A filter for selecting @{@link ExceptionHandler} methods. + * A filter for selecting {@code @ExceptionHandler} methods. */ public final static MethodFilter EXCEPTION_HANDLER_METHODS = new MethodFilter() { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodExceptionResolver.java index 4481118b3c..1f31c9f172 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodExceptionResolver.java @@ -26,16 +26,16 @@ import org.springframework.web.servlet.ModelAndView; * Abstract base class for * {@link org.springframework.web.servlet.HandlerExceptionResolver HandlerExceptionResolver} * implementations that support handling exceptions from handlers of type {@link HandlerMethod}. - * + * * @author Rossen Stoyanchev * @since 3.1 */ public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver { /** - * Checks if the handler is a {@link HandlerMethod} instance and performs the check against the bean - * instance it contains. If the provided handler is not an instance of {@link HandlerMethod}, - * {@code false} is returned instead. + * Checks if the handler is a {@link HandlerMethod} and then delegates to the + * base class implementation of {@link #shouldApplyTo(HttpServletRequest, Object)} + * passing the bean of the {@code HandlerMethod}. Otherwise returns {@code false}. */ @Override protected boolean shouldApplyTo(HttpServletRequest request, Object handler) { @@ -51,7 +51,7 @@ public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHan return false; } } - + @Override protected final ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, @@ -59,7 +59,7 @@ public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHan return doResolveHandlerMethodException(request, response, (HandlerMethod) handler, ex); } - + /** * Actually resolve the given exception that got thrown during on handler execution, * returning a ModelAndView that represents a specific error page if appropriate. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index 741a48b53a..eca11c04f0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -19,7 +19,6 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -34,16 +33,15 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.OrderComparator; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; import org.springframework.web.accept.ContentNegotiationManager; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ExceptionResolver; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.support.ControllerAdviceBean; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; @@ -82,11 +80,11 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); - private final Map, ExceptionHandlerMethodResolver> exceptionHandlersByType = + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap, ExceptionHandlerMethodResolver>(); - private final Map globalExceptionHandlers = - new LinkedHashMap(); + private final Map exceptionHandlerAdviceCache = + new LinkedHashMap(); private HandlerMethodArgumentResolverComposite argumentResolvers; @@ -208,22 +206,14 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce this.contentNegotiationManager = contentNegotiationManager; } - /** - * Provide instances of objects with {@link ExceptionHandler @ExceptionHandler} - * methods to apply globally, i.e. regardless of the selected controller. - *

{@code @ExceptionHandler} methods in the controller are always looked - * up before {@code @ExceptionHandler} methods in global handlers. - */ - public void setGlobalExceptionHandlers(Object... handlers) { - for (Object handler : handlers) { - this.globalExceptionHandlers.put(handler, new ExceptionHandlerMethodResolver(handler.getClass())); - } - } - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } + public ApplicationContext getApplicationContext() { + return this.applicationContext; + } + public void afterPropertiesSet() { if (this.argumentResolvers == null) { List resolvers = getDefaultArgumentResolvers(); @@ -233,7 +223,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce List handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } - initGlobalExceptionHandlers(); + initExceptionHandlerAdviceCache(); } /** @@ -287,39 +277,28 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce return handlers; } - private void initGlobalExceptionHandlers() { - if (this.applicationContext == null) { - logger.warn("Can't detect @ExceptionResolver components if the ApplicationContext property is not set"); + private void initExceptionHandlerAdviceCache() { + if (getApplicationContext() == null) { + return; } - else { - String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class); - for (String name : beanNames) { - Class type = this.applicationContext.getType(name); - if (AnnotationUtils.findAnnotation(type , ExceptionResolver.class) != null) { - Object bean = this.applicationContext.getBean(name); - this.globalExceptionHandlers.put(bean, new ExceptionHandlerMethodResolver(bean.getClass())); - } - } + if (logger.isDebugEnabled()) { + logger.debug("Looking for exception mappings: " + getApplicationContext()); } - if (this.globalExceptionHandlers.size() > 0) { - sortGlobalExceptionHandlers(); - } - } - private void sortGlobalExceptionHandlers() { - Map handlersCopy = - new HashMap(this.globalExceptionHandlers); - List handlers = new ArrayList(handlersCopy.keySet()); - Collections.sort(handlers, new AnnotationAwareOrderComparator()); - this.globalExceptionHandlers.clear(); - for (Object handler : handlers) { - this.globalExceptionHandlers.put(handler, handlersCopy.get(handler)); + List beans = ControllerAdviceBean.findBeans(getApplicationContext()); + Collections.sort(beans, new OrderComparator()); + + for (ControllerAdviceBean bean : beans) { + ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(bean.getBeanType()); + if (resolver.hasExceptionMappings()) { + this.exceptionHandlerAdviceCache.put(bean, resolver); + logger.info("Detected @ExceptionHandler methods in " + bean); + } } } /** - * Find an @{@link ExceptionHandler} method and invoke it to handle the - * raised exception. + * Find an {@code @ExceptionHandler} method and invoke it to handle the raised exception. */ @Override protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, @@ -361,9 +340,11 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce } /** - * Find the @{@link ExceptionHandler} method for the given exception. - * The default implementation searches @{@link ExceptionHandler} methods - * in the class hierarchy of the method that raised the exception. + * Find an {@code @ExceptionHandler} method for the given exception. The default + * implementation searches methods in the class hierarchy of the controller first + * and if not found, it continues searching for additional {@code @ExceptionHandler} + * methods assuming some {@linkplain ControllerAdvice @ControllerAdvice} + * Spring-managed beans were detected. * @param handlerMethod the method where the exception was raised, possibly {@code null} * @param exception the raised exception * @return a method to handle the exception, or {@code null} @@ -371,27 +352,20 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) { if (handlerMethod != null) { Class handlerType = handlerMethod.getBeanType(); - ExceptionHandlerMethodResolver resolver = this.exceptionHandlersByType.get(handlerType); + ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); if (resolver == null) { resolver = new ExceptionHandlerMethodResolver(handlerType); - this.exceptionHandlersByType.put(handlerType, resolver); + this.exceptionHandlerCache.put(handlerType, resolver); } Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method); } } - return getGlobalExceptionHandlerMethod(exception); - } - - /** - * Return a global {@code @ExceptionHandler} method for the given exception or {@code null}. - */ - private ServletInvocableHandlerMethod getGlobalExceptionHandlerMethod(Exception exception) { - for (Entry entry : this.globalExceptionHandlers.entrySet()) { + for (Entry entry : this.exceptionHandlerAdviceCache.entrySet()) { Method method = entry.getValue().resolveMethod(exception); if (method != null) { - return new ServletInvocableHandlerMethod(entry.getKey(), method); + return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method); } } return null; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 139e874a80..b003325bdc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -19,8 +19,11 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -36,6 +39,7 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.OrderComparator; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.task.AsyncTaskExecutor; @@ -53,6 +57,7 @@ import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.support.ControllerAdviceBean; import org.springframework.web.bind.support.DefaultDataBinderFactory; import org.springframework.web.bind.support.DefaultSessionAttributeStore; import org.springframework.web.bind.support.SessionAttributeStore; @@ -62,9 +67,9 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.async.AsyncWebRequest; +import org.springframework.web.context.request.async.AsyncWebUtils; import org.springframework.web.context.request.async.NoSupportAsyncWebRequest; import org.springframework.web.context.request.async.WebAsyncManager; -import org.springframework.web.context.request.async.AsyncWebUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethodSelector; import org.springframework.web.method.annotation.ErrorsMethodArgumentResolver; @@ -142,9 +147,15 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i private HandlerMethodReturnValueHandlerComposite returnValueHandlers; - private final Map, Set> dataBinderFactoryCache = new ConcurrentHashMap, Set>(); + private final Map, Set> initBinderCache = new ConcurrentHashMap, Set>(); - private final Map, Set> modelFactoryCache = new ConcurrentHashMap, Set>(); + private final Map> initBinderAdviceCache = + new LinkedHashMap>(); + + private final Map, Set> modelAttributeCache = new ConcurrentHashMap, Set>(); + + private final Map> modelAttributeAdviceCache = + new LinkedHashMap>(); private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync"); @@ -152,6 +163,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); + /** * Default constructor. */ @@ -454,6 +466,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i List handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } + initControllerAdviceCache(); } /** @@ -565,6 +578,31 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i return handlers; } + private void initControllerAdviceCache() { + if (getApplicationContext() == null) { + return; + } + if (logger.isDebugEnabled()) { + logger.debug("Looking for controller advice: " + getApplicationContext()); + } + + List beans = ControllerAdviceBean.findBeans(getApplicationContext()); + Collections.sort(beans, new OrderComparator()); + + for (ControllerAdviceBean bean : beans) { + Set attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS); + if (!attrMethods.isEmpty()) { + this.modelAttributeAdviceCache.put(bean, attrMethods); + logger.info("Detected @ModelAttribute methods in " + bean); + } + Set binderMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS); + if (!binderMethods.isEmpty()) { + this.initBinderAdviceCache.put(bean, binderMethods); + logger.info("Detected @InitBinder methods in " + bean); + } + } + } + /** * Always return {@code true} since any method argument and return value * type will be processed in some way. A method argument not recognized @@ -694,38 +732,60 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) { SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod); Class handlerType = handlerMethod.getBeanType(); - Set methods = this.modelFactoryCache.get(handlerType); + Set methods = this.modelAttributeCache.get(handlerType); if (methods == null) { methods = HandlerMethodSelector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS); - this.modelFactoryCache.put(handlerType, methods); + this.modelAttributeCache.put(handlerType, methods); } List attrMethods = new ArrayList(); for (Method method : methods) { - InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method); - attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); - attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); - attrMethod.setDataBinderFactory(binderFactory); - attrMethods.add(attrMethod); + Object bean = handlerMethod.getBean(); + attrMethods.add(createModelAttributeMethod(binderFactory, bean, method)); + } + for (Entry> entry : this.modelAttributeAdviceCache.entrySet()) { + Object bean = entry.getKey().resolveBean(); + for (Method method : entry.getValue()) { + attrMethods.add(createModelAttributeMethod(binderFactory, bean, method)); + } } return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler); } + private InvocableHandlerMethod createModelAttributeMethod(WebDataBinderFactory factory, Object bean, Method method) { + InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(bean, method); + attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + attrMethod.setDataBinderFactory(factory); + return attrMethod; + } + private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception { Class handlerType = handlerMethod.getBeanType(); - Set methods = this.dataBinderFactoryCache.get(handlerType); + Set methods = this.initBinderCache.get(handlerType); if (methods == null) { methods = HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS); - this.dataBinderFactoryCache.put(handlerType, methods); + this.initBinderCache.put(handlerType, methods); } - List binderMethods = new ArrayList(); + List initBinderMethods = new ArrayList(); for (Method method : methods) { - InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method); - binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers); - binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer)); - binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); - binderMethods.add(binderMethod); + Object bean = handlerMethod.getBean(); + initBinderMethods.add(createInitBinderMethod(bean, method)); } - return createDataBinderFactory(binderMethods); + for (Entry> entry : this.initBinderAdviceCache .entrySet()) { + Object bean = entry.getKey().resolveBean(); + for (Method method : entry.getValue()) { + initBinderMethods.add(createInitBinderMethod(bean, method)); + } + } + return createDataBinderFactory(initBinderMethods); + } + + private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) { + InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method); + binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers); + binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer)); + binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + return binderMethod; } /** @@ -738,6 +798,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i */ protected ServletRequestDataBinderFactory createDataBinderFactory(List binderMethods) throws Exception { + return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java index 806cc3cb5d..67552d83b6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java @@ -33,14 +33,13 @@ import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ExceptionResolver; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ModelMethodProcessor; @@ -164,7 +163,7 @@ public class ExceptionHandlerExceptionResolverTests { } @Test - public void resolveExceptionResponseWriter() throws UnsupportedEncodingException, NoSuchMethodException { + public void resolveExceptionResponseWriter() throws Exception { IllegalArgumentException ex = new IllegalArgumentException(); HandlerMethod handlerMethod = new HandlerMethod(new ResponseWriterController(), "handle"); this.resolver.afterPropertiesSet(); @@ -176,28 +175,24 @@ public class ExceptionHandlerExceptionResolverTests { } @Test - public void resolveExceptionGlobalHandler() throws UnsupportedEncodingException, NoSuchMethodException { + public void resolveExceptionGlobalHandler() throws Exception { AnnotationConfigApplicationContext cxt = new AnnotationConfigApplicationContext(MyConfig.class); this.resolver.setApplicationContext(cxt); - this.resolver.setGlobalExceptionHandlers(new GlobalExceptionHandler()); this.resolver.afterPropertiesSet(); - IllegalStateException ex = new IllegalStateException(); + IllegalAccessException ex = new IllegalAccessException(); HandlerMethod handlerMethod = new HandlerMethod(new ResponseBodyController(), "handle"); ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex); assertNotNull("Exception was not handled", mav); assertTrue(mav.isEmpty()); - assertEquals("IllegalStateException", this.response.getContentAsString()); + assertEquals("AnotherTestExceptionResolver: IllegalAccessException", this.response.getContentAsString()); } @Test - public void resolveExceptionGlobalHandlerOrdered() throws UnsupportedEncodingException, NoSuchMethodException { + public void resolveExceptionGlobalHandlerOrdered() throws Exception { AnnotationConfigApplicationContext cxt = new AnnotationConfigApplicationContext(MyConfig.class); this.resolver.setApplicationContext(cxt); - GlobalExceptionHandler globalHandler = new GlobalExceptionHandler(); - globalHandler.setOrder(2); - this.resolver.setGlobalExceptionHandlers(globalHandler); this.resolver.afterPropertiesSet(); IllegalStateException ex = new IllegalStateException(); @@ -206,7 +201,7 @@ public class ExceptionHandlerExceptionResolverTests { assertNotNull("Exception was not handled", mav); assertTrue(mav.isEmpty()); - assertEquals("@ExceptionResolver: IllegalStateException", this.response.getContentAsString()); + assertEquals("TestExceptionResolver: IllegalStateException", this.response.getContentAsString()); } @@ -259,41 +254,37 @@ public class ExceptionHandlerExceptionResolverTests { } } - static class GlobalExceptionHandler implements Ordered { - - private int order; - - public int getOrder() { - return order; - } - - public void setOrder(int order) { - this.order = order; - } + @ControllerAdvice + @Order(1) + static class TestExceptionResolver { @ExceptionHandler @ResponseBody public String handleException(IllegalStateException ex) { - return ClassUtils.getShortName(ex.getClass()); + return "TestExceptionResolver: " + ClassUtils.getShortName(ex.getClass()); } } - @ExceptionResolver - @Order(1) - static class AnnotatedExceptionResolver { + @ControllerAdvice + @Order(2) + static class AnotherTestExceptionResolver { - @ExceptionHandler + @ExceptionHandler({IllegalStateException.class, IllegalAccessException.class}) @ResponseBody - public String handleException(IllegalStateException ex) { - return "@ExceptionResolver: " + ClassUtils.getShortName(ex.getClass()); + public String handleException(Exception ex) { + return "AnotherTestExceptionResolver: " + ClassUtils.getShortName(ex.getClass()); } } @Configuration static class MyConfig { - @Bean public AnnotatedExceptionResolver exceptionResolver() { - return new AnnotatedExceptionResolver(); + @Bean public TestExceptionResolver testExceptionResolver() { + return new TestExceptionResolver(); + } + + @Bean public AnotherTestExceptionResolver anotherTestExceptionResolver() { + return new AnotherTestExceptionResolver(); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HandlerMethodAnnotationDetectionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HandlerMethodAnnotationDetectionTests.java index 7c93307a44..240cab9c40 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HandlerMethodAnnotationDetectionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HandlerMethodAnnotationDetectionTests.java @@ -25,12 +25,10 @@ import java.util.Arrays; import java.util.Collection; import java.util.Date; -import org.aopalliance.aop.Advice; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; -import org.springframework.aop.Pointcut; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.support.DefaultPointcutAdvisor; @@ -106,7 +104,7 @@ public class HandlerMethodAnnotationDetectionTests { DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setBeanFactory(context.getBeanFactory()); context.getBeanFactory().addBeanPostProcessor(autoProxyCreator); - context.registerBeanDefinition("controllerAdvice", new RootBeanDefinition(ControllerAdvice.class)); + context.registerBeanDefinition("controllerAdvice", new RootBeanDefinition(ControllerAdvisor.class)); } context.refresh(); @@ -405,9 +403,10 @@ public class HandlerMethodAnnotationDetectionTests { } - static class ControllerAdvice extends DefaultPointcutAdvisor { + @SuppressWarnings("serial") + static class ControllerAdvisor extends DefaultPointcutAdvisor { - public ControllerAdvice() { + public ControllerAdvisor() { super(getControllerPointcut(), new SimpleTraceInterceptor()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java index 6a0808e8d8..cbad10fe23 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java @@ -28,8 +28,10 @@ import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.SessionAttributes; -import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ModelMethodProcessor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -51,31 +53,35 @@ import org.springframework.web.servlet.ModelAndView; public class RequestMappingHandlerAdapterTests { private static int RESOLVER_COUNT; - + private static int INIT_BINDER_RESOLVER_COUNT; - + private static int HANDLER_COUNT; private RequestMappingHandlerAdapter handlerAdapter; - + private MockHttpServletRequest request; - + private MockHttpServletResponse response; + private StaticWebApplicationContext webAppContext; + @BeforeClass public static void setupOnce() { RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); + adapter.setApplicationContext(new StaticWebApplicationContext()); adapter.afterPropertiesSet(); - + RESOLVER_COUNT = adapter.getArgumentResolvers().getResolvers().size(); INIT_BINDER_RESOLVER_COUNT = adapter.getInitBinderArgumentResolvers().getResolvers().size(); HANDLER_COUNT = adapter.getReturnValueHandlers().getHandlers().size(); } - + @Before public void setup() throws Exception { + this.webAppContext = new StaticWebApplicationContext(); this.handlerAdapter = new RequestMappingHandlerAdapter(); - this.handlerAdapter.setApplicationContext(new GenericWebApplicationContext()); + this.handlerAdapter.setApplicationContext(this.webAppContext); this.request = new MockHttpServletRequest(); this.response = new MockHttpServletResponse(); } @@ -105,7 +111,7 @@ public class RequestMappingHandlerAdapterTests { HandlerMethodArgumentResolver redirectAttributesResolver = new RedirectAttributesMethodArgumentResolver(); HandlerMethodArgumentResolver modelResolver = new ModelMethodProcessor(); HandlerMethodReturnValueHandler viewHandler = new ViewNameMethodReturnValueHandler(); - + this.handlerAdapter.setArgumentResolvers(Arrays.asList(redirectAttributesResolver, modelResolver)); this.handlerAdapter.setReturnValueHandlers(Arrays.asList(viewHandler)); this.handlerAdapter.setIgnoreDefaultModelOnRedirect(true); @@ -124,7 +130,7 @@ public class RequestMappingHandlerAdapterTests { HandlerMethodArgumentResolver resolver = new ServletRequestMethodArgumentResolver(); this.handlerAdapter.setCustomArgumentResolvers(Arrays.asList(resolver)); this.handlerAdapter.afterPropertiesSet(); - + assertTrue(this.handlerAdapter.getArgumentResolvers().getResolvers().contains(resolver)); assertMethodProcessorCount(RESOLVER_COUNT + 1, INIT_BINDER_RESOLVER_COUNT + 1, HANDLER_COUNT); } @@ -143,7 +149,7 @@ public class RequestMappingHandlerAdapterTests { HandlerMethodArgumentResolver resolver = new ServletRequestMethodArgumentResolver(); this.handlerAdapter.setInitBinderArgumentResolvers(Arrays.asList(resolver)); this.handlerAdapter.afterPropertiesSet(); - + assertMethodProcessorCount(RESOLVER_COUNT, 1, HANDLER_COUNT); } @@ -156,7 +162,7 @@ public class RequestMappingHandlerAdapterTests { assertTrue(this.handlerAdapter.getReturnValueHandlers().getHandlers().contains(handler)); assertMethodProcessorCount(RESOLVER_COUNT, INIT_BINDER_RESOLVER_COUNT, HANDLER_COUNT + 1); } - + @Test public void setReturnValueHandlers() { HandlerMethodReturnValueHandler handler = new ModelMethodProcessor(); @@ -166,19 +172,31 @@ public class RequestMappingHandlerAdapterTests { assertMethodProcessorCount(RESOLVER_COUNT, INIT_BINDER_RESOLVER_COUNT, 1); } + @Test + public void modelAttributeAdvice() throws Exception { + this.webAppContext.registerSingleton("maa", ModelAttributeAdvice.class); + this.webAppContext.refresh(); + + HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle"); + this.handlerAdapter.afterPropertiesSet(); + ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod); + + assertEquals("globalAttrValue", mav.getModel().get("globalAttr")); + } + + private HandlerMethod handlerMethod(Object handler, String methodName, Class... paramTypes) throws Exception { Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); return new InvocableHandlerMethod(handler, method); } - - private void assertMethodProcessorCount(int resolverCount, int initBinderResolverCount, int handlerCount) { assertEquals(resolverCount, this.handlerAdapter.getArgumentResolvers().getResolvers().size()); assertEquals(initBinderResolverCount, this.handlerAdapter.getInitBinderArgumentResolvers().getResolvers().size()); assertEquals(handlerCount, this.handlerAdapter.getReturnValueHandlers().getHandlers().size()); } + @SuppressWarnings("unused") private static class SimpleController { @@ -189,7 +207,7 @@ public class RequestMappingHandlerAdapterTests { @SessionAttributes("attr1") private static class SessionAttributeController { - + @SuppressWarnings("unused") public void handle() { } @@ -197,11 +215,20 @@ public class RequestMappingHandlerAdapterTests { @SuppressWarnings("unused") private static class RedirectAttributeController { - + public String handle(Model model) { model.addAttribute("someAttr", "someAttrValue"); return "redirect:/path"; } } - + + @ControllerAdvice + private static class ModelAttributeAdvice { + + @ModelAttribute + public void addAttributes(Model model) { + model.addAttribute("globalAttr", "globalAttrValue"); + } + } + } \ No newline at end of file diff --git a/src/reference/docbook/mvc.xml b/src/reference/docbook/mvc.xml index 09cd747815..33b2c895cb 100644 --- a/src/reference/docbook/mvc.xml +++ b/src/reference/docbook/mvc.xml @@ -1720,6 +1720,13 @@ public void populateModel(@RequestParam String number, Model model) { @RequestMapping methods of the same controller. + @ModelAttribute methods can also + be defined in an @ControllerAdvice-annotated + class and such methods apply to all controllers. + The @ControllerAdvice annotation is + a component annotation allowing implementation classes to be autodetected + through classpath scanning. + What happens when a model attribute name is not explicitly specified? In such cases a default name is assigned to the model @@ -2106,9 +2113,11 @@ public void displayHeaderInfo(@RequestHeader("Accept-Encod To customize request parameter binding with PropertyEditors through Spring's WebDataBinder, you can use - either @InitBinder-annotated methods - within your controller or externalize your configuration by providing - a custom WebBindingInitializer. + @InitBinder-annotated methods within + your controller, @InitBinder methods + within an @ControllerAdvice class, + or provide a custom + WebBindingInitializer.
Customizing data binding with @@ -2176,6 +2185,22 @@ public class MyFormController { </property> </bean></programlisting> </section> + + <section id="mvc-ann-initbinder-advice"> + <title>Customizing data binding with externalized + <interfacename>@InitBinder</interfacename> methods + + @InitBinder methods can also + be defined in an @ControllerAdvice-annotated + class in which case they apply to all controllers. This provides an + alternative to using a WebBindingInitializer. + + + The @ControllerAdvice annotation is + a component annotation allowing implementation classes to be autodetected + through classpath scanning. +
+
@@ -3644,10 +3669,11 @@ public String onSubmit(@RequestPart("meta-data") MetaData methods. When present within a controller such methods apply to exceptions raised by that contoroller or any of its sub-classes. Or you can also declare @ExceptionHandler - methods in a type annotated with @ExceptionResolver - in which case they apply globally. - The @ExceptionResolver annotation is - a component annotation that can also be used with a component scan. + methods in an @ControllerAdvice-annotated + class and such methods apply to any controller. + The @ControllerAdvice annotation is + a component annotation allowing implementation classes to be autodetected + through classpath scanning. Here is an example with a controller-level @@ -3810,13 +3836,13 @@ public class SimpleController { only sets the status code and doesn't assume how or what content should be written to the body. - Instead you can create an @ExceptionResolver + Instead you can create an @ControllerAdvice class that handles each of the exceptions handled by the DefaultHandlerExceptionResolver while also writing developer-friendly API error information to the response body consistent with the rest of all API error handling of the application. For example: - @ExceptionResolver + @ControllerAdvice public class ApplicationExceptionResolver { @ExceptionHandler