Spring Qualifier - why and how with type safety
Jasne, opiszmy dokładnie i krok po kroku, jak używać adnotacji @Qualifier w Springu do precyzyjnego wyboru beana.
Czym jest @Qualifier i dlaczego jest potrzebny?
W ekosystemie Springa, mechanizm wstrzykiwania zależności (Dependency Injection) jest kluczowy. Używamy adnotacji @Autowired, aby Spring automatycznie znalazł i wstrzyknął odpowiedniego beana (obiekt zarządzany przez kontener Springa) do naszej klasy.
Problem pojawia się, gdy Spring znajduje więcej niż jednego beana tego samego typu.
Wyobraź sobie sytuację: masz interfejs MessageService i dwie jego implementacje: SmsService i EmailService. Obie klasy są oznaczone jako beany (np. za pomocą @Component lub @Service).
public interface MessageService {
void sendMessage(String message);
}
@Service
public class SmsService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Wysyłanie SMS: " + message);
}
}
@Service
public class EmailService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Wysyłanie e-mail: " + message);
}
}
Teraz, jeśli w innej klasie spróbujesz wstrzyknąć MessageService, Spring napotka problem:
@Component
public class NotificationManager {
private final MessageService messageService;
@Autowired // <-- PROBLEM!
public NotificationManager(MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
Podczas uruchamiania aplikacji, Spring rzuci wyjątkiem, najczęściej NoUniqueBeanDefinitionException. Komunikat błędu będzie jasno mówił: "Oczekiwałem jednego beana typu MessageService, ale znalazłem dwa: smsService i emailService. Nie wiem, którego wybrać!".
I tutaj właśnie z pomocą przychodzi @Qualifier. Jest to adnotacja, która pozwala nam rozwiązać tę niejednoznaczność, precyzyjnie wskazując, który konkretny bean ma zostać wstrzyknięty.
Jak używać @Qualifier? (Metoda z nazwami)
Najprostszym sposobem użycia @Qualifier jest połączenie go z nazwami beanów.
Krok 1: Nazwij swoje beany
Domyślnie, Spring nadaje beanom nazwy oparte na nazwie klasy, zapisane w stylu camelCase (np. klasa SmsService staje się beanem o nazwie smsService). Możemy jednak (i często powinniśmy) nadać im jawne nazwy, aby kod był bardziej czytelny i niezależny od refaktoryzacji nazw klas.
Robimy to, podając nazwę jako argument adnotacji @Component, @Service, @Repository itd.
@Service("smsBean") // Nadajemy jawną nazwę "smsBean"
public class SmsService implements MessageService {
// ...
}
@Service("emailBean") // Nadajemy jawną nazwę "emailBean"
public class EmailService implements MessageService {
// ...
}
Krok 2: Użyj @Qualifier w miejscu wstrzykiwania
Teraz w klasie NotificationManager możemy użyć @Qualifier wraz z @Autowired, aby wskazać, który bean chcemy wstrzyknąć. Nazwa podana w @Qualifier musi pasować do nazwy beana zdefiniowanej w kroku 1.
Wstrzykiwanie przez pole (Field Injection - mniej zalecane):
@Component
public class NotificationManager {
@Autowired
@Qualifier("emailBean") // Chcę wstrzyknąć beana o nazwie "emailBean"
private MessageService messageService;
// ...
}
Wstrzykiwanie przez konstruktor (Constructor Injection - zalecana praktyka):
To jest preferowane podejście, ponieważ promuje niezmienność (immutability) i ułatwia testowanie. Adnotację @Qualifier umieszczamy przy parametrze konstruktora.
@Component
public class NotificationManager {
private final MessageService messageService;
// Adnotacja @Autowired na konstruktorze z jednym argumentem jest opcjonalna w nowszych wersjach Springa
public NotificationManager(@Qualifier("smsBean") MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
W tym przypadku, NotificationManager zawsze będzie używał serwisu SMS.
Zaawansowane użycie: Tworzenie własnych adnotacji kwalifikujących
Używanie stringów ("smsBean", "emailBean") jest skuteczne, ale ma wady:
- Brak bezpieczeństwa typów: Literówka w nazwie zostanie wykryta dopiero podczas uruchamiania aplikacji, a nie w trakcie kompilacji.
- Trudniejsza refaktoryzacja: Jeśli zmienisz nazwę beana, musisz ręcznie znaleźć i poprawić wszystkie miejsca, gdzie jest ona używana w
@Qualifier.
Lepszym i bardziej eleganckim rozwiązaniem jest stworzenie własnych, dedykowanych adnotacji kwalifikujących.
Krok 1: Stwórz własne adnotacje
Tworzymy nowe adnotacje, które same będą "opakowaniem" dla @Qualifier.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier // <-- Kluczowe! To mówi Springowi, że to jest adnotacja kwalifikująca.
public @interface SmsQualifier {
}
I analogicznie dla e-maila:
// importy jak wyżej
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface EmailQualifier {
}
@Target: Określa, gdzie można użyć adnotacji (na polach, metodach, parametrach, typach).@Retention(RetentionPolicy.RUNTIME): Zapewnia, że adnotacja będzie dostępna dla JVM w czasie działania programu, co jest wymagane przez Springa.@Qualifier: Oznacza naszą adnotację jako specjalny typ kwalifikatora.
Krok 2: Użyj własnych adnotacji na beanach
Teraz oznaczamy nasze serwisy nowo stworzonymi adnotacjami.
@Service
@SmsQualifier // Zamiast @Qualifier("smsBean")
public class SmsService implements MessageService {
// ...
}
@Service
@EmailQualifier // Zamiast @Qualifier("emailBean")
public class EmailService implements MessageService {
// ...
}
Krok 3: Użyj własnych adnotacji w miejscu wstrzykiwania
Na koniec, wstrzykujemy zależności, używając naszych bezpiecznych typowo adnotacji.
@Component
public class NotificationManager {
private final MessageService messageService;
public NotificationManager(@EmailQualifier MessageService messageService) { // <-- Elegancko i bezpiecznie!
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
Teraz, jeśli chcielibyśmy zmienić serwis na SMS, wystarczy podmienić @EmailQualifier na @SmsQualifier. IDE natychmiast pomoże nam w autouzupełnianiu i wykryje ewentualne błędy, jeśli adnotacja nie istnieje.