diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanNotOfRequiredTypeFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanNotOfRequiredTypeFailureAnalyzer.java new file mode 100644 index 0000000000..b277a34d82 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanNotOfRequiredTypeFailureAnalyzer.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2016 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.boot.diagnostics.analyzer; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Proxy; + +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a + * {@link BeanNotOfRequiredTypeFailureAnalyzer}. + * + * @author Andy Wilkinson + */ +public class BeanNotOfRequiredTypeFailureAnalyzer + extends AbstractFailureAnalyzer { + + private static final String ACTION = "Consider injecting the bean as one of its " + + "interfaces or forcing the use of CGLib-based " + + "proxies by setting proxyTargetClass=true on @EnableAsync and/or " + + "@EnableCaching."; + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, + BeanNotOfRequiredTypeException cause) { + if (!Proxy.isProxyClass(cause.getActualType())) { + return null; + } + return new FailureAnalysis(getDescription(cause), ACTION, cause); + } + + private String getDescription(BeanNotOfRequiredTypeException ex) { + StringWriter description = new StringWriter(); + PrintWriter printer = new PrintWriter(description); + printer.printf( + "The bean '%s' could not be injected as a '%s' because it is a " + + "JDK dynamic proxy that implements:%n", + ex.getBeanName(), ex.getRequiredType().getName()); + for (Class iface : ex.getRequiredType().getInterfaces()) { + printer.println("\t" + iface.getName()); + } + return description.toString(); + } + +} diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index a9310150ce..fe6431b888 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -34,6 +34,7 @@ org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor # Failure Analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\ +org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\ diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BeanNotOfRequiredTypeFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BeanNotOfRequiredTypeFailureAnalyzerTests.java new file mode 100644 index 0000000000..417ced254b --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BeanNotOfRequiredTypeFailureAnalyzerTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2016 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.boot.diagnostics.analyzer; + +import org.junit.Test; + +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.FailureAnalyzer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * Tests for {@link BeanNotOfRequiredTypeFailureAnalyzer}. + * + * @author Andy Wilkinson + */ +public class BeanNotOfRequiredTypeFailureAnalyzerTests { + + private final FailureAnalyzer analyzer = new BeanNotOfRequiredTypeFailureAnalyzer(); + + @Test + public void jdkProxyCausesInjectionFailure() { + FailureAnalysis analysis = performAnalysis(JdkProxyConfiguration.class); + assertThat(analysis.getDescription()).startsWith("The bean 'asyncBean'"); + assertThat(analysis.getDescription()) + .contains("'" + AsyncBean.class.getName() + "'"); + assertThat(analysis.getDescription()) + .endsWith(String.format("%s%n", SomeInterface.class.getName())); + } + + private FailureAnalysis performAnalysis(Class configuration) { + FailureAnalysis analysis = this.analyzer.analyze(createFailure(configuration)); + assertThat(analysis).isNotNull(); + return analysis; + } + + private Exception createFailure(Class configuration) { + ConfigurableApplicationContext context = null; + try { + context = new AnnotationConfigApplicationContext(configuration); + } + catch (Exception ex) { + return ex; + } + finally { + if (context != null) { + context.close(); + } + } + fail("Expected failure did not occur"); + return null; + } + + @Configuration + @EnableAsync + @Import(UserConfiguration.class) + static class JdkProxyConfiguration { + + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + } + + @Configuration + static class UserConfiguration { + + @Bean + public AsyncBeanUser user(AsyncBean bean) { + return new AsyncBeanUser(bean); + } + + } + + static class AsyncBean implements SomeInterface { + + @Async + public void foo() { + + } + + @Override + public void bar() { + + } + + } + + interface SomeInterface { + + void bar(); + + } + + static class AsyncBeanUser { + + AsyncBeanUser(AsyncBean asyncBean) { + } + + } + +}