Spring Framework – dependency injection w praktyce

TL;DR: Dependency Injection (DI) w Spring to mechanizm automatycznego dostarczania zależności do obiektów. Zamiast samemu tworzyć obiekty w kodzie, Spring zarządza nimi za nas. To prowadzi do luźno powiązanego, testowalnego kodu, który jest łatwiejszy w utrzymaniu.

Dlaczego Dependency Injection zmieni Twój sposób programowania

Dependency Injection to **fundamentalna koncepcja Spring Framework**, która rozwiązuje jeden z największych problemów obiektowego programowania – zarządzanie zależnościami między klasami. **Bez DI Twój kod staje się sztywny, trudny do testowania i mocno powiązany**. Z DIzyskujesz elastyczność, testowalność i kod, który można łatwo rozwijać.

Co się nauczysz:

  • Czym jest Dependency Injection i Inversion of Control
  • Jak skonfigurować Spring Container z XML i adnotacjami
  • Różnice między Constructor, Setter i Field Injection
  • Praktyczne przykłady DI w aplikacjach biznesowych
  • Najlepsze praktyki i częste błędy w DI
  • Jak testować kod wykorzystujący Spring DI
Wymagania wstępne: Podstawowa znajomość Java (klasy, interfejsy, dziedziczenie), umiejętność korzystania z IDE. Znajomość wzorców projektowych będzie pomocna ale nie jest wymagana.

Problem – mocno powiązany kod bez DI

Zacznijmy od przykładu **ZŁEGO** kodu, który nie używa Dependency Injection:

// PRZYKŁAD ZŁEGO KODU - bez Dependency Injection
public class OrderService {
    
    private EmailService emailService;
    private DatabaseService databaseService;
    
    public OrderService() {
        // Sztywne tworzenie zależności!
        this.emailService = new EmailService();
        this.databaseService = new MySQLDatabaseService();
    }
    
    public void processOrder(Order order) {
        // Zapisz zamówienie
        databaseService.save(order);
        
        // Wyślij email potwierdzający
        emailService.sendConfirmation(order.getCustomerEmail(), order);
    }
}

**Problemy tego podejścia:**

Uwaga: Ten kod jest trudny do testowania – nie można łatwo podmienić zależności na mocki w testach jednostkowych.

1. **Mocne powiązanie** – OrderService jest ściśle związany z konkretnymi implementacjami
2. **Brak elastyczności** – nie można łatwo zmienić implementacji EmailService
3. **Trudne testowanie** – jak przetestować OrderService bez wysyłania prawdziwych emaili?
4. **Naruszenie Single Responsibility** – klasa zarządza swoimi zależnościami

Rozwiązanie – Dependency Injection w akcji

Dependency Injection (DI) to wzorzec projektowy, gdzie obiekty otrzymują swoje zależności z zewnątrz, zamiast tworzyć je samodzielnie.
DI to jak restauracja – nie musisz sam gotować, kupować składników czy myć naczyń. Otrzymujesz gotowe danie (zależność) od kelnera (Spring Container).

Oto ten sam kod przepisany z użyciem Spring DI:

// PRZYKŁAD DOBREGO KODU - z Dependency Injection
@Service
public class OrderService {
    
    private final EmailService emailService;
    private final DatabaseService databaseService;
    
    // Constructor Injection - najlepsza praktyka
    @Autowired
    public OrderService(EmailService emailService, DatabaseService databaseService) {
        this.emailService = emailService;
        this.databaseService = databaseService;
    }
    
    public void processOrder(Order order) {
        // Logika biznesowa bez martwienia się o zależności
        databaseService.save(order);
        emailService.sendConfirmation(order.getCustomerEmail(), order);
    }
}

@Service
public class EmailService {
    public void sendConfirmation(String email, Order order) {
        // Logika wysyłania emaila
        System.out.println("Wysyłam email do: " + email);
    }
}

@Repository
public class DatabaseService {
    public void save(Order order) {
        // Logika zapisywania do bazy
        System.out.println("Zapisuję zamówienie: " + order.getId());
    }
}

Konfiguracja Spring Container – XML vs Adnotacje

W Spring 4 mamy dwie główne metody konfiguracji:

Konfiguracja XML (tradycyjna)





    
    
    
    
    
    
    
    
        
        
    


Konfiguracja adnotacjami (nowoczesna – Spring 4)

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // Spring automatycznie znajdzie klasy oznaczone @Service, @Repository, @Component
}

// Główna klasa aplikacji
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);
        
        OrderService orderService = context.getBean(OrderService.class);
        
        Order order = new Order("12345", "jan@example.com");
        orderService.processOrder(order);
    }
}
Pro tip: W 2015 roku adnotacje stają się standardem. XML nadal jest używany w legacy projektach, ale nowe aplikacje powinny preferować konfigurację adnotacjami.

Typy Dependency Injection w Spring

1. Constructor Injection (zalecane)

@Service
public class OrderService {
    
    private final EmailService emailService;
    private final DatabaseService databaseService;
    
    @Autowired
    public OrderService(EmailService emailService, DatabaseService databaseService) {
        this.emailService = emailService;
        this.databaseService = databaseService;
    }
}

**Zalety Constructor Injection:**
– Gwarantuje że wszystkie zależności są dostarczzone przy tworzeniu obiektu
– Umożliwia tworzenie immutable obiektów (final fields)
– Łatwiejsze testowanie – jasno widać co jest potrzebne

2. Setter Injection

@Service
public class OrderService {
    
    private EmailService emailService;
    private DatabaseService databaseService;
    
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
    
    @Autowired
    public void setDatabaseService(DatabaseService databaseService) {
        this.databaseService = databaseService;
    }
}

**Kiedy używać Setter Injection:**
– Opcjonalne zależności
– Circular dependencies (choć to anti-pattern)
– Legacy kod

3. Field Injection

@Service
public class OrderService {
    
    @Autowired
    private EmailService emailService;
    
    @Autowired
    private DatabaseService databaseService;
}
Uwaga: Field Injection jest wygodne, ale utrudnia testowanie i łamie enkapsulację. Używaj Constructor Injection jako domyślnego wyboru.

Praktyczny przykład – System sklepu internetowego

// Interfejsy - luźne powiązanie
public interface PaymentProcessor {
    boolean processPayment(BigDecimal amount, String cardNumber);
}

public interface InventoryService {
    boolean reserveProduct(String productId, int quantity);
    void releaseReservation(String productId, int quantity);
}

// Implementacje
@Service
public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public boolean processPayment(BigDecimal amount, String cardNumber) {
        // Logika płatności kartą
        System.out.println("Przetwarzam płatność: " + amount + " PLN");
        return true;
    }
}

@Service
public class DatabaseInventoryService implements InventoryService {
    @Override
    public boolean reserveProduct(String productId, int quantity) {
        // Sprawdź dostępność w bazie
        System.out.println("Rezerwuję " + quantity + " szt. produktu " + productId);
        return true;
    }
    
    @Override
    public void releaseReservation(String productId, int quantity) {
        System.out.println("Zwalniam rezerwację " + productId);
    }
}

// Główny serwis biznesowy
@Service
public class OrderProcessingService {
    
    private final PaymentProcessor paymentProcessor;
    private final InventoryService inventoryService;
    private final EmailService emailService;
    
    @Autowired
    public OrderProcessingService(PaymentProcessor paymentProcessor,
                                InventoryService inventoryService,
                                EmailService emailService) {
        this.paymentProcessor = paymentProcessor;
        this.inventoryService = inventoryService;
        this.emailService = emailService;
    }
    
    @Transactional
    public void processOrder(Order order) {
        try {
            // 1. Sprawdź dostępność produktów
            for (OrderItem item : order.getItems()) {
                if (!inventoryService.reserveProduct(item.getProductId(), item.getQuantity())) {
                    throw new InsufficientStockException("Brak towaru: " + item.getProductId());
                }
            }
            
            // 2. Przetwórz płatność
            boolean paymentSuccess = paymentProcessor.processPayment(
                order.getTotalAmount(), 
                order.getPaymentDetails().getCardNumber()
            );
            
            if (!paymentSuccess) {
                // Zwolnij rezerwacje
                releaseAllReservations(order);
                throw new PaymentException("Błąd płatności");
            }
            
            // 3. Wyślij potwierdzenie
            emailService.sendOrderConfirmation(order);
            
        } catch (Exception e) {
            releaseAllReservations(order);
            throw e;
        }
    }
    
    private void releaseAllReservations(Order order) {
        for (OrderItem item : order.getItems()) {
            inventoryService.releaseReservation(item.getProductId(), item.getQuantity());
        }
    }
}

Testowanie kodu z Dependency Injection

Oto jak łatwo testować kod używający DI:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class OrderProcessingServiceTest {
    
    @Mock
    private PaymentProcessor paymentProcessor;
    
    @Mock
    private InventoryService inventoryService;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private OrderProcessingService orderService;
    
    @Test
    public void shouldProcessOrderSuccessfully() {
        // Given
        Order order = createTestOrder();
        when(inventoryService.reserveProduct(any(), anyInt())).thenReturn(true);
        when(paymentProcessor.processPayment(any(), any())).thenReturn(true);
        
        // When
        orderService.processOrder(order);
        
        // Then
        verify(paymentProcessor).processPayment(order.getTotalAmount(), 
                                              order.getPaymentDetails().getCardNumber());
        verify(emailService).sendOrderConfirmation(order);
    }
    
    @Test(expected = PaymentException.class)
    public void shouldReleaseReservationsWhenPaymentFails() {
        // Given
        Order order = createTestOrder();
        when(inventoryService.reserveProduct(any(), anyInt())).thenReturn(true);
        when(paymentProcessor.processPayment(any(), any())).thenReturn(false);
        
        // When
        orderService.processOrder(order);
        
        // Then - exception thrown, reservations released
        verify(inventoryService).releaseReservation(any(), anyInt());
    }
    
    private Order createTestOrder() {
        // Helper method to create test data
        return new Order("123", "customer@example.com", new BigDecimal("99.99"));
    }
}
Pro tip: Dzięki DI możemy łatwo podmienić prawdziwe serwisy na mocki w testach. To niemożliwe w kodzie z twardymi zależnościami!

