Dependency Injection and Inversion of Control in Spring

In this tutorial, we’ll learn what IoC (Inversion of Control) and DI (Dependency Injection) are, as well as take a look at how these are implemented in the Spring framework.

What Is Inversion of Control (IoC)?

Inversion of Control is a principle in software engineering that separates the control of objects or how parts of a program are called from the implementation. Just like the Hollywood principle of “Don’t call us, we’ll call you.”, the IoC framework frees the developer from the management of calling the code. This is the “inversion” aspect of the Inversion of Control design principle.

The advantages of this architecture are:

  • decoupling the execution of a task from its implementation
  • making it easier to switch between different implementations
  • greater modularity of a program
  • greater ease in testing a program by isolating a component or mocking its dependencies and allowing components to communicate through contracts

We can achieve Inversion of Control using various software design patterns such as Strategy design pattern, Service Locator pattern, Factory pattern, and Dependency Injection (DI).

We’re going to look at DI next.

What Is Dependency Injection (DI)?

Dependency injection basically provides the objects that an object needs (its dependencies) instead of having it construct them. Paired with IoC, connecting objects with other objects, or “injecting” objects into other objects, is done by an assembler rather than by the objects themselves.

In the code below, we are constructing the Patty dependency of the Burger object from within the constructor.

public class Burger {
    private Patty patty;
 
    public Burger() {
        patty = new BeefPatty();    
    }
}

DI is simply to separate the code from managing its dependencies and providing them from elsewhere. This separation allows us to switch to a different implementation of the dependency easily

public class Burger {
    private Patty patty;
    public Burger(Patty patty) {
        this.patty = patty;
    }
}

Notice that in the code example above, we do not create a new instance of BeefPatty class inside of a constructor. Instead, we inject it using the class’s constructor. Also, in the code example above, Patty is an interface that the BeefPatty class implements.

If you are interested in a video tutorial demonstrating how to use constructor-based dependency injection, then look at the Constructor-based Dependency Injection tutorial.

In the next sections, we’ll look at how we can provide the implementation of Patty using IoC and the Spring Framework through metadata.

The Spring IoC Container

The Spring IoC container is Spring’s implementation of using the IoC pattern paired with DI. It is basically a software container that provides a configurable application context in which pluggable objects, known as beans, are created, initialized, cached, and managed

In the Spring framework, the interface ApplicationContext represents the IoC container. The Spring framework provides several implementations of the ApplicationContext interface. One such implementation we will look at in this tutorial is the AnnotationConfigApplicationContext.

We must provide configuration metadata to enable the Spring IoC container to assemble beans. The most popular way to define this metadata in Spring is through annotations, which we will look at in this tutorial.

Below is how to manually create an instance of the container. By using AnnotationConfigApplicationContext, we are telling Spring that we will be providing the metadata through annotations, and it is defined in the AppConfig class we have passed in the constructor:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

We can use metadata to set the patty attribute in the example above. Then the container will read this metadata and use it to assemble beans at runtime.

Dependency Injection in Spring can be done through constructors, setters, or fields. To look at examples of each type of injection, let us first create some classes we will need for the purpose of example.

Let’s first define a Patty interface, which will be the dependency of our Burger class:

@Component
public interface Patty {
    String getType();
}

Now we will create a BeefPatty class implementing the interface above:

public class BeefPatty implements Patty {
    public BeefPatty() {
    }

    @Override
    public String getType() {
        return "Beef Patty";
    }
}

Now, let us have a deeper look at how we can use different types of dependency injections with IoC to inject the Beef Patty dependency of our Burger class.

Constructor-Based Dependency Injection

In this type of dependency injection, the container will be responsible for invoking a constructor and providing the bean dependencies as arguments in the constructor call. Spring finds the bean to provide against each argument primarily by matching type, followed by the attribute’s name and index to remove ambiguity.

For a video tutorial, look at the Constructor-based Dependency Injection video tutorial.

Let’s define our BurgerWithConstructorInjection class. As the name suggests, we will inject the patty dependency via the constructor.

public class BurgerWithConstructorInjection {
    private Patty patty;

    public BurgerWithConstructorInjection(Patty patty) {
        this.patty = patty;
        System.out.println("Burger created with patty: " + patty.getType());
    }
}

Note that we have defined a constructor which takes an argument of type Patty for Spring to implement a constructor-based injection. Patty is an interface that BeefPatty class implements.

Let’s now define the metadata Spring will need to implement constructor injection for our classes:

