Skip to content

Commit 9ede1d0

Browse files
committed
Revise multiple beans resolution for custom collection types
Closes gh-30022
1 parent c65b0a1 commit 9ede1d0

File tree

2 files changed

+152
-49
lines changed

2 files changed

+152
-49
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java

Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,12 +1357,15 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str
13571357

13581358
InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
13591359
try {
1360+
// Step 1: pre-resolved shortcut for single bean match, e.g. from @Autowired
13601361
Object shortcut = descriptor.resolveShortcut(this);
13611362
if (shortcut != null) {
13621363
return shortcut;
13631364
}
13641365

13651366
Class<?> type = descriptor.getDependencyType();
1367+
1368+
// Step 2: pre-defined value or expression, e.g. from @Value
13661369
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
13671370
if (value != null) {
13681371
if (value instanceof String strValue) {
@@ -1383,13 +1386,20 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str
13831386
}
13841387
}
13851388

1389+
// Step 3a: multiple beans as stream / array / standard collection / plain map
13861390
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
13871391
if (multipleBeans != null) {
13881392
return multipleBeans;
13891393
}
1390-
1394+
// Step 3b: direct bean matches, possibly direct beans of type Collection / Map
13911395
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
13921396
if (matchingBeans.isEmpty()) {
1397+
// Step 3c (fallback): custom Collection / Map declarations for collecting multiple beans
1398+
multipleBeans = resolveMultipleBeansFallback(descriptor, beanName, autowiredBeanNames, typeConverter);
1399+
if (multipleBeans != null) {
1400+
return multipleBeans;
1401+
}
1402+
// Raise exception if nothing found for required injection point
13931403
if (isRequired(descriptor)) {
13941404
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
13951405
}
@@ -1399,10 +1409,12 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str
13991409
String autowiredBeanName;
14001410
Object instanceCandidate;
14011411

1412+
// Step 4: determine single candidate
14021413
if (matchingBeans.size() > 1) {
14031414
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
14041415
if (autowiredBeanName == null) {
1405-
if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
1416+
if (isRequired(descriptor) || !indicatesArrayCollectionOrMap(type)) {
1417+
// Raise exception if no clear match found for required injection point
14061418
return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
14071419
}
14081420
else {
@@ -1421,6 +1433,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str
14211433
instanceCandidate = entry.getValue();
14221434
}
14231435

1436+
// Step 5: validate single result
14241437
if (autowiredBeanNames != null) {
14251438
autowiredBeanNames.add(autowiredBeanName);
14261439
}
@@ -1430,6 +1443,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str
14301443
Object result = instanceCandidate;
14311444
if (result instanceof NullBean) {
14321445
if (isRequired(descriptor)) {
1446+
// Raise exception if null encountered for required injection point
14331447
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
14341448
}
14351449
result = null;
@@ -1491,63 +1505,92 @@ else if (type.isArray()) {
14911505
}
14921506
return result;
14931507
}
1494-
else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
1495-
Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
1496-
if (elementType == null) {
1497-
return null;
1498-
}
1499-
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
1500-
new MultiElementDescriptor(descriptor));
1501-
if (matchingBeans.isEmpty()) {
1502-
return null;
1503-
}
1504-
if (autowiredBeanNames != null) {
1505-
autowiredBeanNames.addAll(matchingBeans.keySet());
1506-
}
1507-
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
1508-
Object result = converter.convertIfNecessary(matchingBeans.values(), type);
1509-
if (result instanceof List<?> list && list.size() > 1) {
1510-
Comparator<Object> comparator = adaptDependencyComparator(matchingBeans);
1511-
if (comparator != null) {
1512-
list.sort(comparator);
1513-
}
1514-
}
1515-
return result;
1508+
else if (Collection.class == type || Set.class == type || List.class == type) {
1509+
return resolveMultipleBeanCollection(descriptor, beanName, autowiredBeanNames, typeConverter);
15161510
}
15171511
else if (Map.class == type) {
1518-
ResolvableType mapType = descriptor.getResolvableType().asMap();
1519-
Class<?> keyType = mapType.resolveGeneric(0);
1520-
if (String.class != keyType) {
1521-
return null;
1522-
}
1523-
Class<?> valueType = mapType.resolveGeneric(1);
1524-
if (valueType == null) {
1525-
return null;
1526-
}
1527-
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, valueType,
1528-
new MultiElementDescriptor(descriptor));
1529-
if (matchingBeans.isEmpty()) {
1530-
return null;
1531-
}
1532-
if (autowiredBeanNames != null) {
1533-
autowiredBeanNames.addAll(matchingBeans.keySet());
1534-
}
1535-
return matchingBeans;
1512+
return resolveMultipleBeanMap(descriptor, beanName, autowiredBeanNames, typeConverter);
15361513
}
1537-
else {
1514+
return null;
1515+
}
1516+
1517+
1518+
@Nullable
1519+
private Object resolveMultipleBeansFallback(DependencyDescriptor descriptor, @Nullable String beanName,
1520+
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
1521+
1522+
Class<?> type = descriptor.getDependencyType();
1523+
1524+
if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
1525+
return resolveMultipleBeanCollection(descriptor, beanName, autowiredBeanNames, typeConverter);
1526+
}
1527+
else if (Map.class.isAssignableFrom(type) && type.isInterface()) {
1528+
return resolveMultipleBeanMap(descriptor, beanName, autowiredBeanNames, typeConverter);
1529+
}
1530+
return null;
1531+
}
1532+
1533+
@Nullable
1534+
private Object resolveMultipleBeanCollection(DependencyDescriptor descriptor, @Nullable String beanName,
1535+
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
1536+
1537+
Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
1538+
if (elementType == null) {
1539+
return null;
1540+
}
1541+
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
1542+
new MultiElementDescriptor(descriptor));
1543+
if (matchingBeans.isEmpty()) {
15381544
return null;
15391545
}
1546+
if (autowiredBeanNames != null) {
1547+
autowiredBeanNames.addAll(matchingBeans.keySet());
1548+
}
1549+
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
1550+
Object result = converter.convertIfNecessary(matchingBeans.values(), descriptor.getDependencyType());
1551+
if (result instanceof List<?> list && list.size() > 1) {
1552+
Comparator<Object> comparator = adaptDependencyComparator(matchingBeans);
1553+
if (comparator != null) {
1554+
list.sort(comparator);
1555+
}
1556+
}
1557+
return result;
15401558
}
15411559