Bean Scopes – zarządzanie cyklem życia obiektów

ScopeOpisKiedy używać
singletonJeden obiekt na cały kontener (domyślne)Bezstanowe serwisy, DAO
prototypeNowy obiekt przy każdym wywołaniuObiekty ze stanem, krótko żyjące
requestJeden obiekt na HTTP requestWeb aplikacje, request-specific data
sessionJeden obiekt na HTTP sessionUser-specific data, koszyk
@Service
@Scope("singleton") // Domyślne - można pominąć
public class EmailService {
    // Jeden obiekt dla całej aplikacji
}

@Component
@Scope("prototype")
public class ShoppingCart {
    // Nowy obiekt przy każdym getBean()
    private List items = new ArrayList<>();
}

Częste błędy i najlepsze praktyki

Błąd #1: Circular Dependencies – klasa A potrzebuje B, która potrzebuje A.
// BŁĄD - circular dependency
@Service
public class UserService {
    @Autowired
    private OrderService orderService; // OrderService też używa UserService!
}

// ROZWIĄZANIE - wprowadź interfejs lub event
@Service
public class UserService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void createUser(User user) {
        // ... logika tworzenia
        eventPublisher.publishEvent(new UserCreatedEvent(user));
    }
}
Pułapka: Field Injection utrudnia testowanie – nie można łatwo ustawić zależności w testach bez Spring context.

**Najlepsze praktyki DI:**

1. **Używaj Constructor Injection** jako domyślnej opcji
2. **Preferuj interfejsy** zamiast konkretnych klas
3. **Unikaj Field Injection** w kodzie produkcyjnym
4. **Jednoznaczne nazwy beanów** gdy masz wiele implementacji
5. **@Qualifier dla rozróżnienia** implementacji tego samego interfejsu

// Wiele implementacji tego samego interfejsu
@Service
@Qualifier("creditCard")
public class CreditCardProcessor implements PaymentProcessor { }

@Service  
@Qualifier("paypal")
public class PayPalProcessor implements PaymentProcessor { }

// Użycie konkretnej implementacji
@Service
public class OrderService {
    private final PaymentProcessor paymentProcessor;
    
    public OrderService(@Qualifier("creditCard") PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }
}
Czy Spring tworzy beany przy starcie aplikacji?

Domyślnie tak – wszystkie singleton beany są tworzone eager (przy starcie). Możesz to zmienić adnotacją @Lazy dla lazy initialization.

Co się stanie jeśli Spring nie znajdzie beana do wstrzyknięcia?

Aplikacja nie wystartuje i rzuci NoSuchBeanDefinitionException. Możesz użyć @Autowired(required = false) dla opcjonalnych zależności.

Czy mogę wstrzyknąć kolekcje obiektów?

Tak! Spring automatycznie wstrzyknie List lub Map wszystkich beanów danego typu: @Autowired List<PaymentProcessor> processors

Jak debugować problemy z DI?

Włącz logi Spring na poziomie DEBUG, użyj @PostConstruct do sprawdzenia czy beany są prawidłowo utworzone, lub skorzystaj z Spring Boot Actuator.

Czy DI wpływa na wydajność?

Narzut jest minimalny – Spring używa reflection do wstrzykiwania, ale to dzieje się tylko przy starcie aplikacji. W runtime nie ma praktycznie żadnego narzutu.

Czy można używać DI bez Spring?

Tak! DI to wzorzec projektowy. Możesz go implementować ręcznie lub użyć innych kontenerów IoC jak Google Guice czy CDI.

Co to jest ApplicationContext?

To centralny interfejs Spring IoC Container. Zarządza beanami, ich konfiguracją i cyklem życia. To „serce” aplikacji Spring.

Następne kroki:

  • Naucz się Spring AOP (Aspect-Oriented Programming)
  • Poznaj Spring Boot dla jeszcze prostszej konfiguracji
  • Dowiedz się więcej o wzorcach projektowych w Java

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Stwórz prostą aplikację z trzema serwisami: UserService (zarządza użytkownikami), NotificationService (wysyła powiadomienia) i AuditService (loguje operacje). Użyj Constructor Injection i napisz testy jednostkowe z mockami. Bonus: dodaj dwie implementacje NotificationService (Email i SMS) i użyj @Qualifier.

Jak zamierzasz wykorzystać Dependency Injection w swoich projektach? Podziel się swoimi doświadczeniami z testowaniem kodu używającego Spring DI!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry