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) {