Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Core Architecture and Internal Mechanics of the Spring Framework

Tech 1

Inversion of Control (IoC) represents a fundamental design principle where the responsibility for creating and managing component instances is delegated to a central container. This approach decouples components by removing the need for objects to instantiate their dependencies directly. The container holds the authority over instantiation and lifecycle management, while the objects managed within this environment are referred to as beans.

Aspect-Oriented Programming (AOP) complements IoC by modularizing cross-cutting concerns. These concerns, such as logging, transaction management, security checks, and rate limiting, often scatter across multiple classes. Implementing them repeatedly leads to code duplication and maintenance challenges. AOP extracts these behaviors into separate aspects.

Spring AOP utilizes dynamic proxies. If a target object implements an interface, the framework employs JDK dynamic proxies. Otherwise, it generates a subclass using CGLIB. At runtime, the proxy intercepts method calls to execute aspect logic defined separately from the business core.

Key AOP terminology includes:

  • Target: The original object being advised.
  • Joinpoint: Any point in the execution flow, typically a method execution, that can be intercepted.
  • Pointcut: A predicate that matches specific joinpoints where advice should apply.
  • Advice: The action taken by an aspect at a particular joinpoint.
  • Aspect: The modularization of a concern, encapsulating pointcuts and advice.

Pointcut expressions, such as execution() for method signatures or @annotation() for specific annotations, define the scope of enhancement.

Bean Lifecycle and Management

The lifecycle of a singleton bean within the container follows a specific sequence:

  1. Instantiation: The container calls createBeanInstance(), using reflection to invoke the constructor.
  2. Population: populateBean() handles property assignment and dependency injection, addressing circular references if present.
  3. Aware Interfaces: If the bean implements interfaces like ApplicationContextAware, the corresponding setter methods are invoked.
  4. Pre-Initialization: Methods annotated with @PostConstruct are executed.
  5. Before Initialization: BeanPostProcessor.postProcessBeforeInitialization is called.
  6. Initializing: If the bean implements InitializingBean, the afterPropertiesSet method runs. Custom init methods defined via @Bean(initMethod = "...") are also executed here.
  7. After Initialization: BeanPostProcessor.postProcessAfterInitialization is invoked, often used for wrapping beans with proxies.
  8. Usage: The bean is ready for use.
  9. Destruction: Upon container shutdown, custom destroy methods (@Bean(destroyMethod = "...")) and DisposableBean.destroy() are called.

Dependency Injection Strategies

Three primary methods exist for injecting dependencies:

  • Field Injection: Simple to implement but prevents the use of final fields and complicates unit testing. Generally discouraged.
  • Constructor Injection: Allows dependencies to be immutable (final) and ensures the object is fully initialized upon creation. It also helps detect circular dependencies early. However, excessive parameters can make constructors unwieldy.
  • Setter Injection: Offers flexibility for optional dependencies but does not support immutability.

Best practice suggests using constructor injection for mandatory dependencies and setter injection for optional ones.

Thread Safety in Beans

Thread safety depends on the bean's scope and state:

  • Prototype Scope: A new instance is created for every request, eliminating shared state issues.
  • Singleton Scope: A single instance is shared. If the bean is stateless (no mutable member variables), it is thread-safe. If it maintains state (mutable variables modified during execution), concurrency issues may arise.

Mitigation strategies for stateful singletons include avoiding mutable fields, using ThreadLocal variables, aplying synchronization locks, or switching to prototype scope.

Spring MVC Overview

Spring MVC serves as the presentation layer framework, functioning similarly to Servlet technology but providing a more structured approach to handling web requests.

Key Annotations

@Component vs @Bean

@Component marks a class as a managed bean, often specialized by @Repository, @Service, or @Controller. @Bean is used within @Configuration classes to define bean creation logic explicitly. @Bean is necessary for third-party classes where source code cannot be modified to add annotations.

@Autowired vs @Resource

@Autowired is Spring-specific and defaults to type-based injection. @Resource is part of JSR-250 (JDK) and defaults to name-based injection. Both can be forced to match by name (@Qualifier for @Autowired, name attribute for @Resource). @Autowired supports constructors and parameters, whereas @Resource is limited to fields and setter methods.

@Scope

Defines the lifecycle of a bean:

  • singleton: One instance per container (default).
  • prototype: New instance per request.
  • request: One instance per HTTP request.
  • session: One instance per HTTP session.

Lifecycle Hooks

@PostConstruct and @PreDestroy are standard Java annotations. The former runs after dependency injection for initialization logic, while the latter executes before the bean is removed from the container.

Implementing a Factory Pattern with Context Awarenes

The following example demonstrates a notification system where specific notifier implementations are registered dynamically using ApplicationContextAware.

Abstract Base Class:

package com.example.notification;

import org.springframework.stereotype.Component;

@Component
public abstract class BaseNotifier {
    
    public abstract String getChannel();
    
    public void sendNotification(String message) {
        System.out.println("Sending via base notifier: " + message);
    }
}

Concrete Implementations:

package com.example.notification;

import org.springframework.stereotype.Component;

@Component
public class EmailNotifier extends BaseNotifier {
    
    @Override
    public String getChannel() {
        return "EMAIL";
    }
    
    @Override
    public void sendNotification(String message) {
        super.sendNotification(message);
        System.out.println("Email sent: " + message);
    }
}
package com.example.notification;

import org.springframework.stereotype.Component;

@Component
public class SmsNotifier extends BaseNotifier {
    
    @Override
    public String getChannel() {
        return "SMS";
    }
    
    @Override
    public void sendNotification(String message) {
        super.sendNotification(message);
        System.out.println("SMS sent: " + message);
    }
}

Registry Factory:

package com.example.notification;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class NotificationRegistry implements ApplicationContextAware {

    private static final Map<String, BaseNotifier> registry = new HashMap<>();
    
    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        Map<String, BaseNotifier> beans = context.getBeansOfType(BaseNotifier.class);
        beans.forEach((key, notifier) -> registry.put(notifier.getChannel(), notifier));
    }
    
    public static BaseNotifier getNotifier(String channel) {
        return registry.get(channel);
    }
}

Verification Test:

package com.example.notification;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
class NotificationRegistryTest {
    
    @Test
    void verifyNotificationChannels() {
        BaseNotifier email = NotificationRegistry.getNotifier("EMAIL");
        email.sendNotification("Test Message");
        
        System.out.println("----------");
        
        BaseNotifier sms = NotificationRegistry.getNotifier("SMS");
        sms.sendNotification("Test Message");
        
        assertTrue(true);
    }
}

Common Transaction Pitfalls

Transactional management may fail under specific conditions:

  • The underlying database engine does not support transactions (e.g., MySQL MyISAM).
  • The class containing the transactional method is not managed by Spring (missing stereotype annotations).
  • The method visibility is not public, as @Transactional typically requires public exposure unless AspectJ is used.
  • Self-invocation occurs within the same class, bypassing the proxy. Solutions include injecting the bean into itself or using AopContext.currentProxy().
  • Exceptions are caught and swallowed within the method, preventing the transaction manager from triggering a rollback.

Circular Dependencies and Three-Level Cache

Spring resolves circular dependencies for singleton beans using a three-level cache mechanism:

  1. Level 1 (singletonObjects): Stores fully initialized and ready-to-use beans.
  2. Level 2 (earlySingletonObjects): Stores raw bean instances exposed early to resolve dependencies.
  3. Level 3 (singletonFactories): Stores factories capable of creating bean instances, potentially wrapped with proxies.

Resolution Flow: When Bean A depends on Bean B, and Bean B depends on Bean A:

  1. A is instantiated but not fully initialized. A factory for A is stored in Level 3.
  2. A requires B. B is instantiated. A factory for B is stored in Level 3.
  3. B requires A. The factory for A in Level 3 is invoked to retrieve an early reference to A (potentially proxied), which is moved to Level 2.
  4. B receives the reference to A, completes initialization, and moves to Level 1.
  5. A receives the reference to B, completes initialization, and moves to Level 1.

Purpose of the Third Level: While two levels could theoretically resolve simple circular references, the third level is crucial for handling AOP proxies. Spring aims to create proxies only after initialization is complete. However, if a circular dependency exists involving a bean that requires proxying, the proxy must be created early to ensure the depandent bean injects the proxy rather than the raw object. The factory in Level 3 allows this proxy creation to be deferred until absolutely necessary, maintaining consistency in the bean lifecycle.

Tags: spring

Related Articles

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.