Introduction
Spring is the most popular dependency injection framework in the JVM world. Thousands or even millions of
applications use Spring or Spring Boot all over the globe. Quick GitHub search shows
that import org.springframework.beans.factory.annotation.Autowired
is found in more than 9M files. Almost all those
usages are just single bean injections. However, Spring supports more complicated injections. If some of your bean types have multiple implementations, Spring can provide you with all beans of a particular type from the ApplicationContext
. For example collection of beans can be filled to
java.util.Set
, array[]
, java.util.List
and even java.util.Map
. Let’s take a closer look at how you can use and more important benefit from this feature.
Application Description
To illustrate these examples, we will implement the “Arrays Calculator” - application, which provides various array operations. For example, we need to find min, max, avg, and the sum of values for a given array. To do so, let’s define an interface:
package com.patotski.example.array.operations;
public interface ArrayOperation {
Double calculate(List<Double> list);
}
Let’s create four implementations of the given interface to implement required operations: min, avg, max, sum. Here is the implementations for the classes ArrayMin
, ArrayAvg
, ArrayMax
, ArraySum
:
package com.patotski.example.array.operations;
@Component
public class ArrayMin implements ArrayOperation {
@Override
public Double calculate(List<Double> list) {
return list.stream().mapToDouble(Double::doubleValue).min().orElse(0.0);
}
}
package com.patotski.example.array.operations;
@Component
public class ArrayAvg implements ArrayOperation {
@Override
public Double calculate(List<Double> list) {
return list.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
}
}
package com.patotski.example.array.operations;
@Component
public class ArrayMax implements ArrayOperation {
@Override
public Double calculate(List<Double> list) {
return list.stream().mapToDouble(Double::doubleValue).max().orElse(0.0);
}
}
package com.patotski.example.array.operations;
@Component
public class ArraySum implements ArrayOperation {
@Override
public Double calculate(List<Double> list) {
return list.stream().mapToDouble(Double::doubleValue).sum();
}
}
In all our experiments, we will use the following List of doubles with the following characteristics (can be found in BaseTest in the test folder):
/**
* min = 1.0
* avg = 3.0
* max = 5.0
* sum = 15.0
**/
List<Double> testList = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
Now all preconditions are filled, and we can start our experiments with Spring injecting bean collections.
Inject Set of beans
Here is InjectSetService
bean where spring autowires java.util.Set
with all present implementations of ArrayOperation
interface:
package com.patotski.example.collection;
@Service
public class InjectSetService {
@Autowired
Set<ArrayOperation> arrayOperations;
public String calculate(List<Double> array) {
return arrayOperations.stream().map(op -> op.calculate(array))
.map(d -> d.toString())
.collect(Collectors.joining(", ", "[", "]"));
}
}
This service does nothing else but streams over the ArrayOperation
set and applies all present implementations to the incoming List of double values. calculate
method calculates all values and returns them concatenated. I also implemented a unit test that outputs the result:
Output: [3.0, 5.0, 1.0, 15.0]
The output contains calculated values but the odd order. Moreover, this order is not deterministic. Let’s try to understand why. On the one hand, the current version of Spring creates an instance of java.util.LinkedHashSet
as Set
implementation, and this collection maintains the insertion order of elements. On the other hand, the insertion order is out of our control. The reason for that is because Spring inserts beans when the component scanner found them on the classpath and after Bean Factory creates them. This behavior may change in future versions of Spring or even may change in JVM versions. As a result, you can’t rely on it in any of your business logic.
Spring injecting List or [] of beans
Both List<ArrayOperation>
and ArrayOperation[] are really similar. In the test project, I implemented both of them. However, I haven’t found any real difference in behavior. Here is the implementation of the service which has List injected:
package com.patotski.example.collection;
@Service
public class InjectListService {
@Autowired
List<ArrayOperation> arrayOperations;
public String calculate(List<Double> array) {
System.out.println(arrayOperations.getClass());
return arrayOperations.stream().map(op -> op.calculate(array))
.map(d -> d.toString())
.collect(Collectors.joining(", ", "[", "]"));
}
}
The idea is the same: iterate all implementations and return a concatenated string of results. The output is similar to what we have seen above:
Output: [3.0, 5.0, 1.0, 15.0]
But with List and array[] injection, it is possible to control the order in which beans will appear in them. Spring documentation mentioned three different ways: implement the org.springframework.core.Ordered interface use the @org.springframework.core.annotation.Order annotation use @javax.annotation.Priority annotation (can’t be used with @Bean, because according to its target can’t be applied to the method)
All approaches are similar, and for the sake of simplicity, I’m using less verbose @Order
annotation:
package com.patotski.example.array.operations;
@Order(0)
@Component
public class ArrayMin implements ArrayOperation {
...
}
package com.patotski.example.array.operations;
@Order(1)
@Component
public class ArrayAvg implements ArrayOperation {
...
}
package com.patotski.example.array.operations;
@Order(2)
@Component
public class ArrayMax implements ArrayOperation {
...
}
package com.patotski.example.array.operations;
@Order(3)
@Component
public class ArraySum implements ArrayOperation {
...
}
Now, if we run our tests once again, outputs for both array[] and List will respect defined beans order:
Output: [1.0, 3.0, 5.0, 15.0]
Spring injecting Map of beans
Spring can also inject beans into the typed Map as long as the map key type is String. The map values contain all beans of the expected type, and the keys contain the corresponding bean names. In the following implementation, together with the calculated value, we return the map key:
package com.patotski.example.collection;
@Service
public class InjectMapService {
@Autowired
Map<String, ArrayOperation> arrayOperations;
public String calculate(List<Double> array) {
System.out.println(arrayOperations.getClass());
return arrayOperations.entrySet().stream()
.map(op -> op.getKey() + " : " + op.getValue().calculate(array))
.collect(Collectors.joining(", ", "[", "]"));
}
}
Running this code provides the following output:
Output: [arrayAvg : 3.0, arrayMax : 5.0, arrayMin : 1.0, arraySum : 15.0]
As stated above, all keys are bean names, and Spring, by default, uses AnnotationBeanNameGenerator
which generates a name from the camel-cased class name. However, it’s possible to specify bean names in the code by just providing a string argument to @Component annotation:
@Component("min")
public class ArrayMin implements ArrayOperation {
...
}
@Component("avg")
public class ArrayAvg implements ArrayOperation {
...
}
@Component("max")
public class ArrayMax implements ArrayOperation {
...
}
@Component("sum")
public class ArraySum implements ArrayOperation {
...
}
After applying these changes, we get the following output:
Output: [avg : 3.0, max : 5.0, min : 1.0, sum : 15.0]
As you can see, all values have specified names derived from bean names.
Source code
All source final source code can be found on GitHub repo Spring Collection Injection .