TP5.1 - Advanced Dependency Injection
Objectives
By the end of this lab, you will be able to:
- ✅ Use the 3 types of injection (constructor, setter, field)
- ✅ Choose between multiple implementations with @Qualifier
- ✅ Manage optional dependencies
- ✅ Understand and resolve circular dependencies
Estimated duration: 1h30
Prerequisites
- ✅ Chapters 1 and 2 of Module 5 completed
- ✅ Maven 3.9+ installed
- ✅ Java 17+ installed
- ✅ VS Code with Java extensions
Preparation: Create the Project
Step 1: Generate the Project with Spring Initializr
Via the website:
-
Go to https://start.spring.io/
-
Configure:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.0 (or latest version)
- Group:
com.example - Artifact:
injection-demo - Name:
injection-demo - Package name:
com.example.injection - Packaging: Jar
- Java: 17
-
Dependencies to add:
- Spring Web
-
Click on GENERATE
-
Unzip and open in VS Code
Step 2: Project Structure
Create the following structure:
src/main/java/com/example/injection/
├── InjectionDemoApplication.java
├── service/
│ ├── EmailService.java
│ ├── GmailService.java
│ ├── SendGridService.java
│ └── NotificationService.java
├── controller/
│ └── TestController.java
└── config/
└── AppConfig.java
Exercise 1: Constructor Injection ✅
Objective
Create a service with constructor injection (best practice).
Step 1.1: Create the EmailService Interface
File: service/EmailService.java
package com.example.injection.service;
public interface EmailService {
void send(String to, String message);
String getProviderName();
}
What we're doing: Create an interface to have multiple possible implementations.
Step 1.2: Create the GmailService Implementation
File: service/GmailService.java
package com.example.injection.service;
import org.springframework.stereotype.Service;
@Service
public class GmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("[Gmail] Sending email to " + to + ": " + message);
}
@Override
public String getProviderName() {
return "Gmail";
}
}
Key points:
@Service→ Spring manages this Bean- Implements
EmailService
Step 1.3: Create the NotificationService with Constructor Injection
File: service/NotificationService.java
package com.example.injection.service;
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
// Dependency declared as 'final' (immutable)
private final EmailService emailService;
// Constructor injection (BEST PRACTICE ✅)
public NotificationService(EmailService emailService) {
this.emailService = emailService;
System.out.println("NotificationService created with: " +
emailService.getProviderName());
}
public void notifyUser(String email, String message) {
System.out.println("=== NotificationService ===");
emailService.send(email, message);
}
}
Key points:
private final→ Immutable- Injection via constructor
- No need for
@Autowired(optional on single constructor)
Step 1.4: Create the Test Controller
File: controller/TestController.java
package com.example.injection.controller;
import com.example.injection.service.NotificationService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
public class TestController {
private final NotificationService notificationService;
// Constructor injection also here
public TestController(NotificationService notificationService) {
this.notificationService = notificationService;
}
@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"Test constructor injection");
return "✅ Constructor injection tested - See console";
}
}
Step 1.5: Test
Start the application:
mvn clean install -DskipTests spring-boot:run
Test in the browser:
http://localhost:8080/api/test/constructor
Console should display:
NotificationService created with: Gmail
=== NotificationService ===
[Gmail] Sending email to user@example.com: Test constructor injection
✅ Expected result: "✅ Constructor injection tested - See console"
Exercise 2: Manage Multiple Implementations with @Qualifier
Objective
Have 2 implementations of EmailService and choose which one to use.
Step 2.1: Create SendGridService
File: service/SendGridService.java
package com.example.injection.service;
import org.springframework.stereotype.Service;
@Service("sendGridService") // ← Explicit name
public class SendGridService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("[SendGrid] Sending email to " + to + ": " + message);
}
@Override
public String getProviderName() {
return "SendGrid";
}
}
Step 2.2: Problem - Two Implementations!
Restart the application:
mvn clean install -DskipTests spring-boot:run
You should see an ERROR:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.example.injection.service.NotificationService
required a single bean, but 2 were found:
- gmailService: defined in file [...]
- sendGridService: defined in file [...]
Action:
Consider marking one of the beans as @Primary, or use @Qualifier
This is normal! Spring doesn't know which one to choose.
Step 2.3: Solution 1 - @Primary
Modify GmailService.java:
package com.example.injection.service;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
@Service
@Primary // ← Default implementation
public class GmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("[Gmail] Sending email to " + to + ": " + message);
}
@Override
public String getProviderName() {
return "Gmail";
}
}
Restart: The application now starts! Gmail is used by default.
Test:
http://localhost:8080/api/test/constructor
Console:
[Gmail] Sending email to user@example.com: ...
Step 2.4: Solution 2 - @Qualifier to Choose
Create a new service that uses SendGrid:
File: service/SendGridNotificationService.java
package com.example.injection.service;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class SendGridNotificationService {
private final EmailService emailService;
// Use @Qualifier to choose SendGrid
public SendGridNotificationService(
@Qualifier("sendGridService") EmailService emailService
) {
this.emailService = emailService;
System.out.println("SendGridNotificationService created with: " +
emailService.getProviderName());
}
public void notifyUser(String email, String message) {
System.out.println("=== SendGridNotificationService ===");
emailService.send(email, message);
}
}
Step 2.5: Test @Qualifier
Add a method in TestController.java:
@RestController
@RequestMapping("/api/test")
public class TestController {
private final NotificationService notificationService;
private final SendGridNotificationService sendGridNotificationService;
public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
}
@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"Test with Gmail");
return "✅ Gmail used - See console";
}
@GetMapping("/qualifier")
public String testQualifier() {
sendGridNotificationService.notifyUser("user@example.com",
"Test with SendGrid");
return "✅ SendGrid used via @Qualifier - See console";
}
}
Test:
http://localhost:8080/api/test/qualifier
Console:
=== SendGridNotificationService ===
[SendGrid] Sending email to user@example.com: Test with SendGrid
✅ You have 2 services using different implementations!
Exercise 3: Optional Dependencies
Objective
Create a service that can work with or without a dependency.
Step 3.1: Create an SmsService (Optional)
File: service/SmsService.java
package com.example.injection.service;
public interface SmsService {
void send(String phone, String message);
}
Do NOT create an implementation yet!
Step 3.2: Create CompleteNotificationService with Optional Dependency
File: service/CompleteNotificationService.java
package com.example.injection.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class CompleteNotificationService {
private final EmailService emailService;
private final Optional<SmsService> smsService; // ← Optional!
public CompleteNotificationService(
EmailService emailService,
Optional<SmsService> smsService // ← Optional Wrapper
) {
this.emailService = emailService;
this.smsService = smsService;
System.out.println("CompleteNotificationService created");
System.out.println("- Email: " + emailService.getProviderName());
System.out.println("- SMS: " + (smsService.isPresent() ? "Available" : "Not available"));
}
public void notify(String email, String phone, String message) {
// Email always sent
emailService.send(email, message);
// SMS only if available
smsService.ifPresent(sms -> sms.send(phone, message));
}
}
Key points:
Optional<SmsService>→ May be absentifPresent()→ Executed only if present
Step 3.3: Test Without SmsService
Add in TestController.java:
private final CompleteNotificationService completeService;
public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService,
CompleteNotificationService completeService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
this.completeService = completeService;
}
@GetMapping("/optional")
public String testOptional() {
completeService.notify("user@example.com", "123-456-7890",
"Test optional dependency");
return "✅ Email sent, SMS not available - See console";
}
Test:
http://localhost:8080/api/test/optional
Console:
CompleteNotificationService created
- Email: Gmail
- SMS: Not available
[Gmail] Sending email to user@example.com: Test optional dependency
✅ The application works even without SmsService!
Step 3.4: Add SmsService and Retest
File: service/SimpleSmsService.java
package com.example.injection.service;
import org.springframework.stereotype.Service;
@Service
public class SimpleSmsService implements SmsService {
@Override
public void send(String phone, String message) {
System.out.println("[SMS] Sending to " + phone + ": " + message);
}
}
Restart and test:
http://localhost:8080/api/test/optional
Console:
CompleteNotificationService created
- Email: Gmail
- SMS: Available
[Gmail] Sending email to user@example.com: Test optional dependency
[SMS] Sending to 123-456-7890: Test optional dependency
✅ Now both are used!
Exercise 4: Inject All Implementations
Objective
Retrieve ALL implementations of an interface.
Step 4.1: Create MultiChannelNotificationService
File: service/MultiChannelNotificationService.java
package com.example.injection.service;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MultiChannelNotificationService {
private final List<EmailService> allEmailServices;
// Spring injects a List with ALL implementations
public MultiChannelNotificationService(List<EmailService> allEmailServices) {
this.allEmailServices = allEmailServices;
System.out.println("MultiChannelNotificationService created with " +
allEmailServices.size() + " email services:");
allEmailServices.forEach(service ->
System.out.println(" - " + service.getProviderName())
);
}
public void broadcastEmail(String to, String message) {
System.out.println("=== Sending via ALL providers ===");
allEmailServices.forEach(service -> service.send(to, message));
}
}
Step 4.2: Test
Add in TestController.java:
private final MultiChannelNotificationService multiChannelService;
public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService,
CompleteNotificationService completeService,
MultiChannelNotificationService multiChannelService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
this.completeService = completeService;
this.multiChannelService = multiChannelService;
}
@GetMapping("/all")
public String testAllImplem() {
multiChannelService.broadcastEmail("user@example.com",
"Broadcast message");
return "✅ Sent via all providers - See console";
}
Test:
http://localhost:8080/api/test/all
Console:
=== Sending via ALL providers ===
[Gmail] Sending email to user@example.com: Broadcast message
[SendGrid] Sending email to user@example.com: Broadcast message
✅ Both implementations are used!
Exercise 5: Setter Injection (Optional Dependencies)
Objective
Use setter injection for truly optional dependencies.
Step 5.1: Create LoggerService with Setter
File: service/LoggerService.java
package com.example.injection.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LoggerService {
private SmsService smsService; // ← Not 'final', can be null
// Setter injection - OPTIONAL
@Autowired(required = false) // ← required = false
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
System.out.println("SMS Service injected into LoggerService");
}
public void logNotification(String message) {
System.out.println("[LOG] " + message);
if (smsService != null) {
System.out.println("[LOG] SMS Service available");
} else {
System.out.println("[LOG] SMS Service not available");
}
}
}
Key points:
@Autowired(required = false)→ Not mandatory- Check
if (smsService != null)
Step 5.2: Test
Add in TestController.java:
private final LoggerService loggerService;
@GetMapping("/setter")
public String testSetter() {
loggerService.logNotification("Test setter injection");
return "✅ Setter injection tested - See console";
}
Test:
http://localhost:8080/api/test/setter
Console (with SmsService):
SMS Service injected into LoggerService
[LOG] Test setter injection
[LOG] SMS Service available
Recap: Comparison of Methods
Comparison Table
| Type | Syntax | Mandatory? | Immutable? | Usage |
|---|---|---|---|---|
| Constructor | public Service(Dep d) | ✅ Yes | ✅ Yes (final) | Recommended for mandatory dependencies |
| Setter | @Autowired(required=false) | ❌ No | ❌ No | Optional dependencies |
| Field | @Autowired private Dep d | ⚠️ Yes | ❌ No | ❌ To avoid |
Optional<T> | Optional<Dep> d | ❌ No | ✅ Yes | Modern optional dependencies |
List<T> | List<Dep> all | ✅ Yes | ✅ Yes | All implementations |
Complete Final Code
Your project should now have:
✅ GmailService (@Primary)
✅ SendGridService
✅ NotificationService (constructor + Gmail)
✅ SendGridNotificationService (constructor + @Qualifier)
✅ CompleteNotificationService (Optional<SmsService>)
✅ SimpleSmsService (optional)
✅ MultiChannelNotificationService (List<EmailService>)
✅ LoggerService (setter + required=false)
Self-Assessment
Answer the following questions:
- Why is constructor injection preferred?
- What is the difference between @Primary and @Qualifier?
- How to manage a truly optional dependency?
- How to inject all implementations of an interface?
- When to use
Optional<T>vs@Autowired(required=false)?
If you can answer all these questions, you have mastered dependency injection! ✅
Bonus Challenge 🎯
Create a multi-channel notification system:
- Interface
NotificationChannel - Implementations:
EmailChannel,SmsChannel,PushChannel - Service
NotificationOrchestratorthat:- Injects all channels
- Allows enabling/disabling channels
- Sends on all active channels
Estimated time: 30 minutes
Next Step
You now master dependency injection! Move on to TP2 to learn about Beans and Scopes.