Skip to content

Commit 5d591ed

Browse files
jkubrynskiDave Syer
authored andcommitted
Consider FactoryBean classes in OnBeanCondition
Update OnBeanCondition to attempt to consider FactoryBean classes for bean type matches. To ensure early instantiation does not occur, the object type from the FactoryBean is deduced by resolving generics on the declaration. Fixes gh-355
1 parent fa9a506 commit 5d591ed

File tree

2 files changed

+167
-35
lines changed

2 files changed

+167
-35
lines changed

spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,24 @@
2020
import java.lang.reflect.Method;
2121
import java.util.ArrayList;
2222
import java.util.Arrays;
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.LinkedHashSet;
2326
import java.util.List;
27+
import java.util.Set;
2428

2529
import org.springframework.beans.factory.BeanFactory;
2630
import org.springframework.beans.factory.BeanFactoryUtils;
31+
import org.springframework.beans.factory.FactoryBean;
32+
import org.springframework.beans.factory.HierarchicalBeanFactory;
33+
import org.springframework.beans.factory.ListableBeanFactory;
34+
import org.springframework.beans.factory.config.BeanDefinition;
2735
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
2836
import org.springframework.context.annotation.Bean;
2937
import org.springframework.context.annotation.Condition;
3038
import org.springframework.context.annotation.ConditionContext;
3139
import org.springframework.context.annotation.ConfigurationCondition;
40+
import org.springframework.core.ResolvableType;
3241
import org.springframework.core.type.AnnotatedTypeMetadata;
3342
import org.springframework.core.type.MethodMetadata;
3443
import org.springframework.util.Assert;
@@ -43,6 +52,7 @@
4352
*
4453
* @author Phillip Webb
4554
* @author Dave Syer
55+
* @author Jakub Kubrynski
4656
*/
4757
class OnBeanCondition extends SpringBootCondition implements ConfigurationCondition {
4858

@@ -100,8 +110,8 @@ private List<String> getMatchingBeans(ConditionContext context, BeanSearchSpec b
100110
boolean considerHierarchy = beans.getStrategy() == SearchStrategy.ALL;
101111

102112
for (String type : beans.getTypes()) {
103-
beanNames.addAll(Arrays.asList(getBeanNamesForType(beanFactory, type,
104-
context.getClassLoader(), considerHierarchy)));
113+
beanNames.addAll(getBeanNamesForType(beanFactory, type,
114+
context.getClassLoader(), considerHierarchy));
105115
}
106116

107117
for (String annotation : beans.getAnnotations()) {
@@ -126,25 +136,94 @@ private boolean containsBean(ConfigurableListableBeanFactory beanFactory,
126136
return beanFactory.containsLocalBean(beanName);
127137
}
128138

129-
private String[] getBeanNamesForType(ConfigurableListableBeanFactory beanFactory,
130-
String type, ClassLoader classLoader, boolean considerHierarchy)
131-
throws LinkageError {
132-
// eagerInit set to false to prevent early instantiation (some
133-
// factory beans will not be able to determine their object type at this
134-
// stage, so those are not eligible for matching this condition)
139+
private Collection<String> getBeanNamesForType(
140+
ConfigurableListableBeanFactory beanFactory, String type,
141+
ClassLoader classLoader, boolean considerHierarchy) throws LinkageError {
135142
try {
136-
Class<?> typeClass = ClassUtils.forName(type, classLoader);
137-
if (considerHierarchy) {
138-
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory,
139-
typeClass, false, false);
140-
}
141-
return beanFactory.getBeanNamesForType(typeClass, false, false);
143+
Set<String> result = new LinkedHashSet<String>();
144+
collectBeanNamesForType(result, beanFactory,
145+
ClassUtils.forName(type, classLoader), considerHierarchy);
146+
return result;
142147
}
143148
catch (ClassNotFoundException ex) {
144-
return NO_BEANS;
149+
return Collections.emptySet();
150+
}
151+
}
152+
153+
private void collectBeanNamesForType(Set<String> result,
154+
ListableBeanFactory beanFactory, Class<?> type, boolean considerHierarchy) {
155+
// eagerInit set to false to prevent early instantiation
156+
result.addAll(Arrays.asList(beanFactory.getBeanNamesForType(type, true, false)));
157+
if (beanFactory instanceof ConfigurableListableBeanFactory) {
158+
collectBeanNamesForTypeFromFactoryBeans(result,
159+
(ConfigurableListableBeanFactory) beanFactory, type);
160+
}
161+
if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory) {
162+
BeanFactory parent = ((HierarchicalBeanFactory) beanFactory)
163+
.getParentBeanFactory();
164+
if (parent instanceof ListableBeanFactory) {
165+
collectBeanNamesForType(result, (ListableBeanFactory) parent, type,
166+
considerHierarchy);
167+
}
168+
}
169+
}
170+
171+
/**
172+
* Attempt to collect bean names for type by considering FactoryBean generics. Some
173+
* factory beans will not be able to determine their object type at this stage, so
174+
* those are not eligible for matching this condition.
175+
*/
176+
private void collectBeanNamesForTypeFromFactoryBeans(Set<String> result,
177+
ConfigurableListableBeanFactory beanFactory, Class<?> type) {
178+
String[] names = beanFactory.getBeanNamesForType(FactoryBean.class, true, false);
179+
for (String name : names) {
180+
name = BeanFactoryUtils.transformedBeanName(name);
181+
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(name);
182+
Class<?> generic = getFactoryBeanGeneric(beanFactory, beanDefinition);
183+
if (generic != null && ClassUtils.isAssignable(type, generic)) {
184+
result.add(name);
185+
}
145186
}
146187
}
147188

189+
private Class<?> getFactoryBeanGeneric(ConfigurableListableBeanFactory beanFactory,
190+
BeanDefinition definition) {
191+
try {
192+
if (StringUtils.hasLength(definition.getFactoryBeanName())
193+
&& StringUtils.hasLength(definition.getFactoryMethodName())) {
194+
return getConfigurationClassFactoryBeanGeneric(beanFactory, definition);
195+
}
196+
if (StringUtils.hasLength(definition.getBeanClassName())) {
197+
return getDirectFactoryBeanGeneric(beanFactory, definition);
198+
}
199+
}
200+
catch (Exception ex) {
201+
}
202+
return null;
203+
}
204+
205+
private Class<?> getConfigurationClassFactoryBeanGeneric(
206+
ConfigurableListableBeanFactory beanFactory, BeanDefinition definition)
207+
throws Exception {
208+
BeanDefinition factoryDefinition = beanFactory.getBeanDefinition(definition
209+
.getFactoryBeanName());
210+
Class<?> factoryClass = ClassUtils.forName(factoryDefinition.getBeanClassName(),
211+
beanFactory.getBeanClassLoader());
212+
Method method = ReflectionUtils.findMethod(factoryClass,
213+
definition.getFactoryMethodName());
214+
return ResolvableType.forMethodReturnType(method).as(FactoryBean.class)
215+
.resolveGeneric();
216+
}
217+
218+
private Class<?> getDirectFactoryBeanGeneric(
219+
ConfigurableListableBeanFactory beanFactory, BeanDefinition definition)
220+
throws ClassNotFoundException, LinkageError {
221+
Class<?> factoryBeanClass = ClassUtils.forName(definition.getBeanClassName(),
222+
beanFactory.getBeanClassLoader());
223+
return ResolvableType.forClass(factoryBeanClass).as(FactoryBean.class)
224+
.resolveGeneric();
225+
}
226+
148227
private String[] getBeanNamesForAnnotation(
149228
ConfigurableListableBeanFactory beanFactory, String type,
150229
ClassLoader classLoader, boolean considerHierarchy) throws LinkageError {

spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.boot.autoconfigure.condition;
1818

19-
import org.junit.Ignore;
2019
import org.junit.Test;
2120
import org.springframework.beans.factory.FactoryBean;
2221
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
@@ -38,6 +37,7 @@
3837
*
3938
* @author Dave Syer
4039
* @author Phillip Webb
40+
* @author Jakub Kubrynski
4141
*/
4242
@SuppressWarnings("resource")
4343
public class ConditionalOnMissingBeanTests {
@@ -102,7 +102,7 @@ public void testAnnotationOnMissingBeanCondition() {
102102
@Test
103103
public void testAnnotationOnMissingBeanConditionWithEagerFactoryBean() {
104104
this.context.register(FooConfiguration.class, OnAnnotationConfiguration.class,
105-
ConfigurationWithFactoryBean.class,
105+
FactoryBeanXmlConfiguration.class,
106106
PropertyPlaceholderAutoConfiguration.class);
107107
this.context.refresh();
108108
assertFalse(this.context.containsBean("bar"));
@@ -111,22 +111,44 @@ public void testAnnotationOnMissingBeanConditionWithEagerFactoryBean() {
111111
}
112112

113113
@Test
114-
@Ignore("This will never work - you need to use XML for FactoryBeans, or else call getObject() inside the @Bean method")
115114
public void testOnMissingBeanConditionWithFactoryBean() {
116-
this.context.register(ExampleBeanAndFactoryBeanConfiguration.class,
115+
this.context.register(FactoryBeanConfiguration.class,
116+
ConditionalOnFactoryBean.class,
117117
PropertyPlaceholderAutoConfiguration.class);
118118
this.context.refresh();
119-
// There should be only one
120-
this.context.getBean(ExampleBean.class);
119+
assertThat(this.context.getBean(ExampleBean.class).toString(),
120+
equalTo("fromFactory"));
121+
}
122+
123+
@Test
124+
public void testOnMissingBeanConditionWithConcreteFactoryBean() {
125+
this.context.register(ConcreteFactoryBeanConfiguration.class,
126+
ConditionalOnFactoryBean.class,
127+
PropertyPlaceholderAutoConfiguration.class);
128+
this.context.refresh();
129+
assertThat(this.context.getBean(ExampleBean.class).toString(),
130+
equalTo("fromFactory"));
131+
}
132+
133+
@Test
134+
public void testOnMissingBeanConditionWithUnhelpfulFactoryBean() {
135+
this.context.register(UnhelpfulFactoryBeanConfiguration.class,
136+
ConditionalOnFactoryBean.class,
137+
PropertyPlaceholderAutoConfiguration.class);
138+
this.context.refresh();
139+
// We could not tell that the FactoryBean would ultimately create an ExampleBean
140+
assertThat(this.context.getBeansOfType(ExampleBean.class).values().size(),
141+
equalTo(2));
121142
}
122143

123144
@Test
124145
public void testOnMissingBeanConditionWithFactoryBeanInXml() {
125-
this.context.register(ConfigurationWithFactoryBean.class,
146+
this.context.register(FactoryBeanXmlConfiguration.class,
147+
ConditionalOnFactoryBean.class,
126148
PropertyPlaceholderAutoConfiguration.class);
127149
this.context.refresh();
128-
// There should be only one
129-
this.context.getBean(ExampleBean.class);
150+
assertThat(this.context.getBean(ExampleBean.class).toString(),
151+
equalTo("fromFactory"));
130152
}
131153

132154
@Configuration
@@ -139,17 +161,41 @@ public String bar() {
139161
}
140162

141163
@Configuration
142-
protected static class ExampleBeanAndFactoryBeanConfiguration {
143-
164+
protected static class FactoryBeanConfiguration {
144165
@Bean
145166
public FactoryBean<ExampleBean> exampleBeanFactoryBean() {
146167
return new ExampleFactoryBean("foo");
147168
}
169+
}
170+
171+
@Configuration
172+
protected static class ConcreteFactoryBeanConfiguration {
173+
@Bean
174+
public ExampleFactoryBean exampleBeanFactoryBean() {
175+
return new ExampleFactoryBean("foo");
176+
}
177+
}
178+
179+
@Configuration
180+
protected static class UnhelpfulFactoryBeanConfiguration {
181+
@Bean
182+
@SuppressWarnings("rawtypes")
183+
public FactoryBean exampleBeanFactoryBean() {
184+
return new ExampleFactoryBean("foo");
185+
}
186+
}
187+
188+
@Configuration
189+
@ImportResource("org/springframework/boot/autoconfigure/condition/factorybean.xml")
190+
protected static class FactoryBeanXmlConfiguration {
191+
}
148192

193+
@Configuration
194+
protected static class ConditionalOnFactoryBean {
149195
@Bean
150196
@ConditionalOnMissingBean(ExampleBean.class)
151197
public ExampleBean createExampleBean() {
152-
return new ExampleBean();
198+
return new ExampleBean("direct");
153199
}
154200
}
155201

@@ -162,11 +208,6 @@ public String bar() {
162208
}
163209
}
164210

165-
@Configuration
166-
@ImportResource("org/springframework/boot/autoconfigure/condition/factorybean.xml")
167-
protected static class ConfigurationWithFactoryBean {
168-
}
169-
170211
@Configuration
171212
@EnableScheduling
172213
protected static class FooConfiguration {
@@ -198,7 +239,7 @@ public String bar() {
198239
protected static class ExampleBeanConfiguration {
199240
@Bean
200241
public ExampleBean exampleBean() {
201-
return new ExampleBean();
242+
return new ExampleBean("test");
202243
}
203244
}
204245

@@ -208,12 +249,24 @@ protected static class ImpliedOnBeanMethod {
208249
@Bean
209250
@ConditionalOnMissingBean
210251
public ExampleBean exampleBean2() {
211-
return new ExampleBean();
252+
return new ExampleBean("test");
212253
}
213254

214255
}
215256

216257
public static class ExampleBean {
258+
259+
private String value;
260+
261+
public ExampleBean(String value) {
262+
this.value = value;
263+
}
264+
265+
@Override
266+
public String toString() {
267+
return this.value;
268+
}
269+
217270
}
218271

219272
public static class ExampleFactoryBean implements FactoryBean<ExampleBean> {
@@ -224,7 +277,7 @@ public ExampleFactoryBean(String value) {
224277

225278
@Override
226279
public ExampleBean getObject() throws Exception {
227-
return new ExampleBean();
280+
return new ExampleBean("fromFactory");
228281
}
229282

230283
@Override

0 commit comments

Comments
 (0)