Spring IoC¶
What is IoC?¶
Inversion of Control is a design principle where the control of object creation and lifecycle is transferred from the application code to a container/framework.
Dependency Injection Types¶
1. Constructor Injection (Recommended)¶
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
// Constructor injection - immutable, testable
@Autowired // Optional in Spring 4.3+ if single constructor
public OrderService(PaymentService paymentService,
InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public void processOrder(Order order) {
inventoryService.reserve(order.getItems());
paymentService.charge(order.getPayment());
}
}
Advantages:
- Fields can be final (immutability)
- Clear dependencies at construction
- Easy to test (pass mocks via constructor)
- Fails fast if dependency missing
2. Setter Injection¶
@Service
public class NotificationService {
private EmailService emailService;
private SmsService smsService;
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired(required = false) // Optional dependency
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}
}
Use when: Dependencies are optional or need to be changed at runtime.
3. Field Injection (Avoid)¶
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Hard to test!
@Autowired
private PasswordEncoder passwordEncoder;
}
Problems:
- Cannot make fields final
- Hard to test without Spring context
- Hides dependencies
- Can lead to circular dependencies
Spring Container¶
ApplicationContext vs BeanFactory¶
Creating Context¶
// Java-based configuration
@Configuration
@ComponentScan("com.example")
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:postgresql://localhost/mydb");
ds.setUsername("user");
ds.setPassword("pass");
return ds;
}
}
// Bootstrap
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
OrderService orderService = context.getBean(OrderService.class);
Bean Definition¶
Stereotype Annotations¶
@Component // Generic component
@Service // Business logic layer
@Repository // Data access layer (adds exception translation)
@Controller // Web MVC controller
@RestController // @Controller + @ResponseBody
@Configuration // Configuration class (contains @Bean methods)
@Bean vs @Component¶
// @Component - Class-level, auto-detected via component scanning
@Component
public class MyService {
// Spring creates and manages this bean
}
// @Bean - Method-level in @Configuration class
@Configuration
public class AppConfig {
@Bean
public ThirdPartyService thirdPartyService() {
// Use for classes you don't own
return new ThirdPartyService(apiKey());
}
@Bean
public String apiKey() {
return System.getenv("API_KEY");
}
}
Bean Scopes¶
Scope Examples¶
@Component
@Scope("singleton") // Default - can omit
public class CacheService {
// Single instance shared across application
}
@Component
@Scope("prototype")
public class ShoppingCart {
// New instance each time injected or requested
}
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
// One per HTTP request
}
Prototype in Singleton Problem¶
@Service
public class OrderProcessor {
// WRONG: Prototype injected once into singleton
@Autowired
private ShoppingCart cart; // Same instance always!
// SOLUTION 1: ObjectFactory
@Autowired
private ObjectFactory<ShoppingCart> cartFactory;
public void process() {
ShoppingCart cart = cartFactory.getObject(); // New each time
}
// SOLUTION 2: Provider (JSR-330)
@Autowired
private Provider<ShoppingCart> cartProvider;
public void process() {
ShoppingCart cart = cartProvider.get(); // New each time
}
// SOLUTION 3: Scoped Proxy
@Autowired
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
private ShoppingCart cart; // Proxy delegates to new instance
}
Bean Lifecycle¶
Lifecycle Callbacks¶
@Service
public class CacheService {
private Cache cache;
// Preferred: JSR-250 annotations
@PostConstruct
public void init() {
cache = new ConcurrentHashMap<>();
loadInitialData();
}
@PreDestroy
public void cleanup() {
cache.clear();
persistToDisk();
}
}
// Alternative: Interface-based
@Service
public class ConnectionPool implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// Initialize connections
}
@Override
public void destroy() throws Exception {
// Close connections
}
}
// Alternative: @Bean attributes
@Configuration
public class AppConfig {
@Bean(initMethod = "init", destroyMethod = "cleanup")
public ResourceManager resourceManager() {
return new ResourceManager();
}
}
Qualifiers and Primary¶
Multiple Beans of Same Type¶
public interface PaymentGateway {
void process(Payment payment);
}
@Service
@Primary // Default when multiple beans exist
public class StripeGateway implements PaymentGateway {
@Override
public void process(Payment payment) { }
}
@Service
@Qualifier("paypal")
public class PayPalGateway implements PaymentGateway {
@Override
public void process(Payment payment) { }
}
// Usage
@Service
public class PaymentService {
@Autowired
private PaymentGateway gateway; // Gets StripeGateway (@Primary)
@Autowired
@Qualifier("paypal")
private PaymentGateway paypalGateway; // Gets PayPalGateway
}
Custom Qualifiers¶
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Premium {
}
@Service
@Premium
public class PremiumSupportService implements SupportService {
}
@Service
public class CustomerService {
@Autowired
@Premium
private SupportService supportService;
}
Profiles¶
// Activate specific beans based on environment
@Configuration
@Profile("development")
public class DevConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
}
@Configuration
@Profile("production")
public class ProdConfig {
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl(System.getenv("DATABASE_URL"));
return ds;
}
}
// Activate profile
// application.properties: spring.profiles.active=production
// Command line: --spring.profiles.active=production
// Programmatic:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.setAdditionalProfiles("production");
app.run(args);
}
}
Profile Expressions¶
@Profile("!production") // NOT production
@Profile("development | test") // development OR test
@Profile("cloud & aws") // cloud AND aws
Conditional Beans¶
@Configuration
public class ConditionalConfig {
@Bean
@ConditionalOnProperty(name = "cache.enabled", havingValue = "true")
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager noOpCacheManager() {
return new NoOpCacheManager();
}
@Bean
@ConditionalOnClass(name = "com.redis.RedisClient")
public CacheManager redisCacheManager() {
return new RedisCacheManager();
}
}
Common Conditional Annotations¶
@ConditionalOnProperty - Based on property value
@ConditionalOnClass - Class exists on classpath
@ConditionalOnMissingClass - Class doesn't exist
@ConditionalOnBean - Bean exists in context
@ConditionalOnMissingBean - Bean doesn't exist
@ConditionalOnExpression - SpEL expression evaluates to true
@ConditionalOnResource - Resource exists on classpath
Lazy Initialization¶
@Component
@Lazy // Bean created on first use, not at startup
public class ExpensiveService {
public ExpensiveService() {
// Heavy initialization
}
}
// Lazy injection
@Service
public class SomeService {
@Autowired
@Lazy
private ExpensiveService expensiveService; // Proxy injected
}
// Global lazy initialization (Spring Boot)
spring.main.lazy-initialization=true
Events¶
// Custom 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; }
}
// Publishing events
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void createOrder(Order order) {
// Save order...
eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
}
}
// Listening to events
@Component
public class NotificationListener {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
sendNotification(event.getOrder());
}
@EventListener
@Async // Process asynchronously
public void handleOrderCreatedAsync(OrderCreatedEvent event) {
// Async processing
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterOrderCommitted(OrderCreatedEvent event) {
// Only after transaction commits
}
}
Common Interview Questions¶
- What is IoC and DI?
- IoC: Container controls object creation/lifecycle
-
DI: Specific form of IoC where dependencies are injected
-
Constructor vs Setter vs Field injection?
- Constructor: Recommended, immutable, testable
- Setter: Optional dependencies
-
Field: Avoid, hard to test
-
Singleton vs Prototype scope?
- Singleton: One instance per container
-
Prototype: New instance each request
-
@Component vs @Bean?
- @Component: Class-level, component scanning
-
@Bean: Method-level, third-party classes
-
How to inject prototype into singleton?
-
Use ObjectFactory, Provider, or scoped proxy
-
Bean lifecycle order?
- Constructor → DI → @PostConstruct → afterPropertiesSet → init-method
- *