Types of Dependency Injection in Spring Framework

Types of Dependency Injection in Spring Framework

Types of Dependency Injection in Spring Framework

In this tutorial, we are going to learn different types of DI
  • Constructor Injection
  • Setter Injection
  • Field Injection
For more on IoC and DI, explore our other articles.

Constructor Injection

In constructor injection, the Spring IoC container injects dependencies via its constructor. The component declares one or more constructors with dependencies as arguments, and IoC injects those dependencies into the component class.
Let's look into the simple Notification System to demonstrate constructor injection. The modern Spring Framework, Spring 4.3+, the @Autowire annotation is optional if the class has a single constructor.
java
package org.csbyte.di;


import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

public class ConstructorDemo {
    public static void main(String[] args) {

        ApplicationContext container = new AnnotationConfigApplicationContext(NotificationAppConfig.class);
        NotificationService manager = container.getBean(NotificationService.class);

        manager.sendAlert(
                "john.doe@example.com",
                "Your package has shipped!"
        );
    }
}

@Configuration
@ComponentScan
class NotificationAppConfig {
    // Directs Spring to scan the current package for components to instantiate and wire
}

interface MessageSender {
    void sendNotification(String recipient, String message);
}

@Component("emailService")
class EmailService implements MessageSender {
    @Override
    public void sendNotification(String recipient, String message) {
        System.out.println("Email Sent to " + recipient + ": " + message);
    }
}

@Service
class NotificationService {

    private final MessageSender emailSender;

    public NotificationService(MessageSender emailSender) {
        System.out.println("Executing Constructor Injection for EmailService");
        this.emailSender = emailSender;
    }

    public void sendAlert(String recipientEmail, String message) {
        emailSender.sendNotification(recipientEmail, message);
    }
}
Here, we are using a single file to use all the classes for simplicity of learning purposes; you can separate them out.
Here, we do have a config class NotificationAppConfig email service, and a notification serverice class for accessing the email service as a dependency, MessageSender for abstraction.
Now, let's look into the constructor injection code.
java
public NotificationService(MessageSender emailSender) {
        System.out.println("Executing Constructor Injection for EmailService");
        this.emailSender = emailSender;
    }
Here, we haven't used the @Autowire annotation to inject the dependency. @Autowired is optional here because it's the only constructor used inside NotificationService component.
We can also use it with annotation, as
java
    @Autowired
    public NotificationService(@Qualifier("emailService") MessageSender emailSender) {
        System.out.println("Executing Constructor Injection for EmailService");
        this.emailSender = emailSender;
    }
@Qualifier("emailService") we will define the emailService dependency must be injected. We will implement the other services to show how other injections work.
Let's look into why constructor injection is the best option for dependency injection.
  • It guarantees immutability, i.e, the dependency injection via the constructor can't be changed or altered. If we look into the code, we define the emailSender dependency field as final:private final MessageSender emailSender;
  • Object can't be created without its dependency, which results in the dependency being non-null or it is mandatory. This will lead to no more NullPointerException in runtime.
  • As the fields are final, it's thread-safe ie the objects are safer in a concurrent environment.
  • Easity testing as we can simply pass mock dependencies directly into the constructor

Setter Injection

In setter dependency injection, the IoC container injects component dependencies via a setter method
java
package org.csbyte.di;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

public class SetterInjectionDemo {
    public static void main(String[] args) {
        ApplicationContext container = new AnnotationConfigApplicationContext(NotificationAppConfig.class);
        NotificationService manager = container.getBean(NotificationService.class);

        manager.sendAlert("+1-555-0199", "Your package has shipped!");
    }
}

@Configuration
@ComponentScan
class NotificationAppConfig {
}

interface MessageSender {
    void sendNotification(String recipient, String message);
}

@Component("smsService")
class SMSService implements MessageSender {
    @Override
    public void sendNotification(String recipient, String message) {
        System.out.println("SMS Sent to " + recipient + ": " + message);
    }
}

@Service
class NotificationService {

    private MessageSender smsSender;

    @Autowired
    public void setSmsSender(MessageSender smsSender) {
        System.out.println("Executing Setter Injection for SMSService");
        this.smsSender = smsSender;
    }

