Skip to content

Commit 1603c4a

Browse files
committed
Programmatic ObjectProvider retrieval through BeanFactory API
Introduces getBeanProvider(Class) and getBeanProvider(ResolvableType), also narrowing getBean(String, Class) and isTypeMatch(String, Class) to a non-null Class argument and enriching NoUniqueBeanDefinitionException with a full ResolvableType. In addition, ObjectProvider supports iterable/stream access for collection-style resolution of multiple matching beans now, and collection injection falls back to an empty collection in a single-constructor case with non-null arguments. Issue: SPR-17075 Issue: SPR-11419 Issue: SPR-15338
1 parent 82194f4 commit 1603c4a

File tree

16 files changed

+918
-100
lines changed

16 files changed

+918
-100
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -148,16 +148,13 @@ public interface BeanFactory {
148148
* <p>Translates aliases back to the corresponding canonical bean name.
149149
* Will ask the parent factory if the bean cannot be found in this factory instance.
150150
* @param name the name of the bean to retrieve
151-
* @param requiredType type the bean must match. Can be an interface or superclass
152-
* of the actual class, or {@code null} for any match. For example, if the value
153-
* is {@code Object.class}, this method will succeed whatever the class of the
154-
* returned instance.
151+
* @param requiredType type the bean must match; can be an interface or superclass
155152
* @return an instance of the bean
156153
* @throws NoSuchBeanDefinitionException if there is no such bean definition
157154
* @throws BeanNotOfRequiredTypeException if the bean is not of the required type
158155
* @throws BeansException if the bean could not be created
159156
*/
160-
<T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException;
157+
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
161158

162159
/**
163160
* Return an instance, which may be shared or independent, of the specified bean.
@@ -181,8 +178,7 @@ public interface BeanFactory {
181178
* but may also be translated into a conventional by-name lookup based on the name
182179
* of the given type. For more extensive retrieval operations across sets of beans,
183180
* use {@link ListableBeanFactory} and/or {@link BeanFactoryUtils}.
184-
* @param requiredType type the bean must match; can be an interface or superclass.
185-
* {@code null} is disallowed.
181+
* @param requiredType type the bean must match; can be an interface or superclass
186182
* @return an instance of the single bean matching the required type
187183
* @throws NoSuchBeanDefinitionException if no bean of the given type was found
188184
* @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found
@@ -200,8 +196,7 @@ public interface BeanFactory {
200196
* but may also be translated into a conventional by-name lookup based on the name
201197
* of the given type. For more extensive retrieval operations across sets of beans,
202198
* use {@link ListableBeanFactory} and/or {@link BeanFactoryUtils}.
203-
* @param requiredType type the bean must match; can be an interface or superclass.
204-
* {@code null} is disallowed.
199+
* @param requiredType type the bean must match; can be an interface or superclass
205200
* @param args arguments to use when creating a bean instance using explicit arguments
206201
* (only applied when creating a new instance as opposed to retrieving an existing one)
207202
* @return an instance of the bean
@@ -213,6 +208,23 @@ public interface BeanFactory {
213208
*/
214209
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
215210

211+
/**
212+
* Return an provider for the specified bean, allowing for lazy on-demand retrieval
213+
* of instances, including availability and uniqueness options.
214+
* @param requiredType type the bean must match; can be an interface or superclass
215+
* @return a corresponding provider handle
216+
* @since 5.1
217+
*/
218+
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
219+
220+
/**
221+
* Return an provider for the specified bean, allowing for lazy on-demand retrieval
222+
* of instances, including availability and uniqueness options.
223+
* @param requiredType type the bean must match; can be a generic type declaration
224+
* @return a corresponding provider handle
225+
* @since 5.1
226+
*/
227+
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
216228

217229
/**
218230
* Does this bean factory contain a bean definition or externally registered singleton
@@ -298,7 +310,7 @@ public interface BeanFactory {
298310
* @see #getBean
299311
* @see #getType
300312
*/
301-
boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
313+
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
302314

303315
/**
304316
* Determine the type of the bean with the given name. More specifically,

spring-beans/src/main/java/org/springframework/beans/factory/NoUniqueBeanDefinitionException.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Arrays;
2020
import java.util.Collection;
2121

22+
import org.springframework.core.ResolvableType;
2223
import org.springframework.lang.Nullable;
2324
import org.springframework.util.StringUtils;
2425

@@ -72,6 +73,29 @@ public NoUniqueBeanDefinitionException(Class<?> type, String... beanNamesFound)
7273
this(type, Arrays.asList(beanNamesFound));
7374
}
7475

76+
/**
77+
* Create a new {@code NoUniqueBeanDefinitionException}.
78+
* @param type required type of the non-unique bean
79+
* @param beanNamesFound the names of all matching beans (as a Collection)
80+
* @since 5.1
81+
*/
82+
public NoUniqueBeanDefinitionException(ResolvableType type, Collection<String> beanNamesFound) {
83+
super(type, "expected single matching bean but found " + beanNamesFound.size() + ": " +
84+
StringUtils.collectionToCommaDelimitedString(beanNamesFound));
85+
this.numberOfBeansFound = beanNamesFound.size();
86+
this.beanNamesFound = beanNamesFound;
87+
}
88+
89+
/**
90+
* Create a new {@code NoUniqueBeanDefinitionException}.
91+
* @param type required type of the non-unique bean
92+
* @param beanNamesFound the names of all matching beans (as an array)
93+
* @since 5.1
94+
*/
95+
public NoUniqueBeanDefinitionException(ResolvableType type, String... beanNamesFound) {
96+
this(type, Arrays.asList(beanNamesFound));
97+
}
98+
7599

76100
/**
77101
* Return the number of beans found when only one matching bean was expected.

spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616

1717
package org.springframework.beans.factory;
1818

19+
import java.util.Iterator;
1920
import java.util.function.Consumer;
2021
import java.util.function.Supplier;
22+
import java.util.stream.Stream;
2123

2224
import org.springframework.beans.BeansException;
2325
import org.springframework.lang.Nullable;
@@ -26,11 +28,15 @@
2628
* A variant of {@link ObjectFactory} designed specifically for injection points,
2729
* allowing for programmatic optionality and lenient not-unique handling.
2830
*
31+
* <p>As of 5.1, this interface extends {@link Iterable} and provides {@link Stream}
32+
* support. It can be therefore be used in {@code for} loops, provides {@link #forEach}
33+
* iteration and allows for collection-style {@link #stream} access.
34+
*
2935
* @author Juergen Hoeller
3036
* @since 4.3
3137
* @param <T> the object type
3238
*/
33-
public interface ObjectProvider<T> extends ObjectFactory<T> {
39+
public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
3440

3541
/**
3642
* Return an instance (possibly shared or independent) of the object
@@ -130,4 +136,27 @@ default void ifUnique(Consumer<T> dependencyConsumer) throws BeansException {
130136
}
131137
}
132138

139+
/**
140+
* Return an {@link Iterator} over resolved object instances.
141+
* <p>The default implementation delegates to {@link #stream()}.
142+
* @since 5.1
143+
* @see #stream()
144+
*/
145+
@Override
146+
default Iterator<T> iterator() {
147+
return stream().iterator();
148+
}
149+
150+
/**
151+
* Return a sequential {@link Stream} over resolved object instances.
152+
* <p>The default implementation returns a stream of one element or an
153+
* empty stream if not available, resolved via {@link #getIfAvailable()}.
154+
* @since 5.1
155+
* @see #iterator()
156+
*/
157+
default Stream<T> stream() {
158+
T instance = getIfAvailable();
159+
return (instance != null ? Stream.of(instance) : Stream.empty());
160+
}
161+
133162
}

spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,41 @@ public boolean isEager() {
199199
return this.eager;
200200
}
201201

202+
/**
203+
* Return whether this descriptor allows for stream-style access to
204+
* result instances.
205+
* <p>By default, dependencies are strictly resolved to the declaration of
206+
* the injection point and therefore only resolve multiple entries if the
207+
* injection point is declared as an array, collection or map. This is
208+
* indicated by returning {@code false} here.
209+
* <p>Overriding this method to return {@code true} indicates that the
210+
* injection point declares the bean type but the resolution is meant to
211+
* end up in a {@link java.util.stream.Stream} for the declared bean type,
212+
* with the caller handling the multi-instance case for the injection point.
213+
* @since 5.1
214+
*/
215+
public boolean isStreamAccess() {
216+
return false;
217+
}
218+
219+
/**
220+
* Resolve the specified not-unique scenario: by default,
221+
* throwing a {@link NoUniqueBeanDefinitionException}.
222+
* <p>Subclasses may override this to select one of the instances or
223+
* to opt out with no result at all through returning {@code null}.
224+
* @param type the requested bean type
225+
* @param matchingBeans a map of bean names and corresponding bean
226+
* instances which have been pre-selected for the given type
227+
* (qualifiers etc already applied)
228+
* @return a bean instance to proceed with, or {@code null} for none
229+
* @throws BeansException in case of the not-unique scenario being fatal
230+
* @since 5.1
231+
*/
232+
@Nullable
233+
public Object resolveNotUnique(ResolvableType type, Map<String, Object> matchingBeans) throws BeansException {
234+
throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet());
235+
}
236+
202237
/**
203238
* Resolve the specified not-unique scenario: by default,
204239
* throwing a {@link NoUniqueBeanDefinitionException}.
@@ -211,7 +246,9 @@ public boolean isEager() {
211246
* @return a bean instance to proceed with, or {@code null} for none
212247
* @throws BeansException in case of the not-unique scenario being fatal
213248
* @since 4.3
249+
* @deprecated as of 5.1, in favor of {@link #resolveNotUnique(ResolvableType, Map)}
214250
*/
251+
@Deprecated
215252
@Nullable
216253
public Object resolveNotUnique(Class<?> type, Map<String, Object> matchingBeans) throws BeansException {
217254
throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet());

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public Object getBean(String name) throws BeansException {
200200
}
201201

202202
@Override
203-
public <T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException {
203+
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
204204
return doGetBean(name, requiredType, null, false);
205205
}
206206

@@ -277,10 +277,13 @@ else if (args != null) {
277277
// Delegation to parent with explicit args.
278278
return (T) parentBeanFactory.getBean(nameToLookup, args);
279279
}
280-
else {
280+
else if (requiredType != null) {
281281
// No args -> delegate to standard getBean method.
282282
return parentBeanFactory.getBean(nameToLookup, requiredType);
283283
}
284+
else {
285+
return (T) parentBeanFactory.getBean(nameToLookup);
286+
}
284287
}
285288

286289
if (!typeCheckOnly) {
@@ -586,7 +589,7 @@ else if (BeanFactoryUtils.isFactoryDereference(name)) {
586589
}
587590

588591
@Override
589-
public boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException {
592+
public boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException {
590593
return isTypeMatch(name, ResolvableType.forRawClass(typeToMatch));
591594
}
592595

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@
4343
import org.springframework.beans.factory.BeanCreationException;
4444
import org.springframework.beans.factory.BeanDefinitionStoreException;
4545
import org.springframework.beans.factory.InjectionPoint;
46+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
4647
import org.springframework.beans.factory.UnsatisfiedDependencyException;
4748
import org.springframework.beans.factory.config.ConstructorArgumentValues;
4849
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
4950
import org.springframework.beans.factory.config.DependencyDescriptor;
51+
import org.springframework.core.CollectionFactory;
5052
import org.springframework.core.GenericTypeResolver;
5153
import org.springframework.core.MethodParameter;
5254
import org.springframework.core.NamedThreadLocal;
@@ -133,7 +135,7 @@ public BeanWrapper autowireConstructor(final String beanName, final RootBeanDefi
133135
}
134136
}
135137
if (argsToResolve != null) {
136-
argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve);
138+
argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true);
137139
}
138140
}
139141

