Add new features on @ControllerAdvice

Prior to this commit, @ControllerAdvice annotated beans would
assist all known Controllers, by applying @ExceptionHandler,
@InitBinder, and @ModelAttribute.

This commit updates the @ControllerAdvice annotation,
which accepts now base package names, assignableTypes,
annotations and basePackageClasses.

If attributes are set, only Controllers that match those
selectors will be assisted by the annotated class.
This commit does not change the default behavior when
no value is set, i.e. @ControllerAdvice().

Issue: SPR-10222
This commit is contained in:
Brian Clozel 2013-10-11 19:28:28 +02:00 committed by Rossen Stoyanchev
parent be4e5d2841
commit c4a8bf9c4d
7 changed files with 450 additions and 15 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@ -34,7 +34,21 @@ import org.springframework.stereotype.Component;
* {@link InitBinder @InitBinder}, and {@link ModelAttribute @ModelAttribute}
* methods that apply to all {@link RequestMapping @RequestMapping} methods.
*
* <p>One of {@link #annotations()}, {@link #basePackageClasses()},
* {@link #basePackages()} or its alias {@link #value()}
* may be specified to define specific subsets of Controllers
* to assist. When multiple selectors are applied, OR logic is applied -
* meaning selected Controllers should match at least one selector.
*
* <p>The default behavior (i.e. if used without any selector),
* the {@code @ControllerAdvice} annotated class will
* assist all known Controllers.
*
* <p>Note that those checks are done at runtime, so adding many attributes and using
* multiple strategies may have negative impacts (complexity, performance).
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 3.2
*/
@Target(ElementType.TYPE)
@ -43,4 +57,56 @@ import org.springframework.stereotype.Component;
@Component
public @interface ControllerAdvice {
/**
* Alias for the {@link #basePackages()} attribute.
* Allows for more concise annotation declarations e.g.:
* {@code @ControllerAdvice("org.my.pkg")} instead of
* {@code @ControllerAdvice(basePackages="org.my.pkg")}.
* @since 4.0
*/
String[] value() default {};
/**
* Array of base packages.
* Controllers that belong to those base packages will be selected
* to be assisted by the annotated class, e.g.:
* {@code @ControllerAdvice(basePackages="org.my.pkg")}
* {@code @ControllerAdvice(basePackages={"org.my.pkg","org.my.other.pkg"})}
*
* <p>{@link #value()} is an alias for this attribute.
* <p>Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
* @since 4.0
*/
String[] basePackages() default {};
/**
* Array of classes.
* Controllers that are assignable to at least one of the given types
* will be assisted by the {@code @ControllerAdvice} annotated class.
* @since 4.0
*/
Class<?>[] assignableTypes() default {};
/**
* Array of annotations.
* Controllers that are annotated with this/one of those annotation(s)
* will be assisted by the {@code @ControllerAdvice} annotated class.
*
* <p>Consider creating a special annotation or use a predefined one,
* like {@link RestController @RestController}.
* @since 4.0
*/
Class<?>[] annotations() default {};
/**
* Type-safe alternative to {@link #value()} for specifying the packages
* to select Controllers to be assisted by the {@code @ControllerAdvice}
* annotated class.
*
* <p>Consider creating a special no-op marker class or interface in each package
* that serves no purpose other than being referenced by this attribute.
* @since 4.0
*/
Class<?>[] basePackageClasses() default {};
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@ -15,9 +15,13 @@
*/
package org.springframework.web.method;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
@ -25,6 +29,7 @@ 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.util.StringUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
/**
@ -36,6 +41,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
* any object, including ones without an {@code @ControllerAdvice}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 3.2
*/
public class ControllerAdviceBean implements Ordered {
@ -46,6 +52,13 @@ public class ControllerAdviceBean implements Ordered {
private final BeanFactory beanFactory;
private final List<Package> basePackages = new ArrayList<Package>();
private final List<Class<Annotation>> annotations = new ArrayList<Class<Annotation>>();
private final List<Class<?>> assignableTypes = new ArrayList<Class<?>>();
private static final Log logger = LogFactory.getLog(ControllerAdviceBean.class);
/**
* Create an instance using the given bean name.
@ -59,7 +72,11 @@ public class ControllerAdviceBean implements Ordered {
"Bean factory [" + beanFactory + "] does not contain bean " + "with name [" + beanName + "]");
this.bean = beanName;
this.beanFactory = beanFactory;
this.order = initOrderFromBeanType(this.beanFactory.getType(beanName));
Class<?> beanType = this.beanFactory.getType(beanName);
this.order = initOrderFromBeanType(beanType);
this.basePackages.addAll(initBasePackagesFromBeanType(beanType));
this.annotations.addAll(initAnnotationsFromBeanType(beanType));
this.assignableTypes.addAll(initAssignableTypesFromBeanType(beanType));
}
private static int initOrderFromBeanType(Class<?> beanType) {
@ -67,6 +84,56 @@ public class ControllerAdviceBean implements Ordered {
return (annot != null) ? annot.value() : Ordered.LOWEST_PRECEDENCE;
}
private static List<Package> initBasePackagesFromBeanType(Class<?> beanType) {
List<Package> basePackages = new ArrayList<Package>();
ControllerAdvice annotation = AnnotationUtils.findAnnotation(beanType,ControllerAdvice.class);
Assert.notNull(annotation,"BeanType ["+beanType.getName()+"] is not annotated @ControllerAdvice");
for (String pkgName : (String[])AnnotationUtils.getValue(annotation)) {
if (StringUtils.hasText(pkgName)) {
Package pack = Package.getPackage(pkgName);
if(pack != null) {
basePackages.add(pack);
} else {
logger.warn("Package [" + pkgName + "] was not found, see ["
+ beanType.getName() + "]");
}
}
}
for (String pkgName : (String[])AnnotationUtils.getValue(annotation,"basePackages")) {
if (StringUtils.hasText(pkgName)) {
Package pack = Package.getPackage(pkgName);
if(pack != null) {
basePackages.add(pack);
} else {
logger.warn("Package [" + pkgName + "] was not found, see ["
+ beanType.getName() + "]");
}
}
}
for (Class<?> markerClass : (Class<?>[])AnnotationUtils.getValue(annotation,"basePackageClasses")) {
Package pack = markerClass.getPackage();
if(pack != null) {
basePackages.add(pack);
} else {
logger.warn("Package was not found for class [" + markerClass.getName()
+ "], see [" + beanType.getName() + "]");
}
}
return basePackages;
}
private static List<Class<Annotation>> initAnnotationsFromBeanType(Class<?> beanType) {
ControllerAdvice annotation = AnnotationUtils.findAnnotation(beanType,ControllerAdvice.class);
Class<Annotation>[] annotations = (Class<Annotation>[])AnnotationUtils.getValue(annotation,"annotations");
return Arrays.asList(annotations);
}
private static List<Class<?>> initAssignableTypesFromBeanType(Class<?> beanType) {
ControllerAdvice annotation = AnnotationUtils.findAnnotation(beanType,ControllerAdvice.class);
Class<?>[] assignableTypes = (Class<?>[])AnnotationUtils.getValue(annotation,"assignableTypes");
return Arrays.asList(assignableTypes);
}
/**
* Create an instance using the given bean instance.
* @param bean the bean
@ -75,6 +142,9 @@ public class ControllerAdviceBean implements Ordered {
Assert.notNull(bean, "'bean' must not be null");
this.bean = bean;
this.order = initOrderFromBean(bean);
this.basePackages.addAll(initBasePackagesFromBeanType(bean.getClass()));
this.annotations.addAll(initAnnotationsFromBeanType(bean.getClass()));
this.assignableTypes.addAll(initAssignableTypesFromBeanType(bean.getClass()));
this.beanFactory = null;
}
@ -124,6 +194,45 @@ public class ControllerAdviceBean implements Ordered {
return (this.bean instanceof String) ? this.beanFactory.getBean((String) this.bean) : this.bean;
}
/**
* Checks whether the bean type given as a parameter should be assisted by
* the current {@code @ControllerAdvice} annotated bean.
*
* @param beanType the type of the bean
* @see org.springframework.web.bind.annotation.ControllerAdvice
* @since 4.0
*/
public boolean isApplicableToBeanType(Class<?> beanType) {
if(hasNoSelector()) {
return true;
}
else if(beanType != null) {
String packageName = beanType.getPackage().getName();
for(Package basePackage : this.basePackages) {
if(packageName.startsWith(basePackage.getName())) {
return true;
}
}
for(Class<Annotation> annotationClass : this.annotations) {
if(AnnotationUtils.findAnnotation(beanType, annotationClass) != null) {
return true;
}
}
for(Class<?> clazz : this.assignableTypes) {
if(ClassUtils.isAssignable(clazz, beanType)) {
return true;
}
}
}
return false;
}
private boolean hasNoSelector() {
return this.basePackages.isEmpty() && this.annotations.isEmpty()
&& this.assignableTypes.isEmpty();
}
@Override
public boolean equals(Object o) {
if (this == o) {

View File

@ -0,0 +1,144 @@
package org.springframework.web.method;
import org.junit.Test;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.RestController;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import static org.junit.Assert.*;
/**
* @author Brian Clozel
*/
public class ControllerAdviceBeanTests {
@Test
public void shouldMatchAll() {
ControllerAdviceBean bean = new ControllerAdviceBean(new SimpleControllerAdvice());
assertApplicable("should match all", bean, AnnotatedController.class);
assertApplicable("should match all", bean, ImplementationController.class);
assertApplicable("should match all", bean, InheritanceController.class);
assertApplicable("should match all", bean, String.class);
}
@Test
public void basePackageSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new BasePackageSupport());
assertApplicable("base package support", bean, AnnotatedController.class);
assertApplicable("base package support", bean, ImplementationController.class);
assertApplicable("base package support", bean, InheritanceController.class);
assertNotApplicable("bean not in package", bean, String.class);
}
@Test
public void basePackageValueSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new BasePackageValueSupport());
assertApplicable("base package support", bean, AnnotatedController.class);
assertApplicable("base package support", bean, ImplementationController.class);
assertApplicable("base package support", bean, InheritanceController.class);
assertNotApplicable("bean not in package", bean, String.class);
}
@Test
public void annotationSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new AnnotationSupport());
assertApplicable("annotation support", bean, AnnotatedController.class);
assertNotApplicable("this bean is not annotated", bean, InheritanceController.class);
}
@Test
public void markerClassSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new MarkerClassSupport());
assertApplicable("base package class support", bean, AnnotatedController.class);
assertApplicable("base package class support", bean, ImplementationController.class);
assertApplicable("base package class support", bean, InheritanceController.class);
assertNotApplicable("bean not in package", bean, String.class);
}
@Test
public void shouldNotMatch() {
ControllerAdviceBean bean = new ControllerAdviceBean(new ShouldNotMatch());
assertNotApplicable("should not match", bean, AnnotatedController.class);
assertNotApplicable("should not match", bean, ImplementationController.class);
assertNotApplicable("should not match", bean, InheritanceController.class);
assertNotApplicable("should not match", bean, String.class);
}
@Test
public void assignableTypesSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new AssignableTypesSupport());
assertApplicable("controller implements assignable", bean, ImplementationController.class);
assertApplicable("controller inherits assignable", bean, InheritanceController.class);
assertNotApplicable("not assignable", bean, AnnotatedController.class);
assertNotApplicable("not assignable", bean, String.class);
}
@Test
public void multipleMatch() {
ControllerAdviceBean bean = new ControllerAdviceBean(new MultipleSelectorsSupport());
assertApplicable("controller implements assignable", bean, ImplementationController.class);
assertApplicable("controller is annotated", bean, AnnotatedController.class);
assertNotApplicable("should not match", bean, InheritanceController.class);
}
private void assertApplicable(String message, ControllerAdviceBean controllerAdvice,
Class<?> controllerBeanType) {
assertNotNull(controllerAdvice);
assertTrue(message,controllerAdvice.isApplicableToBeanType(controllerBeanType));
}
private void assertNotApplicable(String message, ControllerAdviceBean controllerAdvice,
Class<?> controllerBeanType) {
assertNotNull(controllerAdvice);
assertFalse(message,controllerAdvice.isApplicableToBeanType(controllerBeanType));
}
// ControllerAdvice classes
@ControllerAdvice
static class SimpleControllerAdvice {}
@ControllerAdvice(annotations = ControllerAnnotation.class)
static class AnnotationSupport {}
@ControllerAdvice(basePackageClasses = MarkerClass.class)
static class MarkerClassSupport {}
@ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class})
static class AssignableTypesSupport {}
@ControllerAdvice(basePackages = "org.springframework.web.method")
static class BasePackageSupport {}
@ControllerAdvice("org.springframework.web.method")
static class BasePackageValueSupport {}
@ControllerAdvice(annotations = ControllerAnnotation.class,
assignableTypes = ControllerInterface.class)
static class MultipleSelectorsSupport {}
@ControllerAdvice(basePackages = "java.util",
annotations = RestController.class)
static class ShouldNotMatch {}
// Support classes
static class MarkerClass {}
@Retention(RetentionPolicy.RUNTIME)
static @interface ControllerAnnotation {}
@ControllerAnnotation
public static class AnnotatedController {}
static interface ControllerInterface {}
static class ImplementationController implements ControllerInterface {}
static abstract class AbstractController {}
static class InheritanceController extends AbstractController {}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@ -351,8 +351,8 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
* @return a method to handle the exception, or {@code null}
*/
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = (handlerMethod != null) ? handlerMethod.getBeanType() : null;
if (handlerMethod != null) {
Class<?> handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
@ -364,9 +364,11 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
}
}
for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
Method method = entry.getValue().resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
if(entry.getKey().isApplicableToBeanType(handlerType)) {
Method method = entry.getValue().resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
}
}
}
return null;

View File

@ -776,9 +776,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
List<InvocableHandlerMethod> attrMethods = new ArrayList<InvocableHandlerMethod>();
// Global methods first
for (Entry<ControllerAdviceBean, Set<Method>> entry : this.modelAttributeAdviceCache.entrySet()) {
Object bean = entry.getKey().resolveBean();
for (Method method : entry.getValue()) {
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
if(entry.getKey().isApplicableToBeanType(handlerType)) {
Object bean = entry.getKey().resolveBean();
for (Method method : entry.getValue()) {
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
}
}
for (Method method : methods) {
@ -806,9 +808,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<InvocableHandlerMethod>();
// Global methods first
for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache .entrySet()) {
Object bean = entry.getKey().resolveBean();
for (Method method : entry.getValue()) {
initBinderMethods.add(createInitBinderMethod(bean, method));
if(entry.getKey().isApplicableToBeanType(handlerType)) {
Object bean = entry.getKey().resolveBean();
for (Method method : entry.getValue()) {
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
}
for (Method method : methods) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@ -204,6 +204,35 @@ public class ExceptionHandlerExceptionResolverTests {
assertEquals("TestExceptionResolver: IllegalStateException", this.response.getContentAsString());
}
@Test
public void resolveExceptionControllerAdviceHandler() throws Exception {
AnnotationConfigApplicationContext cxt = new AnnotationConfigApplicationContext(MyControllerAdviceConfig.class);
this.resolver.setApplicationContext(cxt);
this.resolver.afterPropertiesSet();
IllegalStateException ex = new IllegalStateException();
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("BasePackageTestExceptionResolver: IllegalStateException", this.response.getContentAsString());
}
@Test
public void resolveExceptionControllerAdviceNoHandler() throws Exception {
AnnotationConfigApplicationContext cxt = new AnnotationConfigApplicationContext(MyControllerAdviceConfig.class);
this.resolver.setApplicationContext(cxt);
this.resolver.afterPropertiesSet();
IllegalStateException ex = new IllegalStateException();
ModelAndView mav = this.resolver.resolveException(this.request, this.response, null, ex);
assertNotNull("Exception was not handled", mav);
assertTrue(mav.isEmpty());
assertEquals("DefaultTestExceptionResolver: IllegalStateException", this.response.getContentAsString());
}
private void assertMethodProcessorCount(int resolverCount, int handlerCount) {
assertEquals(resolverCount, this.resolver.getArgumentResolvers().getResolvers().size());
@ -288,4 +317,51 @@ public class ExceptionHandlerExceptionResolverTests {
}
}
@ControllerAdvice("java.lang")
@Order(1)
static class NotCalledTestExceptionResolver {
@ExceptionHandler
@ResponseBody
public String handleException(IllegalStateException ex) {
return "NotCalledTestExceptionResolver: " + ClassUtils.getShortName(ex.getClass());
}
}
@ControllerAdvice("org.springframework.web.servlet.mvc.method.annotation")
@Order(2)
static class BasePackageTestExceptionResolver {
@ExceptionHandler
@ResponseBody
public String handleException(IllegalStateException ex) {
return "BasePackageTestExceptionResolver: " + ClassUtils.getShortName(ex.getClass());
}
}
@ControllerAdvice
@Order(3)
static class DefaultTestExceptionResolver {
@ExceptionHandler
@ResponseBody
public String handleException(IllegalStateException ex) {
return "DefaultTestExceptionResolver: " + ClassUtils.getShortName(ex.getClass());
}
}
@Configuration
static class MyControllerAdviceConfig {
@Bean public NotCalledTestExceptionResolver notCalledTestExceptionResolver() {
return new NotCalledTestExceptionResolver();
}
@Bean public BasePackageTestExceptionResolver basePackageTestExceptionResolver() {
return new BasePackageTestExceptionResolver();
}
@Bean public DefaultTestExceptionResolver defaultTestExceptionResolver() {
return new DefaultTestExceptionResolver();
}
}
}

View File

@ -25,7 +25,9 @@ import org.junit.Test;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.support.StaticWebApplicationContext;
@ -184,6 +186,21 @@ public class RequestMappingHandlerAdapterTests {
assertEquals("gAttr2", mav.getModel().get("attr2"));
}
@Test
public void modelAttributePackageNameAdvice() throws Exception {
this.webAppContext.registerSingleton("mapa", ModelAttributePackageAdvice.class);
this.webAppContext.registerSingleton("manupa", ModelAttributeNotUsedPackageAdvice.class);
this.webAppContext.refresh();
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
this.handlerAdapter.afterPropertiesSet();
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
assertEquals("lAttr1", mav.getModel().get("attr1"));
assertEquals("gAttr2", mav.getModel().get("attr2"));
assertEquals(null,mav.getModel().get("attr3"));
}
private HandlerMethod handlerMethod(Object handler, String methodName, Class<?>... paramTypes) throws Exception {
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
@ -237,4 +254,21 @@ public class RequestMappingHandlerAdapterTests {
}
}
@ControllerAdvice({"org.springframework.web.servlet.mvc.method.annotation","java.lang"})
private static class ModelAttributePackageAdvice {
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("attr2", "gAttr2");
}
}
@ControllerAdvice("java.lang")
private static class ModelAttributeNotUsedPackageAdvice {
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("attr3", "gAttr3");
}
}
}