1542-
private boolean isRequired(DependencyDescriptor descriptor) {
1543-
return getAutowireCandidateResolver().isRequired(descriptor);
1560+
@Nullable
1561+
private Object resolveMultipleBeanMap(DependencyDescriptor descriptor, @Nullable String beanName,
1562+
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
1563+
1564+
ResolvableType mapType = descriptor.getResolvableType().asMap();
1565+
Class<?> keyType = mapType.resolveGeneric(0);
1566+
if (String.class != keyType) {
1567+
return null;
1568+
}
1569+
Class<?> valueType = mapType.resolveGeneric(1);
1570+
if (valueType == null) {
1571+
return null;
1572+
}
1573+
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, valueType,
1574+
new MultiElementDescriptor(descriptor));
1575+
if (matchingBeans.isEmpty()) {
1576+
return null;
1577+
}
1578+
if (autowiredBeanNames != null) {
1579+
autowiredBeanNames.addAll(matchingBeans.keySet());
1580+
}
1581+
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
1582+
return converter.convertIfNecessary(matchingBeans, descriptor.getDependencyType());
15441583
}
15451584

1546-
private boolean indicatesMultipleBeans(Class<?> type) {
1585+
private boolean indicatesArrayCollectionOrMap(Class<?> type) {
15471586
return (type.isArray() || (type.isInterface() &&
15481587
(Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type))));
15491588
}
15501589

