🏠Spring BootInjecting collection of objects in Spring

Injecting collection of objects in Spring

Spring boot can inject/autowire a collection of objects directly as dependencies to other beans and components. Let’s see how to make use of this feature with an example.

We so far have seen the constructor dependency injection and setter dependency injection. In these posts, we only saw autowiring of single object parameters. But Spring framework also can inject(autowire) an array or collection of objects if all qualifying beans are of the same type.

So, Let’s say you have an order validation service that looks like this.

public class OrderValidationService {

    public void validateOrder(OrderDetail orderDetail) {
        if (orderDetail.getOrderId() == null) {
            throw new OrderValidationException("Order ID not found");
        }
        if (orderDetail.getAddress1().isEmpty() || orderDetail.getAddress2().isEmpty()) {
            throw new OrderValidationException("Address cannot be empty");
        }
        if (orderDetail.getOrderItems().isEmpty()) {
            throw new OrderValidationException("Order has no items");
        }
        BigDecimal totalFromItems = orderDetail.getOrderItems()
                .stream()
                .map(orderItem -> orderItem.getPrice().multiply(BigDecimal.valueOf(orderItem.getCount())))
                .reduce(BigDecimal::add)
                .orElse(BigDecimal.valueOf(0));
        if (totalFromItems.equals(orderDetail.getTotal())) {
            throw new OrderValidationException("Order total doesn't match the order items");
        }
    }
}Code language: Java (java)

As you can see here, We are doing different validations in the same method, and the method keeps growing. For example, if you add coupon validation and email validation etc, the method is going to get bigger as well.

To avoid these situations, you could a number of Validator classes and then use the validation service to use them. Let me show you what I’m talking about.

Define beans that you want to inject as collection

In our case, We are going to split the large validateOrder() method into smaller components. So let’s create a Validator interface.

public interface Validator {
    void validate(OrderDetail orderDetail) throws OrderValidationException;
}Code language: PHP (php)

Now we can define different validators that we want to have.

@Component
public class AddressValidator implements Validator {
    @Override
    public void validate(OrderDetail orderDetail) throws OrderValidationException {
        if (!StringUtils.hasText(orderDetail.getAddress1()) 
                || !StringUtils.hasText(orderDetail.getAddress2())) {
            throw new OrderValidationException("Address cannot be empty");
        }
    }
}
Code language: PHP (php)
@Component
public class IdValidator implements Validator {
    @Override
    public void validate(OrderDetail orderDetail) throws OrderValidationException {
        if (orderDetail.getOrderItems().isEmpty()) {
            throw new OrderValidationException("Order has no items");
        }
    }
}Code language: PHP (php)
@Component
public class CartTotalValidator implements Validator {
    @Override
    public void validate(OrderDetail orderDetail) throws OrderValidationException {
        BigDecimal totalFromItems = orderDetail.getOrderItems()
                .stream()
                .map(orderItem -> orderItem.getPrice().multiply(BigDecimal.valueOf(orderItem.getCount())))
                .reduce(BigDecimal::add)
                .orElse(BigDecimal.valueOf(0));
        if (totalFromItems.equals(orderDetail.getTotal())) {
            throw new OrderValidationException("Order total doesn't match the order items");
        }
    }
}Code language: PHP (php)
@Component
public class ItemValidator implements Validator {
    @Override
    public void validate(OrderDetail orderDetail) throws OrderValidationException {
        if (orderDetail.getOrderItems().isEmpty()) {
            throw new OrderValidationException("Order has no items");
        }
    }
}Code language: PHP (php)

Create a component/bean where you want to autowire a collection of beans

Now let’s rewrite our OrderValidationService component. As we have defined each validation in its own component. We can autowire them all at once. In this case, I’m auto-wiring them into a list of validator beans.

@Component
public class OrderValidationService {

    private List<Validator> validators;

    public OrderValidationService(List<Validator> validators) {
        this.validators = validators;
    }

    public void validateOrder(OrderDetail orderDetail) {
        for (Validator validator : validators) {
            System.out.printf("Validating using %s.\n",validator.getClass());
            validator.validate(orderDetail);
        }
    }
}
Code language: PHP (php)

See how clean the code has become now? And if you want to add more validation, it would simply be a new Validator implementation. There is no update in the ValidationService.

injecting/autowiring a collection of beans to a componet

As you can see here, all of the validators are being called one after another to check a given order. The same autowiring works on setter-based injection as well. Also, you can inject these into arrays or other collections like sets. That is, the following injections are also possible.

public class OrderValidationService {

    private Validator[] validators; //injecting beans of same types as array

    public OrderValidationService(Validator[] validators) {
        this.validators = validators;
    }
//
}Code language: PHP (php)
@Component
public class OrderValidationService {

    private Set<Validator> validators; // injecting beans of same type as Set

    public OrderValidationService(Set<Validator> validators) {
        this.validators = validators;
    }
//
}Code language: JavaScript (javascript)

Bean ordering when Autowiring a collection

Spring Framework uses the natural bean ordering based on bean name or the bean class name in ascending order. As you see in the above screenshot, the bean ordering is based on class names. But if you wish to change the ordering, you could use the @Order annotation.

Note that the ordering only works with Array and List-based injection.

The @Order annotation takes an integer as the positioning value. Set this value to a lower number if you want that component to take precedence over other objects.

For instance, the below snippets will bring the Total and Item validation to run first. Also, the TotalValidator doesn’t need to run if there are no items. So we can take care of such situations with ordering.

@Order(0)
@Component
public class ItemValidator implements Validator {Code language: PHP (php)
@Order(1)
@Component
public class TotalValidator implements Validator {Code language: PHP (php)

With such change, this is how the validation happens now.

bean ordering while autowiring collections

As you see, the item validator and TotalValidator run first. This way we didn’t have to run the other validations. Even though you don’t see the other two validators, they were autowired and were never executed due to the failure at TotalValidator.

Related

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *