Introduction
Dependency Injection (DI) is the heart of the Spring Framework. A deep understanding of DI and Bean Lifecycle will help you design applications that are easy to test, easy to extend, and easy to maintain. This article will delve into injection types, bean scopes and lifecycle hooks.
1. Dependency Injection — Three approaches
1.1 Constructor Injection (Recommended)
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
// Spring tự inject qua constructor
// Với 1 constructor duy nhất, @Autowired là optional
public OrderService(OrderRepository orderRepository,
PaymentService paymentService,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
}
Advantages:
- Immutable: Fields is
final, cannot be changed after initialization - Testable: Easily create instances in unit testing
- Fail-fast: Missing dependency error detected right at startup
- Clear dependencies: Look at the constructor and immediately know what the class needs
1.2 Setter Injection
@Service
public class ReportService {
private ReportGenerator reportGenerator;
private EmailService emailService;
@Autowired
public void setReportGenerator(ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}
@Autowired(required = false) // Optional dependency
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
}
Used when dependency is optional or needs to be reconfigurable at runtime.
1.3 Field Injection (Not recommended)
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Không dùng cách này!
@Autowired
private PasswordEncoder passwordEncoder;
}
Disadvantages:
- Cannot create instances in unit testing without using reflection
- Fields can't
final - Hidden dependencies, hard to see
- Spring Framework 7 warns about deprecation for field injection
2. Handle multiple Beans of the same type
2.1 @Primary
public interface NotificationService {
void send(String to, String message);
}
@Service
@Primary // Bean mặc định khi inject NotificationService
public class EmailNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
// Send email
}
}
@Service
public class SmsNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
// Send SMS
}
}
2.2 @Qualifier
@Service
public class OrderService {
private final NotificationService emailService;
private final NotificationService smsService;
public OrderService(
@Qualifier("emailNotificationService") NotificationService emailService,
@Qualifier("smsNotificationService") NotificationService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
}
2.3 Inject Collection
@Service
public class NotificationManager {
private final List<NotificationService> notificationServices;
// Spring inject TẤT CẢ beans implement NotificationService
public NotificationManager(List<NotificationService> notificationServices) {
this.notificationServices = notificationServices;
}
public void notifyAll(String to, String message) {
notificationServices.forEach(service -> service.send(to, message));
}
}
3. Bean Scopes
3.1 Available scopes
@Component
@Scope("singleton") // Default - một instance cho toàn bộ ApplicationContext
public class SingletonBean { }
@Component
@Scope("prototype") // Mỗi lần inject tạo instance mới
public class PrototypeBean { }
@Component
@Scope("request") // Một instance per HTTP request (web only)
public class RequestScopedBean { }
@Component
@Scope("session") // Một instance per HTTP session (web only)
public class SessionScopedBean { }
3.2 Singleton vs Prototype
@Component
public class SingletonDemo {
// Singleton: Tất cả inject cùng 1 instance
// ⚠️ Phải thread-safe nếu có mutable state
private int counter = 0;
public int increment() {
return ++counter; // Race condition nếu concurrent access!
}
}
@Component
@Scope("prototype")
public class PrototypeDemo {
// Prototype: Mỗi lần inject tạo instance mới
// Spring KHÔNG quản lý lifecycle sau khi tạo
private final UUID id = UUID.randomUUID();
}
3.3 Prototype in Singleton — Common problem
@Service // Singleton
public class OrderProcessor {
// ⚠️ BUG: Prototype bean chỉ được tạo 1 lần khi Singleton khởi tạo!
private final PrototypeBean prototypeBean;
public OrderProcessor(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
// Fix: Dùng ObjectProvider
private final ObjectProvider<PrototypeBean> prototypeBeanProvider;
public OrderProcessor(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public void process() {
PrototypeBean freshBean = prototypeBeanProvider.getObject(); // Mỗi lần mới
}
}
4. Bean Lifecycle
4.1 Lifecycle Flow
Bean Definition Loaded
│
▼
Instantiate (Constructor)
│
▼
Populate Properties (DI)
│
▼
BeanNameAware.setBeanName()
│
▼
BeanFactoryAware.setBeanFactory()
│
▼
ApplicationContextAware.setApplicationContext()
│
▼
BeanPostProcessor.postProcessBeforeInitialization()
│
▼
@PostConstruct method
│
▼
InitializingBean.afterPropertiesSet()
│
▼
Custom init-method
│
▼
BeanPostProcessor.postProcessAfterInitialization()
│
▼
═══════════════════
Bean is Ready ✅
═══════════════════
│
▼ (Application Shutdown)
│
@PreDestroy method
│
▼
DisposableBean.destroy()
│
▼
Custom destroy-method
4.2 Lifecycle Callbacks in practice
@Service
public class CacheService {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private final DataLoader dataLoader;
public CacheService(DataLoader dataLoader) {
this.dataLoader = dataLoader;
}
@PostConstruct
public void init() {
// Chạy sau khi tất cả dependencies đã được inject
// Dùng để warm up cache, validate config, etc.
System.out.println("CacheService initialized, warming up cache...");
cache.putAll(dataLoader.loadInitialData());
}
@PreDestroy
public void cleanup() {
// Chạy trước khi bean bị destroy (application shutdown)
// Dùng để release resources, flush data, etc.
System.out.println("CacheService shutting down, flushing cache...");
cache.clear();
}
}
4.3 SmartInitializingSingleton
@Component
public class ApplicationStartupListener implements SmartInitializingSingleton {
@Override
public void afterSingletonsInstantiated() {
// Chạy sau khi TẤT CẢ singleton beans đã được khởi tạo
System.out.println("All beans are ready! Application fully started.");
}
}
5. Conditional Beans
5.1 Built-in Conditions
@Configuration
public class ConditionalConfig {
@Bean
@ConditionalOnProperty(name = "app.cache.enabled", havingValue = "true")
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "products");
}
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager noOpCacheManager() {
return new NoOpCacheManager(); // Fallback khi không có cache
}
@Bean
@ConditionalOnClass(name = "io.lettuce.core.RedisClient")
public RedisTemplate<String, Object> redisTemplate() {
return new RedisTemplate<>();
}
}
5.2 Custom Conditions
// Tạo custom condition
public class OnProductionCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String[] activeProfiles = context.getEnvironment().getActiveProfiles();
return Arrays.asList(activeProfiles).contains("prod");
}
}
@Bean
@Conditional(OnProductionCondition.class)
public AuditService auditService() {
return new AuditService();
}
6. Lazy Initialization
6.1 Default: Eager Initialization
By default, Spring Boot creates all singleton beans at startup. This helps detect errors early but increases startup time.
6.2 Lazy Initialization
@Service
@Lazy // Bean chỉ được tạo khi lần đầu tiên được sử dụng
public class HeavyReportService {
public HeavyReportService() {
// Expensive initialization
}
}
# Hoặc global lazy initialization
spring:
main:
lazy-initialization: true
Warning: Lazy initialization can hide configuration errors. Only use it when absolutely necessary (startup time is too slow) and ensure there are complete integration tests.
Summary
- Constructor Injection is the recommended approach: immutable, testable, fail-fast
- Default Bean Scope is Singleton (1 instance), use Prototype when you need a new instance each time
- Bean Lifecycle provides hooks (@PostConstruct, @PreDestroy) to manage resources
- Conditional annotations allow to register beans based on conditions (classpath, properties, profile)
Exercises
- Create interface
PaymentGatewaywith 2 implementations:StripeGatewayandPaypalGateway. Use @Primary and @Qualifier to inject the correct bean - Create a Prototype-scoped bean and inject it into the Singleton bean. Verify that each request receives a different instance (using ObjectProvider)
- Create a service with @PostConstruct to load data from JSON file at startup, @PreDestroy to log cleanup information