Spring Core (IoC & AOP)
Inversion of Control (IoC)
In traditional Java, objects create their own dependencies (new MyService()). IoC inverts this: a container creates and wires objects. You describe components; Spring assembles the graph.
Benefits:
- Loose coupling — depend on interfaces, not implementations
- Testability — inject mocks in tests
- Centralized configuration — change wiring without code changes
Spring Container
The ApplicationContext is Spring’s IoC container:
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
UserService service = ctx.getBean(UserService.class);
In Spring Boot, the container starts automatically — you rarely interact with it directly.
Defining Beans
Component Scanning
@Configuration
@ComponentScan("com.example.app")
public class AppConfig {}
Classes annotated with stereotypes are auto-registered:
@Service
public class UserService {
// business logic
}
@Repository
public class JpaUserRepository implements UserRepository {
// data access
}
@Controller
public class UserController {
// web layer
}
Java Configuration
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
ds.setUsername("user");
ds.setPassword("pass");
return ds;
}
@Bean
public UserRepository userRepository(DataSource dataSource) {
return new JdbcUserRepository(dataSource);
}
}
@Bean methods define factory-created objects. Method parameters are auto-injected.
Dependency Injection
Constructor Injection (Recommended)
@Service
public class OrderService {
private final OrderRepository repository;
private final PaymentGateway gateway;
public OrderService(OrderRepository repository, PaymentGateway gateway) {
this.repository = repository;
this.gateway = gateway;
}
public Order placeOrder(OrderRequest req) {
Order order = repository.save(new Order(req));
gateway.charge(order.getTotal());
return order;
}
}
Constructor injection makes dependencies explicit and enables final fields. Spring 4.3+ auto-wires single-constructor beans without @Autowired.
Interface-Based Design
public interface NotificationService {
void send(String to, String message);
}
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
// send email
}
}
Inject the interface — swap implementations via configuration or profiles.
Bean Scopes
| Scope | Description |
|---|---|
singleton |
One instance per container (default) |
prototype |
New instance per request |
request |
One per HTTP request (web apps) |
session |
One per HTTP session (web apps) |
@Component
@Scope("prototype")
public class ReportGenerator { }
Default singleton is correct for stateless services. Use prototype for stateful or per-operation objects.
Bean Lifecycle
@Component
public class CacheWarmer {
@PostConstruct
public void init() {
// runs after dependency injection
System.out.println("Warming cache...");
}
@PreDestroy
public void cleanup() {
// runs before container shutdown
System.out.println("Releasing resources...");
}
}
Implement InitializingBean / DisposableBean only if you need framework-level hooks — annotations are preferred.
Qualifiers and Primary
When multiple beans implement the same interface:
@Service
@Primary
public class StripePaymentGateway implements PaymentGateway { }
@Service
@Qualifier("paypal")
public class PayPalPaymentGateway implements PaymentGateway { }
@Service
public class CheckoutService {
private final PaymentGateway defaultGateway;
private final PaymentGateway paypalGateway;
public CheckoutService(
PaymentGateway defaultGateway,
@Qualifier("paypal") PaymentGateway paypalGateway) {
this.defaultGateway = defaultGateway;
this.paypalGateway = paypalGateway;
}
}
@Primary marks the default; @Qualifier selects by name.
Profiles
@Service
@Profile("dev")
public class MockEmailService implements EmailService {
@Override
public void send(String to, String body) {
System.out.println("DEV: Would send to " + to);
}
}
@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
@Override
public void send(String to, String body) {
// real SMTP
}
}
Activate: spring.profiles.active=dev
Aspect-Oriented Programming (AOP)
Separate cross-cutting concerns from business logic:
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(Timed)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " took " + elapsed + "ms");
return result;
}
}
@Service
public class ReportService {
@Timed
public Report generate() {
// business logic only — no logging boilerplate
}
}
Common AOP uses: logging, transaction management (@Transactional), security checks, caching (@Cacheable).
Conditional Beans
@Bean
@ConditionalOnProperty(name = "feature.new-checkout", havingValue = "true")
public CheckoutServiceV2 checkoutServiceV2() {
return new CheckoutServiceV2();
}
Spring Boot uses conditions extensively for auto-configuration.
Testing the Container
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
class UserServiceTest {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
void findsUser() {
when(userRepository.findById(1L)).thenReturn(Optional.of(new User("Alice")));
assertEquals("Alice", userService.getName(1L));
}
}
@MockBean replaces a container bean with a Mockito mock.
Best Practices
- Program to interfaces; inject implementations
- Constructor injection with
finalfields - Keep
@Configurationclasses focused — one per concern - Avoid circular dependencies — redesign if they appear
- Use profiles for environment-specific beans
Spring Core is the foundation — every other Spring project builds on the IoC container and bean lifecycle.