Document ControllerAdviceBean as internal usage

This commit documents `ControllerAdviceBean` as internal usage, as it is
not meant for application to manually create controller advice bean
instances.

This also refactors the existing partial implementation of the support
for creating controller advice beans "programmatically".

Closes gh-32776
This commit is contained in:
Brian Clozel 2024-07-26 17:52:20 +02:00
parent 94e2bef9a3
commit b888f362e5
3 changed files with 138 additions and 175 deletions

View File

@ -33,7 +33,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.OrderComparator; import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.OrderUtils; import org.springframework.core.annotation.OrderUtils;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -43,10 +42,11 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
/** /**
* Encapsulates information about an {@link ControllerAdvice @ControllerAdvice} * Encapsulates information about an {@link ControllerAdvice @ControllerAdvice}
* Spring-managed bean without necessarily requiring it to be instantiated. * Spring-managed bean without necessarily requiring it to be instantiated.
* The {@link #findAnnotatedBeans(ApplicationContext)} method can be used to
* discover such beans.
* *
* <p>The {@link #findAnnotatedBeans(ApplicationContext)} method can be used to * <p>This class is internal to Spring Framework and is not meant to be used
* discover such beans. However, a {@code ControllerAdviceBean} may be created * by applications to manually create {@code @ControllerAdvice} beans.
* from any object, including ones without an {@code @ControllerAdvice} annotation.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Brian Clozel * @author Brian Clozel
@ -56,11 +56,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
*/ */
public class ControllerAdviceBean implements Ordered { public class ControllerAdviceBean implements Ordered {
/** private final String beanName;
* Reference to the actual bean instance or a {@code String} representing
* the bean name.
*/
private final Object beanOrName;
private final boolean isSingleton; private final boolean isSingleton;
@ -76,38 +72,12 @@ public class ControllerAdviceBean implements Ordered {
private final HandlerTypePredicate beanTypePredicate; private final HandlerTypePredicate beanTypePredicate;
@Nullable
private final BeanFactory beanFactory; private final BeanFactory beanFactory;
@Nullable @Nullable
private Integer order; private Integer order;
/**
* Create a {@code ControllerAdviceBean} using the given bean instance.
* @param bean the bean instance
*/
public ControllerAdviceBean(Object bean) {
Assert.notNull(bean, "Bean must not be null");
this.beanOrName = bean;
this.isSingleton = true;
this.resolvedBean = bean;
this.beanType = ClassUtils.getUserClass(bean.getClass());
this.beanTypePredicate = createBeanTypePredicate(this.beanType);
this.beanFactory = null;
}
/**
* Create a {@code ControllerAdviceBean} using the given bean name and
* {@code BeanFactory}.
* @param beanName the name of the bean
* @param beanFactory a {@code BeanFactory} to retrieve the bean type initially
* and later to resolve the actual bean
*/
public ControllerAdviceBean(String beanName, BeanFactory beanFactory) {
this(beanName, beanFactory, null);
}
/** /**
* Create a {@code ControllerAdviceBean} using the given bean name, * Create a {@code ControllerAdviceBean} using the given bean name,
* {@code BeanFactory}, and {@link ControllerAdvice @ControllerAdvice} * {@code BeanFactory}, and {@link ControllerAdvice @ControllerAdvice}
@ -115,21 +85,20 @@ public class ControllerAdviceBean implements Ordered {
* @param beanName the name of the bean * @param beanName the name of the bean
* @param beanFactory a {@code BeanFactory} to retrieve the bean type initially * @param beanFactory a {@code BeanFactory} to retrieve the bean type initially
* and later to resolve the actual bean * and later to resolve the actual bean
* @param controllerAdvice the {@code @ControllerAdvice} annotation for the * @param controllerAdvice the {@code @ControllerAdvice} annotation for the bean
* bean, or {@code null} if not yet retrieved
* @since 5.2 * @since 5.2
*/ */
public ControllerAdviceBean(String beanName, BeanFactory beanFactory, @Nullable ControllerAdvice controllerAdvice) { public ControllerAdviceBean(String beanName, BeanFactory beanFactory, ControllerAdvice controllerAdvice) {
Assert.hasText(beanName, "Bean name must contain text"); Assert.hasText(beanName, "Bean name must contain text");
Assert.notNull(beanFactory, "BeanFactory must not be null"); Assert.notNull(beanFactory, "BeanFactory must not be null");
Assert.isTrue(beanFactory.containsBean(beanName), () -> "BeanFactory [" + beanFactory + Assert.isTrue(beanFactory.containsBean(beanName), () -> "BeanFactory [" + beanFactory +
"] does not contain specified controller advice bean '" + beanName + "'"); "] does not contain specified controller advice bean '" + beanName + "'");
Assert.notNull(controllerAdvice, "ControllerAdvice must not be null");
this.beanOrName = beanName; this.beanName = beanName;
this.isSingleton = beanFactory.isSingleton(beanName); this.isSingleton = beanFactory.isSingleton(beanName);
this.beanType = getBeanType(beanName, beanFactory); this.beanType = getBeanType(beanName, beanFactory);
this.beanTypePredicate = (controllerAdvice != null ? createBeanTypePredicate(controllerAdvice) : this.beanTypePredicate = createBeanTypePredicate(controllerAdvice);
createBeanTypePredicate(this.beanType));
this.beanFactory = beanFactory; this.beanFactory = beanFactory;
} }
@ -158,21 +127,14 @@ public class ControllerAdviceBean implements Ordered {
@Override @Override
public int getOrder() { public int getOrder() {
if (this.order == null) { if (this.order == null) {
String beanName = null;
Object resolvedBean = null; Object resolvedBean = null;
if (this.beanFactory != null && this.beanOrName instanceof String stringBeanName) { String targetBeanName = ScopedProxyUtils.getTargetBeanName(this.beanName);
beanName = stringBeanName;
String targetBeanName = ScopedProxyUtils.getTargetBeanName(beanName);
boolean isScopedProxy = this.beanFactory.containsBean(targetBeanName); boolean isScopedProxy = this.beanFactory.containsBean(targetBeanName);
// Avoid eager @ControllerAdvice bean resolution for scoped proxies, // Avoid eager @ControllerAdvice bean resolution for scoped proxies,
// since attempting to do so during context initialization would result // since attempting to do so during context initialization would result
// in an exception due to the current absence of the scope. For example, // in an exception due to the current absence of the scope. For example,
// an HTTP request or session scope is not active during initialization. // an HTTP request or session scope is not active during initialization.
if (!isScopedProxy && !ScopedProxyUtils.isScopedTarget(beanName)) { if (!isScopedProxy && !ScopedProxyUtils.isScopedTarget(this.beanName)) {
resolvedBean = resolveBean();
}
}
else {
resolvedBean = resolveBean(); resolvedBean = resolveBean();
} }
@ -180,9 +142,9 @@ public class ControllerAdviceBean implements Ordered {
this.order = ordered.getOrder(); this.order = ordered.getOrder();
} }
else { else {
if (beanName != null && this.beanFactory instanceof ConfigurableBeanFactory cbf) { if (this.beanFactory instanceof ConfigurableBeanFactory cbf) {
try { try {
BeanDefinition bd = cbf.getMergedBeanDefinition(beanName); BeanDefinition bd = cbf.getMergedBeanDefinition(this.beanName);
if (bd instanceof RootBeanDefinition rbd) { if (bd instanceof RootBeanDefinition rbd) {
Method factoryMethod = rbd.getResolvedFactoryMethod(); Method factoryMethod = rbd.getResolvedFactoryMethod();
if (factoryMethod != null) { if (factoryMethod != null) {
@ -225,9 +187,7 @@ public class ControllerAdviceBean implements Ordered {
*/ */
public Object resolveBean() { public Object resolveBean() {
if (this.resolvedBean == null) { if (this.resolvedBean == null) {
// this.beanOrName must be a String representing the bean name if Object resolvedBean = this.beanFactory.getBean(this.beanName);
// this.resolvedBean is null.
Object resolvedBean = obtainBeanFactory().getBean((String) this.beanOrName);
// Don't cache non-singletons (e.g., prototypes). // Don't cache non-singletons (e.g., prototypes).
if (!this.isSingleton) { if (!this.isSingleton) {
return resolvedBean; return resolvedBean;
@ -237,11 +197,6 @@ public class ControllerAdviceBean implements Ordered {
return this.resolvedBean; return this.resolvedBean;
} }
private BeanFactory obtainBeanFactory() {
Assert.state(this.beanFactory != null, "No BeanFactory set");
return this.beanFactory;
}
/** /**
* Check whether the given bean type should be advised by this * Check whether the given bean type should be advised by this
* {@code ControllerAdviceBean}. * {@code ControllerAdviceBean}.
@ -257,17 +212,17 @@ public class ControllerAdviceBean implements Ordered {
@Override @Override
public boolean equals(@Nullable Object other) { public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof ControllerAdviceBean that && return (this == other || (other instanceof ControllerAdviceBean that &&
this.beanOrName.equals(that.beanOrName) && this.beanFactory == that.beanFactory)); this.beanName.equals(that.beanName) && this.beanFactory == that.beanFactory));
} }
@Override @Override
public int hashCode() { public int hashCode() {
return this.beanOrName.hashCode(); return this.beanName.hashCode();
} }
@Override @Override
public String toString() { public String toString() {
return this.beanOrName.toString(); return this.beanName;
} }
@ -308,14 +263,7 @@ public class ControllerAdviceBean implements Ordered {
return (beanType != null ? ClassUtils.getUserClass(beanType) : null); return (beanType != null ? ClassUtils.getUserClass(beanType) : null);
} }
private static HandlerTypePredicate createBeanTypePredicate(@Nullable Class<?> beanType) { private static HandlerTypePredicate createBeanTypePredicate(ControllerAdvice controllerAdvice) {
ControllerAdvice controllerAdvice = (beanType != null ?
AnnotatedElementUtils.findMergedAnnotation(beanType, ControllerAdvice.class) : null);
return createBeanTypePredicate(controllerAdvice);
}
private static HandlerTypePredicate createBeanTypePredicate(@Nullable ControllerAdvice controllerAdvice) {
if (controllerAdvice != null) {
return HandlerTypePredicate.builder() return HandlerTypePredicate.builder()
.basePackage(controllerAdvice.basePackages()) .basePackage(controllerAdvice.basePackages())
.basePackageClass(controllerAdvice.basePackageClasses()) .basePackageClass(controllerAdvice.basePackageClasses())
@ -323,7 +271,5 @@ public class ControllerAdviceBean implements Ordered {
.annotation(controllerAdvice.annotations()) .annotation(controllerAdvice.annotations())
.build(); .build();
} }
return HandlerTypePredicate.forAnyHandlerType();
}
} }

View File

@ -23,22 +23,23 @@ import java.util.List;
import jakarta.annotation.Priority; import jakarta.annotation.Priority;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered; import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/** /**
* Unit and integration tests for {@link ControllerAdviceBean}. * Unit and integration tests for {@link ControllerAdviceBean}.
@ -48,52 +49,50 @@ import static org.mockito.Mockito.verify;
*/ */
class ControllerAdviceBeanTests { class ControllerAdviceBeanTests {
private StaticApplicationContext applicationContext = new StaticApplicationContext();
@Test @Test
void constructorPreconditions() { void shouldFailForNullOrEmptyBeanName() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> new ControllerAdviceBean(null)) .isThrownBy(() -> new ControllerAdviceBean(null, null, null))
.withMessage("Bean must not be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> new ControllerAdviceBean(null, null))
.withMessage("Bean name must contain text"); .withMessage("Bean name must contain text");
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> new ControllerAdviceBean("", null)) .isThrownBy(() -> new ControllerAdviceBean(" ", null, null))
.withMessage("Bean name must contain text"); .withMessage("Bean name must contain text");
}
@Test
void shouldFailForNullBeanFactory() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> new ControllerAdviceBean("\t", null)) .isThrownBy(() -> new ControllerAdviceBean("beanName", null, null))
.withMessage("Bean name must contain text");
assertThatIllegalArgumentException()
.isThrownBy(() -> new ControllerAdviceBean("myBean", null))
.withMessage("BeanFactory must not be null"); .withMessage("BeanFactory must not be null");
} }
@Test @Test
void equalsHashCodeAndToStringForBeanName() { void shouldFailWhenBeanFactoryDoesNotContainBean() {
String beanName = "myBean"; BeanFactory beanFactory = mock(BeanFactory.class);
BeanFactory beanFactory = mock(); given(beanFactory.containsBean(eq("beanName"))).willReturn(false);
given(beanFactory.containsBean(beanName)).willReturn(true); assertThatIllegalArgumentException()
.isThrownBy(() -> new ControllerAdviceBean("beanName", beanFactory, null))
ControllerAdviceBean bean1 = new ControllerAdviceBean(beanName, beanFactory); .withMessageContaining("does not contain specified controller advice bean 'beanName'");
ControllerAdviceBean bean2 = new ControllerAdviceBean(beanName, beanFactory);
assertEqualsHashCodeAndToString(bean1, bean2, beanName);
} }
@Test @Test
void equalsHashCodeAndToStringForBeanInstance() { void shouldFailWhenControllerAdviceNull() {
String toString = "beanInstance"; BeanFactory beanFactory = mock(BeanFactory.class);
Object beanInstance = new Object() { given(beanFactory.containsBean(eq("beanName"))).willReturn(true);
@Override assertThatIllegalArgumentException()
public String toString() { .isThrownBy(() -> new ControllerAdviceBean("beanName", beanFactory, null))
return toString; .withMessage("ControllerAdvice must not be null");
} }
};
ControllerAdviceBean bean1 = new ControllerAdviceBean(beanInstance); @Test
ControllerAdviceBean bean2 = new ControllerAdviceBean(beanInstance); void equalsHashCodeAndToStringForBeanName() {
assertEqualsHashCodeAndToString(bean1, bean2, toString); String beanName = SimpleControllerAdvice.class.getSimpleName();
ControllerAdviceBean bean1 = createControllerAdviceBean(SimpleControllerAdvice.class);
ControllerAdviceBean bean2 = createControllerAdviceBean(SimpleControllerAdvice.class);
assertEqualsHashCodeAndToString(bean1, bean2, beanName);
} }
@Test @Test
@ -103,7 +102,7 @@ class ControllerAdviceBeanTests {
@Test @Test
void orderedWithLowestPrecedenceByDefaultForBeanInstance() { void orderedWithLowestPrecedenceByDefaultForBeanInstance() {
assertOrder(new SimpleControllerAdvice(), Ordered.LOWEST_PRECEDENCE); assertOrder(SimpleControllerAdvice.class, Ordered.LOWEST_PRECEDENCE);
} }
@Test @Test
@ -113,7 +112,7 @@ class ControllerAdviceBeanTests {
@Test @Test
void orderedViaOrderedInterfaceForBeanInstance() { void orderedViaOrderedInterfaceForBeanInstance() {
assertOrder(new OrderedControllerAdvice(), 42); assertOrder(OrderedControllerAdvice.class, 42);
} }
@Test @Test
@ -124,13 +123,13 @@ class ControllerAdviceBeanTests {
@Test @Test
void orderedViaAnnotationForBeanInstance() { void orderedViaAnnotationForBeanInstance() {
assertOrder(new OrderAnnotationControllerAdvice(), 100); assertOrder(OrderAnnotationControllerAdvice.class, 100);
assertOrder(new PriorityAnnotationControllerAdvice(), 200); assertOrder(PriorityAnnotationControllerAdvice.class, 200);
} }
@Test @Test
void shouldMatchAll() { void shouldMatchAll() {
ControllerAdviceBean bean = new ControllerAdviceBean(new SimpleControllerAdvice()); ControllerAdviceBean bean = createControllerAdviceBean(SimpleControllerAdvice.class);
assertApplicable("should match all", bean, AnnotatedController.class); assertApplicable("should match all", bean, AnnotatedController.class);
assertApplicable("should match all", bean, ImplementationController.class); assertApplicable("should match all", bean, ImplementationController.class);
assertApplicable("should match all", bean, InheritanceController.class); assertApplicable("should match all", bean, InheritanceController.class);
@ -139,7 +138,7 @@ class ControllerAdviceBeanTests {
@Test @Test
void basePackageSupport() { void basePackageSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new BasePackageSupport()); ControllerAdviceBean bean = createControllerAdviceBean(BasePackageSupport.class);
assertApplicable("base package support", bean, AnnotatedController.class); assertApplicable("base package support", bean, AnnotatedController.class);
assertApplicable("base package support", bean, ImplementationController.class); assertApplicable("base package support", bean, ImplementationController.class);
assertApplicable("base package support", bean, InheritanceController.class); assertApplicable("base package support", bean, InheritanceController.class);
@ -148,7 +147,7 @@ class ControllerAdviceBeanTests {
@Test @Test
void basePackageValueSupport() { void basePackageValueSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new BasePackageValueSupport()); ControllerAdviceBean bean = createControllerAdviceBean(BasePackageValueSupport.class);
assertApplicable("base package support", bean, AnnotatedController.class); assertApplicable("base package support", bean, AnnotatedController.class);
assertApplicable("base package support", bean, ImplementationController.class); assertApplicable("base package support", bean, ImplementationController.class);
assertApplicable("base package support", bean, InheritanceController.class); assertApplicable("base package support", bean, InheritanceController.class);
@ -157,14 +156,14 @@ class ControllerAdviceBeanTests {
@Test @Test
void annotationSupport() { void annotationSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new AnnotationSupport()); ControllerAdviceBean bean = createControllerAdviceBean(AnnotationSupport.class);
assertApplicable("annotation support", bean, AnnotatedController.class); assertApplicable("annotation support", bean, AnnotatedController.class);
assertNotApplicable("this bean is not annotated", bean, InheritanceController.class); assertNotApplicable("this bean is not annotated", bean, InheritanceController.class);
} }
@Test @Test
void markerClassSupport() { void markerClassSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new MarkerClassSupport()); ControllerAdviceBean bean = createControllerAdviceBean(MarkerClassSupport.class);
assertApplicable("base package class support", bean, AnnotatedController.class); assertApplicable("base package class support", bean, AnnotatedController.class);
assertApplicable("base package class support", bean, ImplementationController.class); assertApplicable("base package class support", bean, ImplementationController.class);
assertApplicable("base package class support", bean, InheritanceController.class); assertApplicable("base package class support", bean, InheritanceController.class);
@ -173,7 +172,7 @@ class ControllerAdviceBeanTests {
@Test @Test
void shouldNotMatch() { void shouldNotMatch() {
ControllerAdviceBean bean = new ControllerAdviceBean(new ShouldNotMatch()); ControllerAdviceBean bean = createControllerAdviceBean(ShouldNotMatch.class);
assertNotApplicable("should not match", bean, AnnotatedController.class); assertNotApplicable("should not match", bean, AnnotatedController.class);
assertNotApplicable("should not match", bean, ImplementationController.class); assertNotApplicable("should not match", bean, ImplementationController.class);
assertNotApplicable("should not match", bean, InheritanceController.class); assertNotApplicable("should not match", bean, InheritanceController.class);
@ -182,7 +181,7 @@ class ControllerAdviceBeanTests {
@Test @Test
void assignableTypesSupport() { void assignableTypesSupport() {
ControllerAdviceBean bean = new ControllerAdviceBean(new AssignableTypesSupport()); ControllerAdviceBean bean = createControllerAdviceBean(AssignableTypesSupport.class);
assertApplicable("controller implements assignable", bean, ImplementationController.class); assertApplicable("controller implements assignable", bean, ImplementationController.class);
assertApplicable("controller inherits assignable", bean, InheritanceController.class); assertApplicable("controller inherits assignable", bean, InheritanceController.class);
assertNotApplicable("not assignable", bean, AnnotatedController.class); assertNotApplicable("not assignable", bean, AnnotatedController.class);
@ -191,7 +190,7 @@ class ControllerAdviceBeanTests {
@Test @Test
void multipleMatch() { void multipleMatch() {
ControllerAdviceBean bean = new ControllerAdviceBean(new MultipleSelectorsSupport()); ControllerAdviceBean bean = createControllerAdviceBean(MultipleSelectorsSupport.class);
assertApplicable("controller implements assignable", bean, ImplementationController.class); assertApplicable("controller implements assignable", bean, ImplementationController.class);
assertApplicable("controller is annotated", bean, AnnotatedController.class); assertApplicable("controller is annotated", bean, AnnotatedController.class);
assertNotApplicable("should not match", bean, InheritanceController.class); assertNotApplicable("should not match", bean, InheritanceController.class);
@ -217,6 +216,13 @@ class ControllerAdviceBeanTests {
assertThat(adviceBeans).extracting(ControllerAdviceBean::getBeanType).containsExactly(expectedTypes); assertThat(adviceBeans).extracting(ControllerAdviceBean::getBeanType).containsExactly(expectedTypes);
} }
private ControllerAdviceBean createControllerAdviceBean(Class<?> beanType) {
String beanName = beanType.getSimpleName();
this.applicationContext.registerSingleton(beanName, beanType);
ControllerAdvice controllerAdvice = AnnotatedElementUtils.findMergedAnnotation(beanType, ControllerAdvice.class);
return new ControllerAdviceBean(beanName, this.applicationContext, controllerAdvice);
}
private void assertEqualsHashCodeAndToString(ControllerAdviceBean bean1, ControllerAdviceBean bean2, String toString) { private void assertEqualsHashCodeAndToString(ControllerAdviceBean bean1, ControllerAdviceBean bean2, String toString) {
assertThat(bean1).isEqualTo(bean2); assertThat(bean1).isEqualTo(bean2);
assertThat(bean2).isEqualTo(bean1); assertThat(bean2).isEqualTo(bean1);
@ -225,24 +231,8 @@ class ControllerAdviceBeanTests {
assertThat(bean2.toString()).isEqualTo(toString); assertThat(bean2.toString()).isEqualTo(toString);
} }
private void assertOrder(Object bean, int expectedOrder) { private void assertOrder(Class<?> beanType, int expectedOrder) {
assertThat(new ControllerAdviceBean(bean).getOrder()).isEqualTo(expectedOrder); assertThat(createControllerAdviceBean(beanType).getOrder()).isEqualTo(expectedOrder);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void assertOrder(Class beanType, int expectedOrder) {
String beanName = "myBean";
BeanFactory beanFactory = mock();
given(beanFactory.containsBean(beanName)).willReturn(true);
given(beanFactory.getType(beanName)).willReturn(beanType);
given(beanFactory.getBean(beanName)).willReturn(BeanUtils.instantiateClass(beanType));
ControllerAdviceBean controllerAdviceBean = new ControllerAdviceBean(beanName, beanFactory);
assertThat(controllerAdviceBean.getOrder()).isEqualTo(expectedOrder);
verify(beanFactory).containsBean(beanName);
verify(beanFactory).getType(beanName);
verify(beanFactory).getBean(beanName);
} }
private void assertApplicable(String message, ControllerAdviceBean controllerAdvice, Class<?> controllerBeanType) { private void assertApplicable(String message, ControllerAdviceBean controllerAdvice, Class<?> controllerBeanType) {
@ -259,18 +249,22 @@ class ControllerAdviceBeanTests {
// ControllerAdvice classes // ControllerAdvice classes
@ControllerAdvice @ControllerAdvice
static class SimpleControllerAdvice {} static class SimpleControllerAdvice {
}
@ControllerAdvice @ControllerAdvice
static class SimpleControllerAdviceWithBeanOrder {} static class SimpleControllerAdviceWithBeanOrder {
}
@ControllerAdvice @ControllerAdvice
@Order(100) @Order(100)
static class OrderAnnotationControllerAdvice {} static class OrderAnnotationControllerAdvice {
}
@ControllerAdvice @ControllerAdvice
@Priority(200) @Priority(200)
static class PriorityAnnotationControllerAdvice {} static class PriorityAnnotationControllerAdvice {
}
@ControllerAdvice @ControllerAdvice
// @Order and @Priority should be ignored due to implementation of Ordered. // @Order and @Priority should be ignored due to implementation of Ordered.
@ -297,45 +291,59 @@ class ControllerAdviceBeanTests {
} }
@ControllerAdvice(annotations = ControllerAnnotation.class) @ControllerAdvice(annotations = ControllerAnnotation.class)
static class AnnotationSupport {} static class AnnotationSupport {
}
@ControllerAdvice(basePackageClasses = MarkerClass.class) @ControllerAdvice(basePackageClasses = MarkerClass.class)
static class MarkerClassSupport {} static class MarkerClassSupport {
}
@ControllerAdvice(assignableTypes = {ControllerInterface.class, @ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class}) AbstractController.class})
static class AssignableTypesSupport {} static class AssignableTypesSupport {
}
@ControllerAdvice(basePackages = "org.springframework.web.method") @ControllerAdvice(basePackages = "org.springframework.web.method")
static class BasePackageSupport {} static class BasePackageSupport {
}
@ControllerAdvice("org.springframework.web.method") @ControllerAdvice("org.springframework.web.method")
static class BasePackageValueSupport {} static class BasePackageValueSupport {
}
@ControllerAdvice(annotations = ControllerAnnotation.class, assignableTypes = ControllerInterface.class) @ControllerAdvice(annotations = ControllerAnnotation.class, assignableTypes = ControllerInterface.class)
static class MultipleSelectorsSupport {} static class MultipleSelectorsSupport {
}
@ControllerAdvice(basePackages = "java.util", annotations = {RestController.class}) @ControllerAdvice(basePackages = "java.util", annotations = {RestController.class})
static class ShouldNotMatch {} static class ShouldNotMatch {
}
// Support classes // Support classes
static class MarkerClass {} static class MarkerClass {
}
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface ControllerAnnotation {} @interface ControllerAnnotation {
}
@ControllerAnnotation @ControllerAnnotation
public static class AnnotatedController {} public static class AnnotatedController {
}
interface ControllerInterface {} interface ControllerInterface {
}
static class ImplementationController implements ControllerInterface {} static class ImplementationController implements ControllerInterface {
}
abstract static class AbstractController {} abstract static class AbstractController {
}
static class InheritanceController extends AbstractController {} static class InheritanceController extends AbstractController {
}
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
static class Config { static class Config {

View File

@ -23,7 +23,9 @@ import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
@ -109,7 +111,7 @@ class RequestResponseBodyAdviceChainTests {
@Test @Test
void controllerAdvice() { void controllerAdvice() {
Object adviceBean = new ControllerAdviceBean(new MyControllerAdvice()); Object adviceBean = createControllerAdviceBean(MyControllerAdvice.class);
RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(Collections.singletonList(adviceBean)); RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(Collections.singletonList(adviceBean));
String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType, String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType,
@ -120,7 +122,7 @@ class RequestResponseBodyAdviceChainTests {
@Test @Test
void controllerAdviceNotApplicable() { void controllerAdviceNotApplicable() {
Object adviceBean = new ControllerAdviceBean(new TargetedControllerAdvice()); Object adviceBean = createControllerAdviceBean(TargetedControllerAdvice.class);
RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(Collections.singletonList(adviceBean)); RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(Collections.singletonList(adviceBean));
String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType, String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType,
@ -129,6 +131,13 @@ class RequestResponseBodyAdviceChainTests {
assertThat(actual).isEqualTo(this.body); assertThat(actual).isEqualTo(this.body);
} }
private ControllerAdviceBean createControllerAdviceBean(Class<?> beanType) {
StaticApplicationContext applicationContext = new StaticApplicationContext();
applicationContext.registerSingleton(beanType.getSimpleName(), beanType);
ControllerAdvice controllerAdvice = AnnotatedElementUtils.findMergedAnnotation(beanType, ControllerAdvice.class);
return new ControllerAdviceBean(beanType.getSimpleName(), applicationContext, controllerAdvice);
}
@ControllerAdvice @ControllerAdvice
private static class MyControllerAdvice implements ResponseBodyAdvice<String> { private static class MyControllerAdvice implements ResponseBodyAdvice<String> {