@Configuration
public class ConstructorBased {
    @Bean
    public Patty patty1() {
        return new BeefPatty();
    }

    @Bean
    public BurgerWithConstructorInjection burger() {
        return new BurgerWithConstructorInjection(patty1());
    }
}

The @Configuration annotation indicates that the class is a source of bean definitions. We can have more than one configuration class in a single application. We use the @Bean annotation on a method to define a bean. If we don’t specify a custom name, the bean name will default to the method name. By default, Beans have a singleton scope, meaning Spring will create only one instance of the bean on the Application startup. For a bean with the default singleton scope, Spring first checks if a cached instance of the bean already exists and only creates a new one if it doesn’t.

Our main class would look like this:

public class IocAndDiConceptsApplication {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConstructorBased.class);
        BurgerWithConstructorInjection burger = applicationContext.getBean(BurgerWithConstructorInjection.class);
    }
}

The output:

...
08:09:09.835 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'constructorBased'
08:09:09.843 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'patty1'
08:09:09.865 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'burger'
Burger created with patty: Beef Patty

We can observe from the output above that firstly, the instance of our Configuration class is instantiated, then the dependency class (the patty in our case) and the class which needs the dependency in the end.

Setter-Based Dependency Injection

For setter-based DI, the container will call setter methods of our class after invoking a no-argument constructor to instantiate the bean.

First, we will define our BurgerWithSetterInjection class. As the name suggests, we will inject the patty dependency via the setter.

public class BurgerWithSetterInjection {
    private Patty patty;

    public void setPatty(Patty patty)
    {
        System.out.println("Burger set with Patty: " + patty.getType());
    }
}

Let’s create the configuration using annotations:

public class SetterBased {
    @Bean
    public Patty patty1() {
        return new BeefPatty();
    }

    @Bean
    public BurgerWithSetterInjection burger() {
        BurgerWithSetterInjection burger = new BurgerWithSetterInjection();
        burger.setPatty(patty1());
        return burger;
    }
}

Our main class:

public class IocAndDiConceptsApplication {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SetterBased.class);
        BurgerWithSetterInjection burger = applicationContext.getBean(BurgerWithSetterInjection.class);
    }
}

The output:

..
08:52:17.339 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'setterBased'
08:52:17.352 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'patty1'
08:52:17.382 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'burger'
Burger set with Patty: Beef Patty

Combining constructor-based and setter-based types of injection for the same bean is possible. The Spring documentation recommends using constructor-based injection for mandatory dependencies and setter-based injection for optional ones.

Field-Based Dependency Injection

In the case of Field-Based DI, we can inject the dependencies by marking them with an @Autowired annotation.

We can define our BurgerWithFieldInjection like this:

public class BurgerWithFieldInjection {
    @Autowired
    private Patty patty;

    public String getPattyType() {
        return patty.getType();
    }
}

Notice that no constructor or setter method is defined in this case. Spring will first try to find a constructor or a setter method. If there’s no constructor or setter method to inject the Patty bean, the container will use reflection to inject the dependency. For the reflection to work, the field must be annotated with @Autowired.

In this case, our Configuration class will be simpler:

@Configuration
public class FieldBased {

    @Bean
    public Patty patty1() {
        return new BeefPatty();
    }

    @Bean
    public BurgerWithFieldInjection burger() {
        return new BurgerWithFieldInjection();
    }
}

The main class:

public class IocAndDiConceptsApplication {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(FieldBased.class);
        BurgerWithFieldInjection burger = applicationContext.getBean(BurgerWithFieldInjection.class);
        System.out.println("Burger autowired with Patty: " + burger.getPattyType());
    }
}

The output:

...
08:58:32.640 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'fieldBased'
08:58:32.645 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'patty1'
08:58:32.660 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'burger'
Burger autowired with Patty: Beef Patty

This approach might look simpler and cleaner, but we don’t recommend using it because it has a few drawbacks, such as:

  • This method uses reflection to inject the dependencies. Therefore, it is slower than constructor-based or setter-based injection.
  • Since we are just plugging the @Autowired annotation on fields of a class, it does not give us a clear view that our class may be using a lot of dependencies and, therefore, may be performing more than one task. Therefore, it’s less easy to cope with the Single Responsibility Principle.

In this tutorial, we learned the concepts of Inversion of Control and Dependency Injection. In addition, we also looked at how Spring makes use of these concepts. Thanks for following the tutorial till the end. I hope this added to your knowledge.

Happy learning!