Autowiring collection of beans in Spring

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

  1. Beans Autowired Annotation