Spring Core

Complete guide to Spring Core: the foundation of the Spring Framework. Learn about Inversion of Control (IoC), Dependency Injection (DI), and how Spring's core container manages your application components.

Table of Contents

1. What is Spring Core?

Spring Core is the foundational module of the Spring Framework that provides the Inversion of Control (IoC) container and Dependency Injection (DI) capabilities. It's the heart of Spring, enabling loose coupling, testability, and maintainability in Java applications.

Spring Core implements the IoC pattern, where the framework controls the object lifecycle and dependencies instead of the application code. This inversion of control allows developers to focus on business logic while Spring manages component creation, wiring, and lifecycle.

The core module provides essential features including resource abstraction, validation, data binding, type conversion, and application events, making it the foundation upon which all other Spring modules are built.

2. Why Use Spring Core?

  • Loose Coupling: Components depend on abstractions, not concrete implementations, making code more flexible and maintainable.
  • Dependency Injection: Dependencies are injected by the container, reducing boilerplate code and improving testability.
  • Testability: Easy to mock dependencies and test components in isolation using Spring's testing framework.
  • Configuration Flexibility: Support for XML, Java annotations, and Java-based configuration.
  • Resource Management: Unified resource abstraction for accessing files, URLs, classpath resources, and more.
  • Lifecycle Management: Automatic management of bean creation, initialization, and destruction.
  • Event-Driven Architecture: Built-in support for application events and listeners.

3. IoC Container

The Inversion of Control (IoC) Container is the core of Spring Framework. It's responsible for instantiating, configuring, and managing beans throughout their lifecycle.

The following diagram illustrates the IoC container architecture and how it manages beans:

graph TB subgraph "Spring IoC Container" A[BeanFactory/ApplicationContext] B[Bean Definition Registry] C[Dependency Injection] D[Lifecycle Management] end subgraph "Configuration Sources" E[XML Configuration] F[Annotation Configuration] G[Java Configuration] end subgraph "Application Components" H[Service Beans] I[Repository Beans] J[Controller Beans] K[Utility Beans] end subgraph "Container Features" L[Resource Loading] M[Event Publishing] N[Internationalization] O[Type Conversion] end E --> B F --> B G --> B B --> A A --> C A --> D C --> H C --> I C --> J C --> K A --> L A --> M A --> N A --> O style A fill:#e1f5ff,stroke:#0273bd,stroke-width:3px style B fill:#fff4e1,stroke:#f57c00,stroke-width:2px style C fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style D fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style H fill:#fce4ec,stroke:#c2185b,stroke-width:2px style I fill:#fce4ec,stroke:#c2185b,stroke-width:2px style J fill:#fce4ec,stroke:#c2185b,stroke-width:2px style K fill:#fce4ec,stroke:#c2185b,stroke-width:2px

The dependency injection flow is illustrated below:

graph LR subgraph "Traditional Approach" A[Service] -->|creates| B[Repository Implementation] A -.->|tight coupling| B end subgraph "Spring IoC Approach" C[Service] -->|depends on| D[Repository Interface] E[Spring Container] -->|injects| F[Repository Implementation] F -.->|implements| D D -.->|used by| C E -->|manages| C E -->|manages| F end style A fill:#ffebee,stroke:#c62828,stroke-width:2px style B fill:#ffebee,stroke:#c62828,stroke-width:2px style C fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style D fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style E fill:#e1f5ff,stroke:#0273bd,stroke-width:3px style F fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

3.1 Inversion of Control Concept

Traditional Approach (Tight Coupling):

public class OrderService {
    private OrderRepository repository = new OrderRepositoryImpl();
    
    public void processOrder(Order order) {
        repository.save(order);
    }
}

