What is CDI?
Think of CDI like a restaurant with an automated service system:
- 🍽️ You (code) don't go to the kitchen to get ingredients
- 🤖 A service system (CDI container) automatically brings what you need
- 📦 You just declare "I need a chef" and the system provides one
- ♻️ The system manages when to create/destroy resources
CDI (Contexts and Dependency Injection) is a framework that automatically creates objects and injects dependencies for you. It's the core of Jakarta EE that ties everything together.
Key Benefits: No manual object creation (no 'new'), loose coupling, easier testing, lifecycle management
The Problem CDI Solves
// Without CDI - Manual creation (tight coupling)
public class OrderService {
private PaymentService paymentService = new PaymentService();
private EmailService emailService = new EmailService();
// Hard to test, hard to change implementation
}
// With CDI - Automatic injection (loose coupling)
public class OrderService {
@Inject
private PaymentService paymentService;
@Inject
private EmailService emailService;
// CDI provides instances automatically!
// Easy to test, easy to swap implementations
}
Basic Dependency Injection with @Inject
Simple Example
// 1. Create a bean (just a regular class)
public class GreetingService {
public String greet(String name) {
return "Hello, " + name + "!";
}
}
// 2. Inject it wherever needed
public class WelcomeController {
@Inject // CDI injects GreetingService automatically
private GreetingService greetingService;
public void welcome() {
String message = greetingService.greet("Alice");
System.out.println(message); // Hello, Alice!
}
}
What happened?
- You marked a field with @Inject
- CDI container creates a GreetingService instance
- CDI automatically assigns it to your field
- You use it without calling 'new'!
Three Ways to Inject
// 1. Field Injection (simplest, most common)
public class ServiceA {
@Inject
private Database database;
}
// 2. Constructor Injection (best for testing)
public class ServiceB {
private final Database database;
@Inject
public ServiceB(Database database) {
this.database = database;
}
}
// 3. Setter Injection (optional dependencies)
public class ServiceC {
private Database database;
@Inject
public void setDatabase(Database database) {
this.database = database;
}
}
CDI Scopes - Bean Lifecycles
Scopes control how long a bean lives and how many instances exist
1. @RequestScoped - One Instance Per HTTP Request
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
@Named // Makes it accessible from JSP/JSF
@RequestScoped // New instance for each HTTP request
public class ShoppingCartBean {
private List<Product> items = new ArrayList<>();
public void addItem(Product product) {
items.add(product);
}
// Destroyed after request completes
}
2. @SessionScoped - One Instance Per User Session
import jakarta.enterprise.context.SessionScoped;
import java.io.Serializable;
@Named
@SessionScoped // Lives across multiple requests from same user
public class UserSessionBean implements Serializable {
private String username;
private boolean loggedIn;
public void login(String username) {
this.username = username;
this.loggedIn = true;
}
// Destroyed when session expires or user logs out
}
3. @ApplicationScoped - One Instance for Entire App
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped // Singleton - shared by all users
public class ConfigService {
private Map<String, String> config = new HashMap<>();
@PostConstruct
public void init() {
// Load config once when app starts
config.put("app.name", "My App");
}
// Lives until application stops
}
4. @Dependent - New Instance Every Time (Default)
import jakarta.enterprise.context.Dependent;
@Dependent // Default scope if no annotation
public class Calculator {
public int add(int a, int b) {
return a + b;
}
// New instance created every time it's injected
// Destroyed with the bean that owns it
}
| Scope | Lifetime | Instances | Use Case |
|---|---|---|---|
| @Dependent | Same as parent | New every injection | Stateless utilities |
| @RequestScoped | One HTTP request | One per request | Form data, request processing |
| @SessionScoped | User session | One per user | User login, shopping cart |
| @ApplicationScoped | Entire app | One (singleton) | Config, caches, shared data |
| @ConversationScoped | Multi-step flow | One per conversation | Wizards, multi-page forms |
Qualifiers - Choosing Between Implementations
Problem: What if you have multiple implementations of an interface?
Define Qualifiers
import jakarta.inject.Qualifier;
import java.lang.annotation.*;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface CreditCard { }
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface PayPal { }
Use Qualifiers on Implementations
// Interface
public interface PaymentProcessor {
void processPayment(double amount);
}
// Implementation 1
@CreditCard // Qualifier
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing $" + amount + " via Credit Card");
}
}
// Implementation 2
@PayPal // Qualifier
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing $" + amount + " via PayPal");
}
}
Inject Specific Implementation
public class CheckoutService {
@Inject
@CreditCard // Inject CreditCardProcessor
private PaymentProcessor creditCardProcessor;
@Inject
@PayPal // Inject PayPalProcessor
private PaymentProcessor payPalProcessor;
public void checkout(String paymentMethod, double amount) {
if ("credit".equals(paymentMethod)) {
creditCardProcessor.processPayment(amount);
} else {
payPalProcessor.processPayment(amount);
}
}
}
Producers - Custom Bean Creation
Sometimes you need more control over how beans are created
@Produces Methods
public class DatabaseProducer {
@Produces
@ApplicationScoped
public DataSource createDataSource() {
// Custom creation logic
DataSource ds = new DataSource();
ds.setUrl("jdbc:postgresql://localhost/mydb");
ds.setUsername("admin");
ds.setPassword("secret");
return ds;
}
@Produces
@Named("maxConnections")
public int getMaxConnections() {
return 100;
}
}
// Now you can inject them
public class UserRepository {
@Inject
private DataSource dataSource; // Injected from producer
@Inject
@Named("maxConnections")
private int maxConnections;
}
@Produces with Qualifiers
public class LoggerProducer {
@Produces
@Dependent
public Logger createLogger(InjectionPoint injectionPoint) {
// Automatically get logger for each class
Class<?> targetClass = injectionPoint.getMember().getDeclaringClass();
return Logger.getLogger(targetClass.getName());
}
}
// Usage - each class gets its own logger
public class UserService {
@Inject
private Logger logger; // Logger for UserService
public void doSomething() {
logger.info("UserService.doSomething called");
}
}
@Disposes - Cleanup Resources
public class ResourceManager {
@Produces
@RequestScoped
public Connection createConnection() throws SQLException {
return DriverManager.getConnection("jdbc:...");
}
public void closeConnection(@Disposes Connection connection) {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Lifecycle Callbacks
@PostConstruct and @PreDestroy
import jakarta.annotation.*;
@ApplicationScoped
public class CacheService {
private Map<String, Object> cache;
@PostConstruct // Called AFTER bean creation and injection
public void init() {
System.out.println("Initializing cache...");
cache = new HashMap<>();
// Load initial data
cache.put("key1", "value1");
}
@PreDestroy // Called BEFORE bean destruction
public void cleanup() {
System.out.println("Cleaning up cache...");
if (cache != null) {
cache.clear();
}
// Save data, close connections, etc.
}
public Object get(String key) {
return cache.get(key);
}
}
Order of execution:
1. Constructor called
2. Dependencies injected (@Inject)
3. @PostConstruct method called ← Initialize here
4. Bean ready to use
... bean used by application ...
5. @PreDestroy method called ← Cleanup here
6. Bean destroyed
Events - Publish/Subscribe Pattern
Fire and Observe Events
// 1. Event class (just a POJO)
public class UserRegisteredEvent {
private final String username;
private final String email;
public UserRegisteredEvent(String username, String email) {
this.username = username;
this.email = email;
}
public String getUsername() { return username; }
public String getEmail() { return email; }
}
// 2. Fire event (publisher)
public class RegistrationService {
@Inject
private Event<UserRegisteredEvent> userRegisteredEvent;
public void register(String username, String email) {
// Register user in database...
// Fire event
userRegisteredEvent.fire(new UserRegisteredEvent(username, email));
}
}
// 3. Observe event (subscribers)
public class EmailNotificationService {
public void onUserRegistered(@Observes UserRegisteredEvent event) {
System.out.println("Sending welcome email to " + event.getEmail());
// Send email...
}
}
public class AnalyticsService {
public void onUserRegistered(@Observes UserRegisteredEvent event) {
System.out.println("Tracking registration for " + event.getUsername());
// Track analytics...
}
}
What happens:
- User registers → RegistrationService.register() called
- Event fired →
userRegisteredEvent.fire(event) - All @Observes methods automatically called
- EmailNotificationService sends email
- AnalyticsService tracks data
- Completely decoupled!
Interceptors - Cross-Cutting Concerns
Add behavior to methods without changing the code
Create Interceptor Binding
import jakarta.interceptor.InterceptorBinding;
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logged { }
Create Interceptor
import jakarta.interceptor.*;
@Logged
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LoggingInterceptor {
@AroundInvoke
public Object logMethod(InvocationContext context) throws Exception {
String methodName = context.getMethod().getName();
System.out.println("[LOG] Entering: " + methodName);
long start = System.currentTimeMillis();
try {
// Proceed with the actual method call
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
System.out.println("[LOG] Exiting: " + methodName + " (" + duration + "ms)");
}
}
}
Use Interceptor
public class UserService {
@Logged // This method will be logged
public User findUser(Long id) {
// Your business logic
return database.find(id);
}
@Logged
public void saveUser(User user) {
database.save(user);
}
}
Output:
[LOG] Entering: findUser
[LOG] Exiting: findUser (23ms)
[LOG] Entering: saveUser
[LOG] Exiting: saveUser (45ms)
Common use cases: Logging, transactions, security, caching, performance monitoring
Best Practices
✅ DO:
- Use constructor injection for required dependencies - Makes testing easier
- Choose the right scope - @RequestScoped for request data, @ApplicationScoped for singletons
- Use qualifiers for multiple implementations - Clear and explicit
- Implement Serializable for @SessionScoped - Required for session replication
- Use @PostConstruct for initialization - Not the constructor
- Use events for loose coupling - Modules don't know about each other
- Keep beans stateless when possible - Thread-safe and scalable
❌ DON'T:
- Don't inject @RequestScoped into @ApplicationScoped - Scope mismatch!
- Don't create beans with 'new' - CDI won't manage them
- Don't forget @Named for JSF access - Required for view binding
- Don't store heavy objects in @SessionScoped - Memory issues
- Don't use field injection for testing - Use constructor injection
- Don't put business logic in producers - Keep them simple
Summary
- CDI is the dependency injection framework at the heart of Jakarta EE
- @Inject automatically provides dependencies (no 'new' needed)
- Scopes control bean lifecycle: @Dependent, @RequestScoped, @SessionScoped, @ApplicationScoped
- @Qualifier distinguishes between multiple implementations
- @Produces creates beans with custom logic
- @PostConstruct/@PreDestroy lifecycle callbacks for init/cleanup
- Events enable publish-subscribe pattern with @Observes
- Interceptors add cross-cutting concerns (logging, security, etc.)
- Benefits: Loose coupling, easier testing, automatic lifecycle management
- Similar to: Spring's @Autowired but standards-based