1590+
private boolean isRequired(DependencyDescriptor descriptor) {
1591+
return getAutowireCandidateResolver().isRequired(descriptor);
1592+
}
1593+
15511594
@Nullable
15521595
private Comparator<Object> adaptDependencyComparator(Map<String, ?> matchingBeans) {
15531596
Comparator<Object> comparator = getDependencyComparator();
@@ -1609,7 +1652,7 @@ protected Map<String, Object> findAutowireCandidates(
16091652
}
16101653
}
16111654
if (result.isEmpty()) {
1612-
boolean multiple = indicatesMultipleBeans(requiredType);
1655+
boolean multiple = indicatesArrayCollectionOrMap(requiredType);
16131656
// Consider fallback matches if the first pass failed to find anything...
16141657
DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
16151658
for (String candidate : candidateNames) {
@@ -1675,7 +1718,7 @@ protected String determineAutowireCandidate(Map<String, Object> candidates, Depe
16751718
if (priorityCandidate != null) {
16761719
return priorityCandidate;
16771720
}
1678-
// Fallback
1721+
// Fallback: pick directly registered dependency or qualified bean name match
16791722
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
16801723
String candidateName = entry.getKey();
16811724
Object beanInstance = entry.getValue();
@@ -2094,7 +2137,6 @@ public Object getIfUnique() throws BeansException {
20942137
public boolean isRequired() {
20952138
return false;
20962139
}
2097-
20982140
@Override
20992141
@Nullable
21002142
public Object resolveNotUnique(ResolvableType type, Map<String, Object> matchingBeans) {

spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import java.util.Map;
3535
import java.util.Properties;
3636
import java.util.Set;
37+
import java.util.SortedMap;
38+
import java.util.SortedSet;
3739
import java.util.concurrent.Callable;
3840
import java.util.function.Consumer;
3941
import java.util.function.Function;
@@ -1403,6 +1405,20 @@ void constructorInjectionWithPlainHashMapAsBean() {
14031405
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap"));
14041406
}
14051407

1408+
@Test
1409+
void constructorInjectionWithSortedMapFallback() {
1410+
RootBeanDefinition bd = new RootBeanDefinition(SortedMapConstructorInjectionBean.class);
1411+
bf.registerBeanDefinition("annotatedBean", bd);
1412+
TestBean tb1 = new TestBean();
1413+
TestBean tb2 = new TestBean();
1414+
bf.registerSingleton("testBean1", tb1);
1415+
bf.registerSingleton("testBean2", tb2);
1416+
1417+
SortedMapConstructorInjectionBean bean = bf.getBean("annotatedBean", SortedMapConstructorInjectionBean.class);
1418+
assertThat(bean.getTestBeanMap()).containsEntry("testBean1", tb1);
1419+
assertThat(bean.getTestBeanMap()).containsEntry("testBean2", tb2);
1420+
}
1421+
14061422
@Test
14071423
void constructorInjectionWithTypedSetAsBean() {
14081424
RootBeanDefinition bd = new RootBeanDefinition(SetConstructorInjectionBean.class);
@@ -1444,13 +1460,28 @@ void constructorInjectionWithCustomSetAsBean() {
14441460
RootBeanDefinition tbs = new RootBeanDefinition(CustomCollectionFactoryMethods.class);
14451461
tbs.setUniqueFactoryMethodName("testBeanSet");
14461462
bf.registerBeanDefinition("myTestBeanSet", tbs);
1463+
bf.registerSingleton("testBean1", new TestBean());
1464+
bf.registerSingleton("testBean2", new TestBean());
14471465

14481466
CustomSetConstructorInjectionBean bean = bf.getBean("annotatedBean", CustomSetConstructorInjectionBean.class);
14491467
assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet"));
14501468
bean = bf.getBean("annotatedBean", CustomSetConstructorInjectionBean.class);
14511469
assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet"));
14521470
}
14531471

1472+
@Test
1473+
void constructorInjectionWithSortedSetFallback() {
1474+
RootBeanDefinition bd = new RootBeanDefinition(SortedSetConstructorInjectionBean.class);
1475+
bf.registerBeanDefinition("annotatedBean", bd);
1476+
TestBean tb1 = new TestBean();
1477+
TestBean tb2 = new TestBean();
1478+
bf.registerSingleton("testBean1", tb1);
1479+
bf.registerSingleton("testBean2", tb2);
1480+
1481+
SortedSetConstructorInjectionBean bean = bf.getBean("annotatedBean", SortedSetConstructorInjectionBean.class);
1482+
assertThat(bean.getTestBeanSet()).contains(tb1, tb2);
1483+
}
1484+
14541485
@Test
14551486
void selfReference() {
14561487
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class));
@@ -3116,6 +3147,21 @@ public Map<String, TestBean> getTestBeanMap() {
31163147
}
31173148

31183149

3150+
public static class SortedMapConstructorInjectionBean {
3151+
3152+
private SortedMap<String, TestBean> testBeanMap;
3153+
3154+
@Autowired
3155+
public SortedMapConstructorInjectionBean(SortedMap<String, TestBean> testBeanMap) {
3156+
this.testBeanMap = testBeanMap;
3157+
}
3158+
3159+
public SortedMap<String, TestBean> getTestBeanMap() {
3160+
return this.testBeanMap;
3161+
}
3162+
}
3163+
3164+
31193165
public static class QualifiedMapConstructorInjectionBean {
31203166

31213167
private Map<String, TestBean> testBeanMap;
@@ -3146,6 +3192,21 @@ public Set<TestBean> getTestBeanSet() {
31463192
}
31473193

31483194

3195+
public static class SortedSetConstructorInjectionBean {
3196+
3197+
private SortedSet<TestBean> testBeanSet;
3198+
3199+
@Autowired
3200+
public SortedSetConstructorInjectionBean(SortedSet<TestBean> testBeanSet) {
3201+
this.testBeanSet = testBeanSet;
3202+
}
3203+
3204+
public SortedSet<TestBean> getTestBeanSet() {
3205+
return this.testBeanSet;
3206+
}
3207+
}
3208+
3209+
31493210
public static class SelfInjectionBean {
31503211

31513212
@Autowired

0 commit comments

Comments
 (0)