Problems:

  • OrderService is tightly coupled to OrderRepositoryImpl
  • Difficult to test (can't easily mock the repository)
  • Hard to switch implementations
  • Violates dependency inversion principle

Spring IoC Approach (Loose Coupling):

@Service
public class OrderService {
    private final OrderRepository repository;
    
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
    
    public void processOrder(Order order) {
        repository.save(order);
    }
}

Benefits:

  • OrderService depends on abstraction (interface)
  • Spring injects the implementation
  • Easy to test with mock implementations
  • Can switch implementations via configuration

3.2 Dependency Injection

Dependency Injection (DI) is a design pattern where dependencies are provided to an object rather than the object creating them itself. Spring supports three types of dependency injection:

3.2.1 Constructor Injection

Dependencies are injected through the constructor. This is the recommended approach as it ensures immutability and required dependencies.

@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

3.2.2 Setter Injection

Dependencies are injected through setter methods. Useful for optional dependencies.

@Service
public class NotificationService {
    private EmailService emailService;
    private SmsService smsService;
    
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
    
    @Autowired(required = false)
    public void setSmsService(SmsService smsService) {
        this.smsService = smsService;
    }
}

3.2.3 Field Injection

Dependencies are injected directly into fields using reflection. Less recommended but convenient for simple cases.

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;
}

3.3 Container Types

Spring provides two main IoC container implementations:

3.3.1 BeanFactory

The BeanFactory is the basic container that provides fundamental DI capabilities. It uses lazy initialization - beans are created only when requested.

BeanFactory factory = new XmlBeanFactory(
    new ClassPathResource("beans.xml")
);
UserService userService = factory.getBean(UserService.class);

3.3.2 ApplicationContext

The ApplicationContext extends BeanFactory and provides additional enterprise features:

  • Automatic BeanFactoryPostProcessor and BeanPostProcessor registration
  • Automatic MessageSource registration for internationalization
  • Automatic ApplicationEventPublisher registration for events
  • Eager bean initialization
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = context.getBean(UserService.class);

4. Core Features

4.1 Resource Abstraction

Spring provides a unified Resource interface for accessing various types of resources (files, URLs, classpath resources, etc.) in a consistent way.

@Service
public class FileService {
    private final ResourceLoader resourceLoader;
    
    public FileService(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
    
    public void loadFile() throws IOException {
        // Load from classpath
        Resource resource = resourceLoader.getResource("classpath:config.properties");
        
        // Load from file system
        Resource fileResource = resourceLoader.getResource("file:/path/to/file.txt");
        
        // Load from URL
        Resource urlResource = resourceLoader.getResource("https://example.com/data.json");
        
        InputStream inputStream = resource.getInputStream();
        // Process the resource
    }
}

4.2 Validation Framework

Spring Core integrates with Bean Validation (JSR-303) for declarative validation of beans.

public class User {
    @NotNull
    @Size(min = 3, max = 50)
    private String username;
    
    @Email
    @NotNull
    private String email;
    
    @Min(18)
    private int age;
    
    // getters and setters
}

@Service
public class UserService {
    private final Validator validator;
    
    public UserService(Validator validator) {
        this.validator = validator;
    }
    
    public void validateUser(User user) {
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        if (!violations.isEmpty()) {
            throw new ValidationException("Validation failed");
        }
    }
}

4.3 Data Binding

Spring's data binding allows automatic conversion and binding of request parameters to Java objects.

@Controller
public class UserController {
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        // Spring automatically binds JSON/XML to User object
        return ResponseEntity.ok(user);
    }
    
    @GetMapping("/users")
    public ResponseEntity<List<User>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        // Spring automatically converts query parameters
        return ResponseEntity.ok(/* ... */);
    }
}

4.4 Type Conversion

Spring provides a powerful type conversion system that automatically converts between different types.

@Service
public class ConversionService {
    private final ConversionService conversionService;
    
    public ConversionService(org.springframework.core.convert.ConversionService conversionService) {
        this.conversionService = conversionService;
    }
    
    public void convertTypes() {
        // String to Integer
        Integer number = conversionService.convert("123", Integer.class);
        
        // String to Date
        Date date = conversionService.convert("2024-01-01", Date.class);
        
        // Custom conversions
        String value = conversionService.convert(new CustomType(), String.class);
    }
}

4.5 Application Events

Spring's event mechanism allows components to communicate in a loosely coupled way through events.

// Define an event
public class OrderCreatedEvent extends ApplicationEvent {
    private final Order order;
    
    public OrderCreatedEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }
    
    public Order getOrder() {
        return order;
    }
}

// Publish an event
@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;
    
    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    
    public void createOrder(Order order) {
        // Save order
        // ...
        
        // Publish event
        eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
    }
}

// Listen to events
@Component
public class OrderEventListener {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        Order order = event.getOrder();
        // Send notification, update inventory, etc.
    }
}

5. Project Setup

To use Spring Core in your project, add the Spring Core dependency to your build configuration.

