Ensure ErrorControllers work when using AOP

Add a BeanFactoryPostProcessor to set PRESERVE_TARGET_CLASS_ATTRIBUTE
to true on all ErrorController bean definitions. Without this attribute
AOP advice on @Controllers causes ErrorController beans to be created
as JDK proxies (since they implement a single valid looking interface)
and therefore not get found by Spring MVC.

Fixes gh-4236
This commit is contained in:
Phillip Webb 2015-10-20 13:42:38 -07:00
parent ee93307402
commit cfbac20807
2 changed files with 73 additions and 2 deletions

View File

@ -24,8 +24,12 @@ import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
@ -102,6 +106,11 @@ public class ErrorMvcAutoConfiguration
new ErrorPage(this.properties.getServletPrefix() + this.errorPath));
}
@Bean
public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() {
return new PreserveErrorControllerTargetClassPostProcessor();
}
@Configuration
@ConditionalOnProperty(prefix = "error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
@ -225,4 +234,29 @@ public class ErrorMvcAutoConfiguration
}
/**
* {@link BeanFactoryPostProcessor} to ensure that the target class of ErrorController
* MVC beans are preserved when using AOP.
*/
static class PreserveErrorControllerTargetClassPostProcessor
implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
String[] errorControllerBeans = beanFactory
.getBeanNamesForType(ErrorController.class, false, false);
for (String errorControllerBean : errorControllerBeans) {
try {
beanFactory.getBeanDefinition(errorControllerBean).setAttribute(
AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
}
catch (Throwable ex) {
// Ignore
}
}
}
}
}

View File

@ -24,6 +24,10 @@ import java.lang.annotation.Target;
import javax.servlet.ServletException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
@ -34,6 +38,7 @@ import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfigurati
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.ApplicationContextTestUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ -101,11 +106,21 @@ public class BasicErrorControllerDirectMockMvcTests {
setup((ConfigurableWebApplicationContext) new SpringApplication(
WebMvcIncludedConfiguration.class).run("--server.port=0",
"--error.whitelabel.enabled=false"));
thrown.expect(ServletException.class);
this.thrown.expect(ServletException.class);
this.mockMvc.perform(get("/error").accept(MediaType.TEXT_HTML));
}
@Test
public void errorControllerWithAop() throws Exception {
setup((ConfigurableWebApplicationContext) new SpringApplication(
WithAopConfiguration.class).run("--server.port=0"));
MvcResult response = this.mockMvc
.perform(get("/error").accept(MediaType.TEXT_HTML))
.andExpect(status().isOk()).andReturn();
String content = response.getResponse().getContentAsString();
assertTrue("Wrong content: " + content, content.contains("status=999"));
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ -115,6 +130,7 @@ public class BasicErrorControllerDirectMockMvcTests {
HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected static @interface MinimalWebConfiguration {
}
@Configuration
@ -127,6 +143,7 @@ public class BasicErrorControllerDirectMockMvcTests {
@MinimalWebConfiguration
@EnableWebMvc
protected static class WebMvcIncludedConfiguration {
// For manual testing
public static void main(String[] args) {
SpringApplication.run(WebMvcIncludedConfiguration.class, args);
@ -137,6 +154,7 @@ public class BasicErrorControllerDirectMockMvcTests {
@Configuration
@MinimalWebConfiguration
protected static class VanillaConfiguration {
// For manual testing
public static void main(String[] args) {
SpringApplication.run(VanillaConfiguration.class, args);
@ -147,11 +165,30 @@ public class BasicErrorControllerDirectMockMvcTests {
@Configuration
@MinimalWebConfiguration
protected static class ChildConfiguration {
// For manual testing
public static void main(String[] args) {
new SpringApplicationBuilder(ParentConfiguration.class)
.child(ChildConfiguration.class).run(args);
}
}
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false)
@MinimalWebConfiguration
@Aspect
protected static class WithAopConfiguration {
@Pointcut("within(@org.springframework.stereotype.Controller *)")
private void controllerPointCut() {
};
@Around("controllerPointCut()")
public Object mvcAdvice(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
}
}