    public void sendAlert(String phoneNumber, String message) {
        System.out.println("\n--- Initiating SMS Broadcast ---");
        smsSender.sendNotification(phoneNumber, message);
    }
}
It can be flexible for optional dependencies that can be provided later by calling the setter method.
Another benefit of setter injection is that we can swap dependencies for a different implementation without creating a new instance of the parent component. Here is the example:
java
package org.csbyte.di;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

public class SetterInjectionDemo {
    public static void main(String[] args) {
        ApplicationContext container = new AnnotationConfigApplicationContext(NotificationAppConfig.class);
        NotificationService manager = container.getBean(NotificationService.class);

        // Initial run using the production SMS service wired by Spring
        manager.sendAlert("+1-555-0199", "Your package has shipped!");

        // Grab a different implementation from the container
        MessageSender mockSms = container.getBean("mockSmsService", MessageSender.class);

        // Swap it on-the-fly without creating a new NotificationService instance!
        System.out.println("\nSwapping implementation to Mock Service...");
        manager.setSmsSender(mockSms);

        // Run it again—same parent object, completely different behavior
        manager.sendAlert("+1-555-0199", "Your package has shipped!");
    }
}

@Configuration
@ComponentScan
class NotificationAppConfig {
}

interface MessageSender {
    void sendNotification(String recipient, String message);
}

@Component("smsService")
class SMSService implements MessageSender {
    @Override
    public void sendNotification(String recipient, String message) {
        System.out.println("REAL SMS Sent to " + recipient + ": " + message);
    }
}

@Component("mockSmsService")
class MockSMSService implements MessageSender {
    @Override
    public void sendNotification(String recipient, String message) {
        System.out.println("MOCK SMS (Simulated) to " + recipient + ": " + message);
    }
}

@Service
class NotificationService {

    private MessageSender smsSender;

    // Acts as Spring's initial wire-up mechanism, AND our runtime gateway swap
    @Autowired
    public void setSmsSender(@Qualifier("smsService") MessageSender smsSender) {
        this.smsSender = smsSender;
    }

    public void sendAlert(String phoneNumber, String message) {
        smsSender.sendNotification(phoneNumber, message);
    }
}
This mechanism is useful for a circuit breaker or a failover. In the example, if the primary sms provider fails or goes down, the program can grab the backup service from the context.

Field Injection

Field injection is a way to inject dependencies directly into our class as fields. We simply annotate the private field with the @Autowired annotation.
java
package org.csbyte.di;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

class FieldInjectionDemo {
    public static void main(String[] args) {
        ApplicationContext container = new AnnotationConfigApplicationContext(NotificationAppConfig.class);
        NotificationService manager = container.getBean(NotificationService.class);

        manager.sendAlert("device_token_xyz123", "Your package has shipped!");
    }
}

@Configuration
@ComponentScan
class NotificationAppConfig {
}

interface MessageSender {
    void sendNotification(String recipient, String message);
}

@Component("pushService")
class PushNotificationService implements MessageSender {
    @Override
    public void sendNotification(String recipient, String message) {
        System.out.println("Push Notification Sent to device " + recipient + ": " + message);
    }
}

@Service
class NotificationService {

    // FIELD INJECTION
    // No constructor, no setter. Spring injects this directly into the
    // private field via reflection after the class is instantiated.
    @Autowired
    @Qualifier("pushService")
    private MessageSender pushSender;

    public void sendAlert(String deviceId, String message) {
        System.out.println("\n--- Initiating Push Notification Broadcast ---");
        pushSender.sendNotification(deviceId, message);
    }
}
If you notice here, the dependency field is private. Even if it is private, how does the IoC container inject it? Actually, IoC will use reflection to populate the required dependencies.
In general, field injection is not recommended.
  • Single Responsibility Principle Violation: Having more dependencies in a single class component means more responsibility.
  • Losing immutability: The field's injection occurs once the constructor instantiates the class or component; because of this, we can't make the field as final .
  • Difficult writing unit tests: The dependencies must be injected manually for the test class.
  • Violates Encapsulation: It requires Reflection to inject the values, making it tightly coupled to the Spring container.
In this tutorial, we learned the different types of DI, the importance of using them, and the disadvantages of each.