Module 36 - Spring Core
The foundation that every other Spring module builds on: IoC container - Spring creates and manages object instances; Dependency Injection - Spring wires dependencies automatically; Bean lifecycle - hooks to run code at startup and shutdown; Bean scopes - singleton vs prototype instance policies; AOP - cross-cutting concerns (logging, timing, security) without modifying business code.
IoC Container
Traditional code Spring IoC
──────────────────────────────────── ─────────────────────────────────────────
UserService s = new UserService( ApplicationContext ctx = SpringApplication.run(...)
new InMemoryUserRepository()); UserService s = ctx.getBean(UserService.class);
You control object creation. Spring controls object creation.
You wire dependencies manually. Spring wires dependencies automatically.
Hard to swap implementations. Swap by changing @Primary or @Qualifier.
Hard to test (new = tight coupling). Easy to test: inject a mock instead.
The IoC container reads your @Component, @Service, @Repository, @Controller, and @Bean declarations, instantiates each class, resolves their dependencies, and stores the result in an internal registry called the ApplicationContext.
Dependency Injection Styles
Constructor Injection (preferred)
@Service
public class UserService {
private final UserRepository userRepository; // final → immutable
// @Autowired optional when exactly one constructor (Spring 4.3+)
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
Pros:
Dependency is explicit and mandatory - cannot construct without it
Field can be final - immutability guaranteed
Class is testable without Spring: new UserService(new FakeRepo())
Cons:
Verbose for many dependencies (use @RequiredArgsConstructor from Lombok)
Setter Injection
@Service
public class ReportService {
private NotificationService notificationService;
@Autowired
@Qualifier("smsNotificationService")
public void setNotificationService(NotificationService svc) {
this.notificationService = svc;
}
}
Use when: dependency is optional, or you need to swap it after construction.
Avoid for: mandatory dependencies (field stays null until setter is called).
Field Injection (avoid in production code)
@Service
public class SomeService {
@Autowired // NOT recommended
private UserRepository repo;
}
Problems:
Cannot be final - mutability risk
Cannot test without Spring (no constructor or setter to inject a mock)
Hidden dependency - not visible in the constructor signature
@Primary and @Qualifier
Problem: Two beans implement the same interface.
Spring doesn't know which one to inject - ambiguous dependency.
Solution 1 - @Primary: mark one implementation as the default.
Solution 2 - @Qualifier: specify the exact bean name at the injection point.
@Service @Primary
public class EmailNotificationService implements NotificationService { ... }
@Service
public class SmsNotificationService implements NotificationService { ... }
// Injection - @Primary wins by default:
@Autowired
NotificationService notification; // → EmailNotificationService
// @Qualifier overrides @Primary:
@Autowired
@Qualifier("smsNotificationService") // bean name = class name camelCased
NotificationService smsOnly; // → SmsNotificationService
@Configuration and @Bean
// Use @Configuration + @Bean when:
// - The class is from a library (can't add @Component to it)
// - You need to configure the bean before registration
// - You want wiring logic to be explicit and centralized
@Configuration
public class AppConfig {
@Bean
public AppProperties appProperties() {
return new AppProperties("My App", "1.0.0", 100);
}
}
// AppProperties is now a Spring-managed singleton - @Autowired works anywhere:
@Autowired AppProperties props;
@Configuration uses CGLIB proxying:
appProperties() ← called from another @Bean method
→ Spring intercepts the call and returns the existing singleton
→ NOT a second new AppProperties() instance
Bean Lifecycle
1 Constructor called - Spring instantiates the bean
2 Dependencies injected - @Autowired fields/setters populated
3 @PostConstruct - your initialization code runs
4 Bean in active use - handles application calls
5 @PreDestroy - cleanup code runs when context shuts down
6 Object garbage collected
@Service
public class AuditService {
@PostConstruct
void init() {
// Runs after all dependencies are injected.
// Safe to call injected beans here.
// Use for: opening connections, loading config, warming caches
}
@PreDestroy
void shutdown() {
// Runs when ApplicationContext.close() is called.
// Use for: closing connections, flushing buffers, releasing resources
}
}
Note: Spring does NOT call @PreDestroy on prototype-scoped beans.
The caller is responsible for prototype bean lifecycle management.
Bean Scopes
Singleton (default)
One instance per ApplicationContext.
All injection points receive the same object.
State mutations are visible across the whole application.
@Component // default scope = singleton
public class SingletonCounter {
private int count = 0;
public void increment() { count++; }
}
// Two injections - same object:
@Autowired SingletonCounter a;
@Autowired SingletonCounter b;
a.increment();
b.getCount(); // → 1 (a and b are the same instance)
Prototype
New instance every time the bean is requested from the context.
State is isolated per instance.
@PreDestroy NOT called by Spring.
@Component
@Scope("prototype")
public class PrototypeTask {
private String status = "NEW";
public void execute() { status = "DONE"; }
}
// ObjectProvider - correct way to get prototypes inside a singleton:
@Autowired ObjectProvider<PrototypeTask> taskProvider;
PrototypeTask t1 = taskProvider.getObject();
PrototypeTask t2 = taskProvider.getObject();
t1.execute();
t1.getStatus(); // "DONE"
t2.getStatus(); // "NEW" - separate instance, isolated state
Scope Instances State sharing Use case
─────────────────────────────────────────────────────────────────────────
singleton 1 Shared Services, repositories, configs
prototype N (1/req) Isolated Stateful tasks, mutable helpers
request 1/HTTP req Per-request Web: request-scoped data
session 1/HTTP ses Per-session Web: shopping cart, user prefs
AOP - Aspect-Oriented Programming
AOP separates cross-cutting concerns (logging, timing, transactions, security) from business logic. Instead of adding log statements inside every method, one aspect intercepts all matching methods automatically.
Concepts
Join point - a point in execution (method call, field access, exception)
Spring AOP supports only method execution join points.
Pointcut - an expression that matches a set of join points.
Advice - code to run at a join point: @Before, @After, @Around, etc.
Aspect - class that holds pointcuts + advice (@Aspect).
Proxy - Spring wraps the target bean in a CGLIB proxy.
Advice fires when the proxy's method is called.
Internal this.method() calls bypass the proxy - no advice.
Pointcut Expression Syntax
execution( [modifier] returnType [declaring-type].methodName(params) )
execution(* com.example.service.*.*(..))
* - any return type
com…service.* - any class in the service package
.* - any method name
(..) - any parameters
execution(public String com.example.UserService.find*(String))
- public methods returning String, starting with "find",
taking one String parameter, in UserService
Advice Types
@Aspect @Component
public class LoggingAspect {
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
// Runs before the method - cannot prevent execution
@Before("userServiceMethods()")
public void logBefore(JoinPoint jp) {
// jp.getSignature().getName() → method name
// jp.getArgs() → method arguments
}
// Runs after successful return - binds the return value
@AfterReturning(pointcut = "userServiceMethods()", returning = "result")
public void logAfterReturning(JoinPoint jp, Object result) { }
// Runs when the method throws - does NOT suppress the exception
@AfterThrowing(pointcut = "userServiceMethods()", throwing = "ex")
public void logAfterThrowing(JoinPoint jp, Throwable ex) { }
// Runs always, after return or throw (like finally)
@After("userServiceMethods()")
public void logAfter(JoinPoint jp) { }
// Wraps the entire call - can modify args, return value, suppress exception
@Around("execution(* com.example.service.ReportService.*(..))")
public Object timeAround(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
Object result = pjp.proceed(); // calls the actual method
log.info("elapsed: {}ns", System.nanoTime() - start);
return result;
}
}
Advice Execution Order
Normal return:
@Around (before proceed)
@Before
→ target method executes
@AfterReturning
@After
@Around (after proceed)
Exception thrown:
@Around (before proceed)
@Before
→ target method throws
@AfterThrowing
@After
@Around rethrows (unless it catches and swallows)
AOP Proxy Limitation
// PROBLEM - internal call bypasses the proxy:
@Service
public class OrderService {
public void placeOrder(...) {
validate(...); // this.validate() - NOT intercepted by AOP
}
public void validate(...) { ... }
}
// SOLUTION - inject self-reference OR move the method to a separate bean:
@Service
public class OrderValidator {
public void validate(...) { ... } // called from outside → intercepted
}
Module 36 - What Was Built
module-36-spring-core/
├── pom.xml (Spring Boot 3.3.5, spring-boot-starter, spring-boot-starter-aop,
│ spring-boot-starter-test)
└── src/
├── main/java/com/javatraining/springcore/
│ ├── SpringCoreApplication.java
│ ├── config/
│ │ ├── AppConfig.java - @Configuration + @Bean registration
│ │ └── AppProperties.java - record registered as a Spring bean
│ ├── repository/
│ │ ├── UserRepository.java - interface
│ │ └── InMemoryUserRepository.java - @Repository implementation
│ ├── service/
│ │ ├── UserService.java - constructor injection (preferred style)
│ │ ├── NotificationService.java - interface with two implementations
│ │ ├── EmailNotificationService.java - @Primary default
│ │ ├── SmsNotificationService.java - @Qualifier("smsNotificationService")
│ │ ├── ReportService.java - setter injection + @Qualifier
│ │ └── OrderService.java - throws to trigger @AfterThrowing
│ ├── lifecycle/
│ │ └── AuditService.java - @PostConstruct / @PreDestroy
│ ├── scope/
│ │ ├── SingletonCounter.java - default singleton scope
│ │ └── PrototypeTask.java - @Scope("prototype")
│ └── aop/
│ └── LoggingAspect.java - @Before, @AfterReturning, @AfterThrowing,
│ @After, @Around with named @Pointcut
└── test/java/com/javatraining/springcore/
├── DependencyInjectionTest.java 5 tests - constructor injection, @Primary, @Qualifier
├── BeanConfigTest.java 3 tests - @Configuration/@Bean, context lookup, singleton
├── BeanLifecycleTest.java 4 tests - @PostConstruct, usability after init
├── BeanScopeTest.java 5 tests - singleton identity, prototype isolation,
│ ObjectProvider
└── AopTest.java 9 tests - all five advice types, ordering,
exception propagation
All tests: 26 passing.
Key Takeaways
IoC Spring creates beans; you declare what you need
Constructor DI Preferred: explicit, final fields, testable without Spring
@Primary Default when multiple beans match the interface
@Qualifier Overrides @Primary - selects by bean name
@Configuration Explicit bean registration for third-party or parameterized beans
@PostConstruct Runs after injection - open connections, warm caches
@PreDestroy Runs on context close - close resources
Prototype @Scope("prototype") - fresh instance per getObject() call
ObjectProvider Correct way to fetch prototypes from inside a singleton
AOP Proxy CGLIB proxy wraps the bean - advice fires on external calls only
@Before Runs before; cannot stop execution
@AfterReturning Runs on success; can inspect return value
@AfterThrowing Runs on exception; does NOT suppress it
@After Runs always (like finally)
@Around Full control: proceed(), modify return, suppress exception