Skip to content

Commit

Permalink
Revise multiple beans resolution for custom collection types
Browse files Browse the repository at this point in the history
Closes gh-30022
  • Loading branch information
jhoeller committed Aug 12, 2023
1 parent c65b0a1 commit 9ede1d0
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1357,12 +1357,15 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str

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

Class<?> type = descriptor.getDependencyType();

// Step 2: pre-defined value or expression, e.g. from @Value
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
if (value instanceof String strValue) {
Expand All @@ -1383,13 +1386,20 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str
}
}

// Step 3a: multiple beans as stream / array / standard collection / plain map
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
return multipleBeans;
}

// Step 3b: direct bean matches, possibly direct beans of type Collection / Map
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
// Step 3c (fallback): custom Collection / Map declarations for collecting multiple beans
multipleBeans = resolveMultipleBeansFallback(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
return multipleBeans;
}
// Raise exception if nothing found for required injection point
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
Expand All @@ -1399,10 +1409,12 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str
String autowiredBeanName;
Object instanceCandidate;

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

// Step 5: validate single result
if (autowiredBeanNames != null) {
autowiredBeanNames.add(autowiredBeanName);
}
Expand All @@ -1430,6 +1443,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str
Object result = instanceCandidate;
if (result instanceof NullBean) {
if (isRequired(descriptor)) {
// Raise exception if null encountered for required injection point
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
result = null;
Expand Down Expand Up @@ -1491,63 +1505,92 @@ else if (type.isArray()) {
}
return result;
}
else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
if (elementType == null) {
return null;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
new MultiElementDescriptor(descriptor));
if (matchingBeans.isEmpty()) {
return null;
}
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
Object result = converter.convertIfNecessary(matchingBeans.values(), type);
if (result instanceof List<?> list && list.size() > 1) {
Comparator<Object> comparator = adaptDependencyComparator(matchingBeans);
if (comparator != null) {
list.sort(comparator);
}
}
return result;
else if (Collection.class == type || Set.class == type || List.class == type) {
return resolveMultipleBeanCollection(descriptor, beanName, autowiredBeanNames, typeConverter);
}
else if (Map.class == type) {
ResolvableType mapType = descriptor.getResolvableType().asMap();
Class<?> keyType = mapType.resolveGeneric(0);
if (String.class != keyType) {
return null;
}
Class<?> valueType = mapType.resolveGeneric(1);
if (valueType == null) {
return null;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, valueType,
new MultiElementDescriptor(descriptor));
if (matchingBeans.isEmpty()) {
return null;
}
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
return matchingBeans;
return resolveMultipleBeanMap(descriptor, beanName, autowiredBeanNames, typeConverter);
}
else {
return null;
}


@Nullable
private Object resolveMultipleBeansFallback(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {

Class<?> type = descriptor.getDependencyType();

if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
return resolveMultipleBeanCollection(descriptor, beanName, autowiredBeanNames, typeConverter);
}
else if (Map.class.isAssignableFrom(type) && type.isInterface()) {
return resolveMultipleBeanMap(descriptor, beanName, autowiredBeanNames, typeConverter);
}
return null;
}

@Nullable
private Object resolveMultipleBeanCollection(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {

Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
if (elementType == null) {
return null;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
new MultiElementDescriptor(descriptor));
if (matchingBeans.isEmpty()) {
return null;
}
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
Object result = converter.convertIfNecessary(matchingBeans.values(), descriptor.getDependencyType());
if (result instanceof List<?> list && list.size() > 1) {
Comparator<Object> comparator = adaptDependencyComparator(matchingBeans);
if (comparator != null) {
list.sort(comparator);
}
}
return result;
}

private boolean isRequired(DependencyDescriptor descriptor) {
return getAutowireCandidateResolver().isRequired(descriptor);
@Nullable
private Object resolveMultipleBeanMap(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {

ResolvableType mapType = descriptor.getResolvableType().asMap();
Class<?> keyType = mapType.resolveGeneric(0);
if (String.class != keyType) {
return null;
}
Class<?> valueType = mapType.resolveGeneric(1);
if (valueType == null) {
return null;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, valueType,
new MultiElementDescriptor(descriptor));
if (matchingBeans.isEmpty()) {
return null;
}
if (autowiredBeanNames != null) {
autowiredBeanNames.addAll(matchingBeans.keySet());
}
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
return converter.convertIfNecessary(matchingBeans, descriptor.getDependencyType());
}

private boolean indicatesMultipleBeans(Class<?> type) {
private boolean indicatesArrayCollectionOrMap(Class<?> type) {
return (type.isArray() || (type.isInterface() &&
(Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type))));
}

private boolean isRequired(DependencyDescriptor descriptor) {
return getAutowireCandidateResolver().isRequired(descriptor);
}

@Nullable
private Comparator<Object> adaptDependencyComparator(Map<String, ?> matchingBeans) {
Comparator<Object> comparator = getDependencyComparator();
Expand Down Expand Up @@ -1609,7 +1652,7 @@ protected Map<String, Object> findAutowireCandidates(
}
}
if (result.isEmpty()) {
boolean multiple = indicatesMultipleBeans(requiredType);
boolean multiple = indicatesArrayCollectionOrMap(requiredType);
// Consider fallback matches if the first pass failed to find anything...
DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
for (String candidate : candidateNames) {
Expand Down Expand Up @@ -1675,7 +1718,7 @@ protected String determineAutowireCandidate(Map<String, Object> candidates, Depe
if (priorityCandidate != null) {
return priorityCandidate;
}
// Fallback
// Fallback: pick directly registered dependency or qualified bean name match
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
String candidateName = entry.getKey();
Object beanInstance = entry.getValue();
Expand Down Expand Up @@ -2094,7 +2137,6 @@ public Object getIfUnique() throws BeansException {
public boolean isRequired() {
return false;
}

@Override
@Nullable
public Object resolveNotUnique(ResolvableType type, Map<String, Object> matchingBeans) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -1403,6 +1405,20 @@ void constructorInjectionWithPlainHashMapAsBean() {
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap"));
}

@Test
void constructorInjectionWithSortedMapFallback() {
RootBeanDefinition bd = new RootBeanDefinition(SortedMapConstructorInjectionBean.class);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb1 = new TestBean();
TestBean tb2 = new TestBean();
bf.registerSingleton("testBean1", tb1);
bf.registerSingleton("testBean2", tb2);

SortedMapConstructorInjectionBean bean = bf.getBean("annotatedBean", SortedMapConstructorInjectionBean.class);
assertThat(bean.getTestBeanMap()).containsEntry("testBean1", tb1);
assertThat(bean.getTestBeanMap()).containsEntry("testBean2", tb2);
}

@Test
void constructorInjectionWithTypedSetAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(SetConstructorInjectionBean.class);
Expand Down Expand Up @@ -1444,13 +1460,28 @@ void constructorInjectionWithCustomSetAsBean() {
RootBeanDefinition tbs = new RootBeanDefinition(CustomCollectionFactoryMethods.class);
tbs.setUniqueFactoryMethodName("testBeanSet");
bf.registerBeanDefinition("myTestBeanSet", tbs);
bf.registerSingleton("testBean1", new TestBean());
bf.registerSingleton("testBean2", new TestBean());

CustomSetConstructorInjectionBean bean = bf.getBean("annotatedBean", CustomSetConstructorInjectionBean.class);
assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet"));
bean = bf.getBean("annotatedBean", CustomSetConstructorInjectionBean.class);
assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet"));
}

@Test
void constructorInjectionWithSortedSetFallback() {
RootBeanDefinition bd = new RootBeanDefinition(SortedSetConstructorInjectionBean.class);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb1 = new TestBean();
TestBean tb2 = new TestBean();
bf.registerSingleton("testBean1", tb1);
bf.registerSingleton("testBean2", tb2);

SortedSetConstructorInjectionBean bean = bf.getBean("annotatedBean", SortedSetConstructorInjectionBean.class);
assertThat(bean.getTestBeanSet()).contains(tb1, tb2);
}

@Test
void selfReference() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class));
Expand Down Expand Up @@ -3116,6 +3147,21 @@ public Map<String, TestBean> getTestBeanMap() {
}


public static class SortedMapConstructorInjectionBean {

private SortedMap<String, TestBean> testBeanMap;

@Autowired
public SortedMapConstructorInjectionBean(SortedMap<String, TestBean> testBeanMap) {
this.testBeanMap = testBeanMap;
}

public SortedMap<String, TestBean> getTestBeanMap() {
return this.testBeanMap;
}
}


public static class QualifiedMapConstructorInjectionBean {

private Map<String, TestBean> testBeanMap;
Expand Down Expand Up @@ -3146,6 +3192,21 @@ public Set<TestBean> getTestBeanSet() {
}


public static class SortedSetConstructorInjectionBean {

private SortedSet<TestBean> testBeanSet;

@Autowired
public SortedSetConstructorInjectionBean(SortedSet<TestBean> testBeanSet) {
this.testBeanSet = testBeanSet;
}

public SortedSet<TestBean> getTestBeanSet() {
return this.testBeanSet;
}
}


public static class SelfInjectionBean {

@Autowired
Expand Down

0 comments on commit 9ede1d0

Please sign in to comment.