5.1 Gradle Configuration

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    // Spring Core
    implementation 'org.springframework:spring-core:6.1.0'
    implementation 'org.springframework:spring-context:6.1.0'
    implementation 'org.springframework:spring-beans:6.1.0'
    
    // Optional: For annotation-based configuration
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    
    // Testing
    testImplementation 'org.springframework:spring-test:6.1.0'
    testImplementation 'junit:junit:4.13.2'
}

5.2 Maven Configuration

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>6.1.0</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.1.0</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>6.1.0</version>
    </dependency>
</dependencies>

6. Configuration

Spring Core supports multiple configuration approaches: XML, annotations, and Java-based configuration.

6.1 XML Configuration

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="userRepository" class="com.example.repository.UserRepositoryImpl"/>
    
    <bean id="userService" class="com.example.service.UserService">
        <constructor-arg ref="userRepository"/>
    </bean>
</beans>

6.2 Annotation-Based Configuration

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // Configuration class
}

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}

6.3 Java-Based Configuration

@Configuration
public class AppConfig {
    @Bean
    public UserRepository userRepository() {
        return new UserRepositoryImpl();
    }
    
    @Bean
    public UserService userService() {
        return new UserService(userRepository());
    }
}

7. Real-World Examples

7.1 Example 1: Basic Service with Dependency Injection

// Repository interface
public interface UserRepository {
    User findById(Long id);
    void save(User user);
}

// Repository implementation
@Repository
public class UserRepositoryImpl implements UserRepository {
    @Override
    public User findById(Long id) {
        // Implementation
        return new User(id, "John Doe");
    }
    
    @Override
    public void save(User user) {
        // Implementation
    }
}

// Service with constructor injection
@Service
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public User getUser(Long id) {
        return userRepository.findById(id);
    }
    
    public void createUser(User user) {
        userRepository.save(user);
    }
}

7.2 Example 2: Multiple Implementations with Qualifier

public interface PaymentService {
    void processPayment(BigDecimal amount);
}

@Service("creditCardPayment")
public class CreditCardPaymentService implements PaymentService {
    @Override
    public void processPayment(BigDecimal amount) {
        // Credit card processing
    }
}

@Service("paypalPayment")
public class PayPalPaymentService implements PaymentService {
    @Override
    public void processPayment(BigDecimal amount) {
        // PayPal processing
    }
}

@Service
public class OrderService {
    private final PaymentService paymentService;
    
    public OrderService(@Qualifier("creditCardPayment") PaymentService paymentService) {
        this.paymentService = paymentService;
    }
    
    public void checkout(Order order) {
        paymentService.processPayment(order.getTotal());
    }
}

7.3 Example 3: Conditional Bean Creation

@Configuration
public class DatabaseConfig {
    @Bean
    @ConditionalOnProperty(name = "database.type", havingValue = "mysql")
    public DataSource mysqlDataSource() {
        return new MysqlDataSource();
    }
    
    @Bean
    @ConditionalOnProperty(name = "database.type", havingValue = "postgresql")
    public DataSource postgresqlDataSource() {
        return new PostgresqlDataSource();
    }
}

7.4 Example 4: Application Events

// Event
public class UserRegisteredEvent extends ApplicationEvent {
    private final User user;
    
    public UserRegisteredEvent(Object source, User user) {
        super(source);
        this.user = user;
    }
    
    public User getUser() {
        return user;
    }
}

// Publisher
@Service
public class RegistrationService {
    private final ApplicationEventPublisher eventPublisher;
    private final UserRepository userRepository;
    
    public RegistrationService(ApplicationEventPublisher eventPublisher,
                               UserRepository userRepository) {
        this.eventPublisher = eventPublisher;
        this.userRepository = userRepository;
    }
    
    public void registerUser(User user) {
        userRepository.save(user);
        eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
    }
}

// Listeners
@Component
public class EmailNotificationListener {
    @EventListener
    @Async
    public void handleUserRegistered(UserRegisteredEvent event) {
        // Send welcome email
        sendWelcomeEmail(event.getUser());
    }
}

@Component
public class AnalyticsListener {
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        // Track registration
        trackRegistration(event.getUser());
    }
}

8. Best Practices

8.1 Prefer Constructor Injection

Use constructor injection for required dependencies as it ensures immutability and makes dependencies explicit.