@@ -195,7 +197,7 @@ public BeanWrapper autowireConstructor(final String beanName, final RootBeanDefi
195197
}
196198
}
197199
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
198-
getUserDeclaredConstructor(candidate), autowiring);
200+
getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
199201
}
200202
catch (UnsatisfiedDependencyException ex) {
201203
if (logger.isTraceEnabled()) {
@@ -407,7 +409,7 @@ public BeanWrapper instantiateUsingFactoryMethod(
407409
}
408410
}
409411
if (argsToResolve != null) {
410-
argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve);
412+
argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve, true);
411413
}
412414
}
413415

@@ -471,8 +473,8 @@ public BeanWrapper instantiateUsingFactoryMethod(
471473
if (pnd != null) {
472474
paramNames = pnd.getParameterNames(candidate);
473475
}
474-
argsHolder = createArgumentArray(
475-
beanName, mbd, resolvedValues, bw, paramTypes, paramNames, candidate, autowiring);
476+
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw,
477+
paramTypes, paramNames, candidate, autowiring, candidates.length == 1);
476478
}
477479
catch (UnsatisfiedDependencyException ex) {
478480
if (logger.isTraceEnabled()) {
@@ -654,7 +656,7 @@ private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd,
654656
private ArgumentsHolder createArgumentArray(
655657
String beanName, RootBeanDefinition mbd, @Nullable ConstructorArgumentValues resolvedValues,
656658
BeanWrapper bw, Class<?>[] paramTypes, @Nullable String[] paramNames, Executable executable,
657-
boolean autowiring) throws UnsatisfiedDependencyException {
659+
boolean autowiring, boolean fallback) throws UnsatisfiedDependencyException {
658660

659661
TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
660662
TypeConverter converter = (customConverter != null ? customConverter : bw);
@@ -720,8 +722,8 @@ private ArgumentsHolder createArgumentArray(
720722
"] - did you specify the correct bean references as arguments?");
721723
}
722724
try {
723-
Object autowiredArgument =
724-
resolveAutowiredArgument(methodParam, beanName, autowiredBeanNames, converter);
725+
Object autowiredArgument = resolveAutowiredArgument(
726+
methodParam, beanName, autowiredBeanNames, converter, fallback);
725727
args.rawArguments[paramIndex] = autowiredArgument;
726728
args.arguments[paramIndex] = autowiredArgument;
727729
args.preparedArguments[paramIndex] = new AutowiredArgumentMarker();
@@ -749,8 +751,8 @@ private ArgumentsHolder createArgumentArray(
749751
/**
750752
* Resolve the prepared arguments stored in the given bean definition.
751753
*/
752-
private Object[] resolvePreparedArguments(
753-
String beanName, RootBeanDefinition mbd, BeanWrapper bw, Executable executable, Object[] argsToResolve) {
754+
private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw,
755+
Executable executable, Object[] argsToResolve, boolean fallback) {
754756

755757
TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
756758
TypeConverter converter = (customConverter != null ? customConverter : bw);
@@ -764,7 +766,7 @@ private Object[] resolvePreparedArguments(
764766
MethodParameter methodParam = MethodParameter.forExecutable(executable, argIndex);
765767
GenericTypeResolver.resolveParameterType(methodParam, executable.getDeclaringClass());
766768
if (argValue instanceof AutowiredArgumentMarker) {
767-
argValue = resolveAutowiredArgument(methodParam, beanName, null, converter);
769+
argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, fallback);
768770
}
769771
else if (argValue instanceof BeanMetadataElement) {
770772
argValue = valueResolver.resolveValueIfNecessary("constructor argument", argValue);
@@ -806,17 +808,33 @@ protected Constructor<?> getUserDeclaredConstructor(Constructor<?> constructor)
806808
*/
807809
@Nullable
808810
protected Object resolveAutowiredArgument(MethodParameter param, String beanName,
809-
@Nullable Set<String> autowiredBeanNames, TypeConverter typeConverter) {
811+
@Nullable Set<String> autowiredBeanNames, TypeConverter typeConverter, boolean fallback) {
810812

811-
if (InjectionPoint.class.isAssignableFrom(param.getParameterType())) {
813+
Class<?> paramType = param.getParameterType();
814+
if (InjectionPoint.class.isAssignableFrom(paramType)) {
812815
InjectionPoint injectionPoint = currentInjectionPoint.get();
813816
if (injectionPoint == null) {
814817
throw new IllegalStateException("No current InjectionPoint available for " + param);
815818
}
816819
return injectionPoint;
817820
}
818-
return this.beanFactory.resolveDependency(
819-
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
821+
try {
822+
return this.beanFactory.resolveDependency(
823+
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
824+
}
825+
catch (NoSuchBeanDefinitionException ex) {
826+
if (fallback) {
827+
// Single constructor or factory method -> let's return an
828+
// empty collection for a non-null collection parameter.
829+
if (CollectionFactory.isApproximableCollectionType(paramType)) {
830+
return CollectionFactory.createCollection(paramType, 0);
831+
}
832+
else if (CollectionFactory.isApproximableMapType(paramType)) {
833+
return CollectionFactory.createMap(paramType, 0);
834+
}
835+
}
836+
throw ex;
837+
}
820838
}
821839

822840
static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectionPoint) {

0 commit comments

Comments
 (0)