Blog Design Pattern Spring Boot

Mastering Dependency Injection and Inversion of Control: A Comprehensive Guide

Dependency Injection

Inversion of Control (IoC) is a design principle in which the control flow of a program is inverted: instead of the application controlling the flow of control, the external framework or container does. This inversion helps to decouple the execution of a task from its implementation, making the system more modular and flexible.

Dependency Injection (DI) is a form of IoC. It’s a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). Instead of a client specifying which service it will use, something tells the client what service to use. The “injection” refers to the passing of a dependency (a service) into the object (a client) that would use it.

Here’s a simple example:

Java
public class TextEditor {
    private SpellChecker checker;

    // This is Dependency Injection. The dependency (a SpellChecker) is passed into the TextEditor.
    public TextEditor(SpellChecker checker) {
        this.checker = checker;
    }

    public void checkSpelling() {
        checker.checkSpelling();
    }
}

public class SpellChecker {
    public void checkSpelling() {
        // Check spelling...
    }
}

In this example, TextEditor depends on SpellChecker. Instead of creating a SpellChecker inside TextEditor (which would tightly couple the two classes), we inject SpellChecker into TextEditor via the constructor.

This way, TextEditor doesn’t need to know about the implementation details of SpellChecker, making our code more flexible and easier to test and maintain.

In a Spring application, the Spring container is responsible for injecting dependencies when it creates the beans. This is done either through XML configuration or annotations (@Autowired@Inject, etc.).

Another Example of Implementing Dependency Injection

Scenario: A Messaging Service

Suppose we have an application that requires sending messages. We might have multiple ways of sending these messages (e.g., email, SMS).

Step 1: Define a Common Interface

First, we define a common interface for our messaging services. This ensures that our application can use any messaging service interchangeably.

Java

public interface MessageService {
    void sendMessage(String message, String recipient);
}

Step 2: Implement the Interface

Next, we implement this interface with specific services.

Email Service Implementation:

Java
public class EmailService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        // Logic to send email
        System.out.println("Email sent to " + recipient + " with message: " + message);
    }
}

SMS Service Implementation:

Java
public class SMSService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        // Logic to send SMS
        System.out.println("SMS sent to " + recipient + " with message: " + message);
    }
}

Step 3: Consumer Class Without DI

Without DI, a consumer class might directly instantiate and use a specific service implementation.

Java
public class ApplicationWithOutDI {
    private MessageService emailService = new EmailService();// Direct dependency
    public void processMessage(String message, String recipient){
        emailService.sendMessage(message, recipient);
    }

    public static void main(String[] args) {
        ApplicationWithOutDI applicationWithDI = new ApplicationWithOutDI();
        applicationWithDI.processMessage("Hello, World!", "xyz@email.com");
    }
}

This design is not flexible as changing the messaging service requires modifying the Application class.

Step 4: Implementing Using DI

To use DI, we modify the Application class to accept any MessageService implementation. This way, the Application class is not responsible for creating the service instance.

Java
public class Application {
    private MessageService service;

    // Constructor injection
    public Application(MessageService service) {
        this.service = service;
    }

    public void processMessages(String message, String recipient) {
        // Use the injected service
        service.sendMessage(message, recipient);
    }
}

5: Using an IoC Container

An IoC container can manage the instantiation and injection of dependencies. Here’s a simplistic way to manually achieve this:

Java
public class IoCContainer {
    public static void main(String[] args) {
        // Creating the service and injecting it into the application
        MessageService emailService = new EmailService();
        Application app = new Application(emailService);

        app.processMessages("Hello, DI!", "user@example.com");
    }
}

In this example, the IoCContainer class acts as a simple injector that creates the EmailService and passes it to the Application. This decouples the Application from the specific service implementation, making the system more modular, flexible, and easier to test and maintain. In real-world applications, frameworks like Spring for Java or Autofac for .NET can automate this process, handling the creation and injection of dependencies.

From Theory to Practice: Mastering Dependency Injection with Spring Boot

Spring Boot simplifies the use of the Spring framework for Java development, offering a more streamlined way to set up and configure applications. It leverages the same Inversion of Control (IoC) principles as Spring but with less configuration. Spring Boot automatically configures Spring components based on the libraries present on the classpath and other factors. Here’s an example demonstrating how IoC works in a Spring Boot application:

Step 1: Setup Spring Boot Project

Create a new Spring Boot project using Spring Initializr (https://start.spring.io/) or your favourite IDE with Spring Boot support. For this example, you’ll need the Spring Web and Spring Boot DevTools dependencies.

Step 2: Define a Common Interface

Similar to the previous example, define a MessageService interface for sending messages.

Java
public interface MessageService {
    void sendMessage(String message, String recipient);
}

Step 3: Implement the Interface

Implement this interface with a specific service, such as EmailService.

Java
import org.springframework.stereotype.Service;

@Service
public class EmailService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Email sent to " + recipient + " with message: " + message);
    }
}

Step 4: Create a Spring Boot Application Class

Spring Boot applications start with a main class annotated with @SpringBootApplication. This annotation encompasses @Configuration, @EnableAutoConfiguration, and @ComponentScan annotations.

Create an application class that uses the MessageService to send a message. Spring Boot’s auto-configuration support will automatically configure and inject the EmailService bean into this class.

Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {

    private final MessageService messageService;

    @Autowired
    public Application(MessageService messageService) {
        this.messageService = messageService;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        messageService.sendMessage("Hello, Spring Boot IoC!", "user@example.com");
    }
}

Step 5: Run the Application

When you run this Spring Boot application, the Spring IoC container automatically instantiates, configures, and injects the EmailService into the Application class without requiring explicit bean configuration. This process is facilitated by Spring Boot’s @SpringBootApplication and @Autowired annotations, demonstrating the convention-over-configuration philosophy of Spring Boot.

Spring Boot’s IoC container works behind the scenes to manage your beans, relying on classpath scanning, sensible defaults, and auto-configuration to reduce the amount of manual configuration needed. This allows developers to focus more on their application’s business logic rather than boilerplate code for configuring Spring.

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over