SP
Dependency Injection
Spring core v1.0.0
Spring Dependency Injection
Overview
Dependency Injection (DI) is the core principle of the Spring Framework, enabling loose coupling, testability, and modular design. This skill covers constructor injection, bean scopes, configuration options, and advanced injection patterns.
Key Concepts
IoC Container Lifecycle
┌─────────────────────────────────────────────────────────────┐
│ Spring IoC Container Lifecycle │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. CONFIGURATION LOADING │
│ └── @Configuration, @Component scanning, XML │
│ │
│ 2. BEAN DEFINITION REGISTRATION │
│ └── BeanDefinition objects created │
│ │
│ 3. BEAN POST-PROCESSOR REGISTRATION │
│ └── BeanFactoryPostProcessor runs │
│ │
│ 4. BEAN INSTANTIATION │
│ ├── Constructor injection │
│ └── Setter/Field injection │
│ │
│ 5. DEPENDENCY INJECTION │
│ └── Wire dependencies │
│ │
│ 6. INITIALIZATION │
│ ├── @PostConstruct │
│ ├── InitializingBean.afterPropertiesSet() │
│ └── Custom init methods │
│ │
│ 7. READY FOR USE │
│ │
│ 8. DESTRUCTION (on context close) │
│ ├── @PreDestroy │
│ └── DisposableBean.destroy() │
│ │
└─────────────────────────────────────────────────────────────┘
Bean Scopes
| Scope | Description | Use Case |
|---|---|---|
| singleton | One instance per container (default) | Stateless services |
| prototype | New instance each request | Stateful beans |
| request | One per HTTP request | Request-scoped data |
| session | One per HTTP session | User session data |
| application | One per ServletContext | App-wide singletons |
Best Practices
1. Prefer Constructor Injection
Constructor injection ensures dependencies are not null and allows for immutability.
2. Avoid Field Injection
Field injection hides dependencies and makes testing difficult.
3. Use Interface-Based Dependencies
Depend on interfaces for flexibility and testability.
4. Keep Beans Focused
Each bean should have a single responsibility.
5. Use Qualifiers for Disambiguation
When multiple beans of the same type exist, use @Qualifier.
Code Examples
Example 1: Constructor Injection Patterns
// ✅ RECOMMENDED: Constructor injection with final fields
@Service
@RequiredArgsConstructor // Lombok generates constructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final EventPublisher eventPublisher;
private final OrderValidator orderValidator;
public Order createOrder(CreateOrderCommand command) {
orderValidator.validate(command);
Order order = Order.create(command);
order = orderRepository.save(order);
eventPublisher.publish(new OrderCreatedEvent(order));
return order;
}
}
// Constructor injection without Lombok
@Service
public class OrderServiceExplicit {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
// @Autowired is optional on single constructor (Spring 4.3+)
public OrderServiceExplicit(OrderRepository orderRepository,
PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
}
// ❌ AVOID: Field injection (hidden dependencies, hard to test)
@Service
public class BadOrderService {
@Autowired
private OrderRepository orderRepository; // Not final, can be null
@Autowired
private PaymentService paymentService;
}
// ❌ AVOID: Setter injection (allows partial initialization)
@Service
public class AnotherBadService {
private OrderRepository orderRepository;
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Example 2: Configuration Classes
@Configuration
public class AppConfiguration {
// Simple bean definition
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
// Bean with dependencies
@Bean
public OrderService orderService(OrderRepository repository,
PaymentClient paymentClient,
ObjectMapper objectMapper) {
return new OrderService(repository, paymentClient, objectMapper);
}
// Conditional bean
@Bean
@ConditionalOnProperty(name = "app.cache.enabled", havingValue = "true")
public CacheManager cacheManager() {
return new CaffeineCacheManager();
}
// Profile-specific bean
@Bean
@Profile("production")
public MetricsCollector productionMetrics() {
return new DatadogMetricsCollector();
}
@Bean
@Profile("!production")
public MetricsCollector developmentMetrics() {
return new LoggingMetricsCollector();
}
}
// Nested configuration for organization
@Configuration
public class InfrastructureConfiguration {
@Configuration
@ConditionalOnProperty(name = "app.database.type", havingValue = "postgresql")
static class PostgresConfiguration {
@Bean
public DataSource dataSource(DatabaseProperties properties) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(properties.getUrl());
config.setUsername(properties.getUsername());
config.setPassword(properties.getPassword());
config.setMaximumPoolSize(properties.getMaxPoolSize());
return new HikariDataSource(config);
}
}
}
Example 3: Qualifiers and Primary
// Multiple implementations
public interface NotificationService {
void send(Notification notification);
}
@Service("emailNotification")
@Primary // Default when no qualifier specified
public class EmailNotificationService implements NotificationService {
@Override
public void send(Notification notification) {
// Send email
}
}
@Service("smsNotification")
public class SmsNotificationService implements NotificationService {
@Override
public void send(Notification notification) {
// Send SMS
}
}
@Service("pushNotification")
public class PushNotificationService implements NotificationService {
@Override
public void send(Notification notification) {
// Send push notification
}
}
// Using qualifiers
@Service
@RequiredArgsConstructor
public class NotificationOrchestrator {
@Qualifier("emailNotification")
private final NotificationService emailService;
@Qualifier("smsNotification")
private final NotificationService smsService;
// Gets the @Primary bean (email)
private final NotificationService defaultService;
// Inject all implementations
private final List<NotificationService> allServices;
// Inject by name in map
private final Map<String, NotificationService> servicesByName;
public void notifyAll(Notification notification) {
for (NotificationService service : allServices) {
try {
service.send(notification);
} catch (Exception e) {
log.error("Failed to send via {}", service.getClass().getSimpleName(), e);
}
}
}
public void notifyByChannel(String channel, Notification notification) {
NotificationService service = servicesByName.get(channel + "Notification");
if (service != null) {
service.send(notification);
}
}
}
// Custom qualifier annotation
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface HighPriority {}
@Service
@HighPriority
public class HighPriorityNotificationService implements NotificationService {
// Implementation
}
@Service
public class ConsumerService {
public ConsumerService(@HighPriority NotificationService notificationService) {
// Gets HighPriorityNotificationService
}
}
Example 4: Bean Scopes
// Prototype scope - new instance each time
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RequestContext {
private final String requestId = UUID.randomUUID().toString();
private final Instant createdAt = Instant.now();
// New instance created for each injection
}
// Request scope - one per HTTP request
@Component
@RequestScope
public class UserContext {
private String userId;
private List<String> roles = new ArrayList<>();
public void setUser(String userId, List<String> roles) {
this.userId = userId;
this.roles = roles;
}
}
// Injecting prototype into singleton (proxy approach)
@Service
@RequiredArgsConstructor
public class OrderProcessor {
// Proxy ensures new instance each access
@Lookup
public RequestContext getRequestContext() {
// Spring overrides this method
return null;
}
public void process(Order order) {
RequestContext ctx = getRequestContext(); // New instance each call
log.info("Processing order {} in request {}", order.getId(), ctx.getRequestId());
}
}
// Alternative: ObjectProvider for lazy/optional dependencies
@Service
public class FlexibleService {
private final ObjectProvider<ExpensiveBean> expensiveBean;
private final ObjectProvider<OptionalFeature> optionalFeature;
public FlexibleService(ObjectProvider<ExpensiveBean> expensiveBean,
ObjectProvider<OptionalFeature> optionalFeature) {
this.expensiveBean = expensiveBean;
this.optionalFeature = optionalFeature;
}
public void doWork() {
// Lazy initialization - only created when needed
ExpensiveBean bean = expensiveBean.getObject();
// Optional dependency - may not exist
optionalFeature.ifAvailable(feature -> feature.enable());
}
}
Example 5: Advanced Injection Patterns
// Factory pattern with Spring
@Configuration
public class PaymentProcessorConfiguration {
@Bean
public PaymentProcessorFactory paymentProcessorFactory(
ApplicationContext context) {
return new PaymentProcessorFactory(context);
}
}
@Component
@RequiredArgsConstructor
public class PaymentProcessorFactory {
private final ApplicationContext context;
public PaymentProcessor create(PaymentMethod method) {
return switch (method) {
case CREDIT_CARD -> context.getBean(CreditCardProcessor.class);
case PAYPAL -> context.getBean(PayPalProcessor.class);
case BANK_TRANSFER -> context.getBean(BankTransferProcessor.class);
};
}
}
// Self-injection for transactional methods
@Service
public class SelfInjectingService {
@Lazy
@Autowired
private SelfInjectingService self;
public void publicMethod() {
// Call through proxy to get transaction
self.transactionalMethod();
}
@Transactional
public void transactionalMethod() {
// This is now properly transactional
}
}
// Circular dependency resolution
@Service
@RequiredArgsConstructor
public class ServiceA {
private final ObjectProvider<ServiceB> serviceBProvider;
public void doWork() {
ServiceB serviceB = serviceBProvider.getObject();
serviceB.assist();
}
}
@Service
@RequiredArgsConstructor
public class ServiceB {
private final ServiceA serviceA;
public void assist() {
// Can safely use serviceA
}
}
// BeanPostProcessor for custom initialization
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean.getClass().isAnnotationPresent(Logged.class)) {
log.info("Initializing logged bean: {}", beanName);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
// Custom Scope implementation
@Component
public class TenantScope implements Scope {
private final ThreadLocal<Map<String, Object>> scopedObjects =
ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = scopedObjects.get();
return scope.computeIfAbsent(name, k -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
return scopedObjects.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// Handle cleanup
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return TenantContext.getCurrentTenant();
}
public void clear() {
scopedObjects.remove();
}
}
Anti-Patterns
❌ Using ApplicationContext Directly
// WRONG - Service locator pattern
@Service
public class BadService {
@Autowired
private ApplicationContext context;
public void process() {
OrderRepository repo = context.getBean(OrderRepository.class);
// Use repo...
}
}
// ✅ CORRECT - Inject dependency directly
@Service
@RequiredArgsConstructor
public class GoodService {
private final OrderRepository orderRepository;
public void process() {
// Use orderRepository...
}
}
❌ Circular Dependencies
// WRONG - Circular dependency
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // ServiceB also depends on ServiceA
}
// ✅ CORRECT - Break cycle with redesign or ObjectProvider
@Service
@RequiredArgsConstructor
public class ServiceA {
private final ObjectProvider<ServiceB> serviceB; // Lazy resolution
}
Testing Strategies
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@InjectMocks
private OrderService orderService;
@Test
void shouldCreateOrder() {
// Constructor injection makes mocking trivial
when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
Order result = orderService.createOrder(validCommand);
assertThat(result).isNotNull();
verify(orderRepository).save(any());
}
}
// Integration test with context
@SpringBootTest
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@MockBean
private PaymentService paymentService; // Mock external dependency
@Test
void shouldProcessOrder() {
when(paymentService.charge(any())).thenReturn(PaymentResult.success());
Order result = orderService.createOrder(validCommand);
assertThat(result.getStatus()).isEqualTo(OrderStatus.CREATED);
}
}