The Spring ecosystem provides comprehensive infrastructure for building Java applications. It primarily consists of:
- Spring Framwork - The core framework
- Spring Boot - Adds auto-configuration capabilities that initialize resources based on conventions during startup
- Spring MVC - Web framework for building RESTful applications
- Spring Cloud - Distributed system extensions for Spring Boot
At its core, Spring is a lightweight open-source framework built on two fundamental concepts:
- IOC (Inversion of Control)
- AOP (Aspect-Oriented Programming)
Inversion of Control (IOC)
Core Concept
Inversion of Control is a design principle where the control of object creation and lifecycle is transferred from the application code to a container. In Spring, this responsibility falls to the IOC container.
The primary benefit of IOC is reduced coupling between components. The underlying implementation relies on XML configuration files, reflection, and the simple factory pattern. The container reads configuration metadata (bean identifiers and class names), uses reflection to instantiate objects, and manages their lifecycle.
IOC Container Implementations
Spring provides two main interfaces for the IOC container:
- BeanFactory Interface - The basic implementation with minimal features. It uses lazy loading, meaning objects are created only when first requested. Not typically used directly by developers.
- ApplicationContext Interface - A more feature-rich implementation that extends BeanFactory. It eagerly loads and initializes all beans during startup, resulting in slower startup but faster runtime performance.
Bean Management
The Spring container manages beans through two primary approaches:
XML Configuration
- Creating Objects: Define beans in XML configuration files
- Dependency Injection (DI): Configure properties through constructor injection or setter injection. Supports simple values, references to other beans, inner beans, and collections.
Annotation-Based Configuration
- Creating Objects: Use @Component, @Service, @Repository, or @Controller annotations on classes after configuring component scanning in XML
- Dependency Injection: Use annotations like @Autowired, @Value, @Qualifier, or @Resource on properties
Relationship Between IOC and DI
IOC represents the broader concept of transferring control to the container, while DI is the specific mechanism used to implement object property assignment. The container uses reflection combined with the factory pattern to create objects and DI to populate their dependencies.
Bean Scopes
Beans can be configured with different scopes:
- Singleton (default): One instance per container
- Prototype: New instance each time requested
- Request: One instance per HTTP request
- Session: One instance per HTTP session
Singleton Thread Safety
Singleton beens are thread-safe when they're stateless (no instance variables). For stateful beans, either use the prototype scope or implement thread-local storage to ensure thread safety within singleton beans.
Bean Lifecycle
The typical bean lifecycle follows these stages:
- Instantiation
- Property Injection
- BeanPostProcessor's before initialization method
- Initialization (via InitializingBean.afterPropertiesSet() or custom init method)
- BeanPostProcessor's after initialization method
- Ready for use
- Destruction (via DisposableBean.destroy() or custom destroy method)
Extended lifecycle includes:
- BeanFactoryPostProcessor manipulation of bean definitions
- InstantiationAwareBeanPostProcessor hooks before and after instantiation
- Aware interface callbacks (BeanNameAware, BeanFactoryAware, ApplicationContextAware)
Circular Dependency Resolution
Spring resolves circular dependencies using a three-level cache system:
- Level 1 Cache (singletonObjects): Stores fully initialized beans
- Level 2 Cache (earlySingletonObjects): Stores early exposed bean objects
- Level 3 Cache (singletonFactories): Stores bean factories that can create early references
When no circular dependency exists, only the Level 1 cache is used. With circular dependencies but no AOP, the Level 2 cache is utilized. When circular dependencies involve AOP proxy objects, the Level 3 cache is required to handle the proxy creation process.
Aspect-Oriented Programming (AOP)
Core Concept
AOP is a programing paradigm that separates cross-cutting concerns (like logging, transaction management, security) from business logic. This separation reduces coupling and improves code reusability.
Proxy Patterns in AOP
Static Proxy
Static proxies involve creating a proxy class at compile time that implements the same interface as the target class. The proxy wraps the target object and can add functionality before or after method calls.
Dynamic Proxy
Dynamic proxies create proxy classes at runtime. Spring primarily uses two types:
- JDK Dynamic Proxy: Requires target classes to implement interfaces. Uses reflection and the java.lang.reflect.Proxy class.
- CGLIB Proxy: Works with classes without interfaces. Creates subclasses at runtime using bytecode manipulation. Cannot proxy final classes or methods.
Performance comparison: CGLIB has slower creation but faster invocation. Spring defaults to JDK proxies but switches to CGLIB when working with classes that don't implement interfaces.
Common Design Patterns in Spring
- Simple Factory Pattern: Used in IOC for bean creation
- Singleton Pattern: Applied to bean scopes
- Dynamic Proxy Pattern: Fundamental to AOP implementation
AOP Implementation
Key Terminology
- Join Point: Points in the execution of a program (typically method calls)
- Pointcut: Predicate that matches join points where advice should be applied
- Advice: Action taken by an aspect at a particular join point
- Aspect: Module encapsulating pointcuts and advice
Advice Types
- Before Advice: Executes before the join point (@Before)
- After Returning Advice: Executes after the join point completes normally (@AfterReturning)
- After Throwing Advice: Executes after the join point throws an exception (@AfterThrowing)
- After (Finally) Advice: Executes after the join point, regardless of outcome (@After)
- Around Advice: Surrounds the join point, controlling when and how it's executed (@Around)
Implementation Approaches
XML Configuration
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="dataAccessOperation"
expression="execution(* com.example.dao.*.*(..))"/>
<aop:aspect ref="loggingAspect">
<aop:before pointcut-ref="dataAccessOperation"
method="logMethodEntry"/>
<aop:after pointcut-ref="dataAccessOperation"
method="logMethodExit"/>
</aop:aspect>
</aop:config>
</beans>
Annotation-Based Configuration
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Entering: " + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("Exiting: " + joinPoint.getSignature().getName() +
" with result: " + result);
}
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Method " + joinPoint.getSignature().getName() +
" execution started");
try {
Object result = joinPoint.proceed();
System.out.println("Method " + joinPoint.getSignature().getName() +
" execution completed");
return result;
} catch (Exception e) {
System.out.println("Method " + joinPoint.getSignature().getName() +
" execution failed with: " + e.getMessage());
throw e;
}
}
}
Spring Transaction Management
Transaction Management Approaches
- Programmatic Transaction Management: Explicit transaction management using TransactionTemplate or PlatformTransactionManager. Provides fine-grained control but is invasive to business logic.
- Declarative Transaction Management: Built on AOP, applying transactional behavior through configuration or annotations. Non-intrusive and recommended for most use cases.
Declarative transaction management is generally preferred as it keeps business logic clean of transaction management code. The only limitation is that it operates at the method level, while programmatic management can handle code blocks.
Transaction Propagation
Transaction propagation defines how transactions behave when methods are nested. Spring supports several propagation behaviors:
- REQUIRED (default): If an existing transaction is present, join it. Otherwise, create a new one.
- REQUIRES_NEW: Suspend the current transaction (if any), create a new one, and resume the original transaction upon completion.
- SUPPORTS: If an existing transaction is present, join it. Otherwise, execute non-transactionally.
- NOT_SUPPORTED: Execute non-transactionally, suspending any existing transaction.
- NEVER: Execute non-transactionally, throwing an exception if an existing transaction is present.
- MANDATORY: Require an existing transaction, throwing an exception if none exists.
NESTED: Execute within a nested transaction if an existing transaction is present, creating a savepoint for partial rollback capabilities.
These propagation rules answer the fundamental question of whether to start a new transaction, join an existing one, or execute without transaction context when a transactional method is invoked.