// Good
@Service
public class UserService {
    private final UserRepository repository;
    
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
}

// Avoid
@Service
public class UserService {
    @Autowired
    private UserRepository repository;
}

8.2 Use Interfaces for Dependencies

Depend on abstractions (interfaces) rather than concrete implementations to maintain flexibility.

// Good - depends on interface
public class OrderService {
    private final PaymentService paymentService; // interface
}

// Avoid - depends on implementation
public class OrderService {
    private final CreditCardPaymentService paymentService; // concrete class
}

8.3 Avoid Circular Dependencies

Design your components to avoid circular dependencies. If necessary, use setter injection or refactor the design.

8.4 Use @ComponentScan Wisely

Limit component scanning to specific packages to improve startup time and avoid scanning unnecessary classes.

@Configuration
@ComponentScan(basePackages = "com.example.service", "com.example.repository")
public class AppConfig {
}

8.5 Profile-Based Configuration

Use profiles to manage different configurations for different environments.

@Configuration
@Profile("dev")
public class DevConfig {
    @Bean
    public DataSource dataSource() {
        return new H2DataSource();
    }
}

@Configuration
@Profile("prod")
public class ProdConfig {
    @Bean
    public DataSource dataSource() {
        return new ProductionDataSource();
    }
}

9. Testing

Spring provides excellent testing support through the Spring TestContext framework.

9.1 Unit Testing with Mocks

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testGetUser() {
        // Given
        User expectedUser = new User(1L, "John Doe");
        when(userRepository.findById(1L)).thenReturn(expectedUser);
        
        // When
        User result = userService.getUser(1L);
        
        // Then
        assertEquals(expectedUser, result);
        verify(userRepository).findById(1L);
    }
}

9.2 Integration Testing

@SpringJUnitConfig(AppConfig.class)
class UserServiceIntegrationTest {
    @Autowired
    private UserService userService;
    
    @Test
    void testCreateUser() {
        User user = new User(null, "Jane Doe");
        userService.createUser(user);
        
        assertNotNull(user.getId());
    }
}

9.3 Testing with Test Context

@SpringBootTest
@ActiveProfiles("test")
class ApplicationIntegrationTest {
    @Autowired
    private ApplicationContext context;
    
    @Test
    void testContextLoads() {
        assertNotNull(context);
        assertTrue(context.containsBean("userService"));
    }
}

10. Advanced Concepts

10.1 Bean Scopes

Spring supports different bean scopes that control the lifecycle and visibility of beans:

  • Singleton: One instance per container (default)
  • Prototype: New instance every time
  • Request: One instance per HTTP request (web only)
  • Session: One instance per HTTP session (web only)
  • Application: One instance per ServletContext (web only)
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeBean {
    // New instance created each time
}

@Component
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class RequestScopedBean {
    // One instance per HTTP request
}

10.2 Bean Post Processors

BeanPostProcessor allows you to intercept and modify beans before and after initialization.

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        // Called before @PostConstruct
        return bean;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // Called after @PostConstruct
        if (bean instanceof Auditable) {
            ((Auditable) bean).setCreatedAt(LocalDateTime.now());
        }
        return bean;
    }
}

10.3 Factory Beans

FactoryBean allows you to create complex objects that require custom initialization logic.

public class ConnectionFactoryBean implements FactoryBean<Connection> {
    private String url;
    private String username;
    private String password;
    
    @Override
    public Connection getObject() throws Exception {
        return DriverManager.getConnection(url, username, password);
    }
    
    @Override
    public Class<?> getObjectType() {
        return Connection.class;
    }
    
    @Override
    public boolean isSingleton() {
        return false;
    }
    
    // setters for url, username, password
}

11. Conclusion

Spring Core is the foundation of the Spring Framework, providing essential IoC and DI capabilities that enable loose coupling, testability, and maintainability in Java applications. Understanding Spring Core is crucial for mastering the Spring ecosystem.

The IoC container, dependency injection mechanisms, resource abstraction, and event system form the backbone of Spring's architecture. By following best practices like constructor injection, interface-based design, and proper configuration, you can build robust and maintainable applications.

As you progress, you'll see how Spring Core integrates seamlessly with other Spring modules like Spring Beans, Spring Context, and Spring AOP, providing a comprehensive framework for enterprise Java development.

Post a Comment

0 Comments