Spring Core: IoC & DI

Inversion of Control and Dependency Injection

← Back to Index

What is Spring Core?

Spring Core is the foundation of the Spring Framework, providing the fundamental features of Inversion of Control (IoC) and Dependency Injection (DI). It manages the lifecycle and configuration of application objects (beans).

Core Concepts
  • IoC Container - Manages object creation and lifecycle
  • Dependency Injection - Objects receive their dependencies instead of creating them
  • Beans - Objects managed by the Spring IoC container
  • Application Context - The IoC container implementation

Inversion of Control (IoC)

IoC is a design principle where the control of object creation and lifecycle is inverted from the application code to a container or framework.

Traditional Approach (Without IoC)

public class UserService {
    private UserRepository userRepository;

    public UserService() {
        // Application code controls object creation
        this.userRepository = new UserRepositoryImpl();
    }

    public User getUserById(int id) {
        return userRepository.findById(id);
    }
}

IoC Approach (With Spring)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    private final UserRepository userRepository;

    // Spring container controls object creation and injection
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(int id) {
        return userRepository.findById(id);
    }
}
Benefits of IoC
  • Loose coupling between classes
  • Easier testing (can inject mocks)
  • Better separation of concerns
  • Centralized configuration

Dependency Injection (DI)

DI is the implementation of IoC where dependencies are "injected" into objects by the container instead of objects creating their own dependencies.

Types of Dependency Injection

1. Constructor Injection (Recommended)

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    // Constructor injection - dependencies are injected via constructor
    @Autowired // @Autowired is optional if there's only one constructor
    public OrderService(OrderRepository orderRepository,
                       PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    public void processOrder(Order order) {
        orderRepository.save(order);
        paymentService.processPayment(order);
    }
}

2. Setter Injection

@Service
public class EmailService {
    private EmailProvider emailProvider;

    // Setter injection - dependency injected via setter method
    @Autowired
    public void setEmailProvider(EmailProvider emailProvider) {
        this.emailProvider = emailProvider;
    }

    public void sendEmail(String to, String message) {
        emailProvider.send(to, message);
    }
}

3. Field Injection (Not Recommended)

@Service
public class NotificationService {
    // Field injection - less preferred
    @Autowired
    private EmailService emailService;

    @Autowired
    private SmsService smsService;

    public void notifyUser(User user) {
        emailService.sendEmail(user.getEmail(), "Hello!");
        smsService.sendSms(user.getPhone(), "Hello!");
    }
}
Why Constructor Injection is Preferred
  • Promotes immutability (final fields)
  • Makes dependencies explicit and required
  • Easier to test (can instantiate without Spring)
  • Prevents NullPointerExceptions

Spring Beans

A bean is an object that is instantiated, assembled, and managed by the Spring IoC container.

Declaring Beans with Annotations

Component Scanning

// Spring automatically detects and registers these as beans

@Component  // Generic stereotype
public class UtilityComponent {
    // Component logic
}

@Service  // Service layer stereotype
public class UserService {
    // Business logic
}

@Repository  // Data access layer stereotype
public class UserRepository {
    // Database operations
}

@Controller  // Presentation layer stereotype
public class UserController {
    // HTTP request handling
}

Java Configuration

import org.springframework.context.annotation.*;

@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        return new UserService(userRepository());
    }

    @Bean
    public UserRepository userRepository() {
        return new UserRepositoryImpl();
    }

    // Bean with custom name
    @Bean(name = "emailSender")
    public EmailService emailService() {
        return new EmailServiceImpl();
    }
}

Bean Scopes

Bean scope defines the lifecycle and visibility of bean instances.

Common Scopes

Singleton (Default)

@Component
@Scope("singleton")  // This is the default, can be omitted
public class ConfigService {
    // One instance per Spring container
    // Shared across the entire application
}

Prototype

@Component
@Scope("prototype")
public class TaskProcessor {
    // New instance created every time it's requested
    // Useful for stateful objects
}

Web Scopes (Spring Web)

// Request scope - one instance per HTTP request
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
      proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    // New instance for each HTTP request
}

// Session scope - one instance per HTTP session
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
      proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {
    // Same instance throughout user's session
}
Scope Summary
  • singleton - One instance per container (default)
  • prototype - New instance every time
  • request - One per HTTP request (web apps)
  • session - One per HTTP session (web apps)
  • application - One per ServletContext (web apps)

Application Context

The ApplicationContext is the central interface for providing configuration to a Spring application.

Creating Application Context

With Spring Boot (Recommended)

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication  // Combines @Configuration, @EnableAutoConfiguration, @ComponentScan
public class Application {
    public static void main(String[] args) {
        // Spring Boot creates and manages the ApplicationContext
        SpringApplication.run(Application.class, args);
    }
}

Programmatic Creation (Without Spring Boot)

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        // Create context from Java configuration
        ApplicationContext context =
            new AnnotationConfigApplicationContext(AppConfig.class);

        // Retrieve beans
        UserService userService = context.getBean(UserService.class);
        userService.doSomething();

        // Close context (important!)
        ((AnnotationConfigApplicationContext) context).close();
    }
}

Complete Example

Here's a complete example showing IoC and DI in action:

1. Repository Layer

import org.springframework.stereotype.Repository;
import java.util.*;

@Repository
public class UserRepository {
    private Map<Long, User> database = new HashMap<>();

    public User findById(Long id) {
        return database.get(id);
    }

    public void save(User user) {
        database.put(user.getId(), user);
    }

    public List<User> findAll() {
        return new ArrayList<>(database.values());
    }
}

2. Service Layer

import org.springframework.stereotype.Service;
import java.util.*;

@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    // Constructor injection
    public UserService(UserRepository userRepository,
                       EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    public User createUser(User user) {
        userRepository.save(user);
        emailService.sendWelcomeEmail(user.getEmail());
        return user;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id);
    }

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
}

3. Supporting Service

import org.springframework.stereotype.Service;

@Service
public class EmailService {
    public void sendWelcomeEmail(String email) {
        System.out.println("Sending welcome email to: " + email);
        // Email sending logic
    }
}

4. Controller (Optional)

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        return userService.createUser(user);
    }
}

Why Spring Core Matters

Common Pitfalls

Circular Dependencies
@Service
public class A {
    private final B b;
    public A(B b) { this.b = b; }
}

@Service
public class B {
    private final A a;
    public B(A a) { this.a = a; }  // CIRCULAR DEPENDENCY!
}

// Solution: Redesign your classes or use @Lazy annotation
Field Injection Makes Testing Harder

Use constructor injection instead of field injection for better testability and to make dependencies explicit.