1. Namespace context w XML
Gdy <bean>, constructor-arg i property już nie straszą, w legacy XML szybko wypływa kolejna warstwa — tagi context:*. Jeśli otwierasz legacy XML-plik i w pierwszej linii widzisz pięć xmlns i trzy URL-e, mózg słusznie mówi: „Zamknij to. To nie kod, to zaklęcie". Ale w rzeczywistości jest to dość pragmatyczne: XML to język, gdzie różne „zestawy tagów" żyją w różnych namespaceach, i Spring starannie rozkłada swoje tagi po takich zestawach.
W najprostszym XML-konfigu mamy tylko namespace beans, który daje tagi <beans>, <bean>, <property>, <constructor-arg>. Ale gdy widzisz context:annotation-config lub context:component-scan, oznacza to, że podłączony jest jeszcze jeden namespace — context. Dodaje „kontekstowe" tagi, które odnoszą się nie do konkretnej definicji beana, lecz do konfiguracji infrastruktury kontenera.
Minimalny „szkielet" XML z namespace context wygląda tak:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Podłączyliśmy namespace context: teraz dozwolone są tagi w postaci <context:...> -->
<!-- schemaLocation — podpowiedź dla IDE/walidacji: które XSD opisują te tagi -->
<!-- tu będą <bean> i <context:...> -->
</beans>
Tu ważne są dwie myśli.
Pierwsza myśl prosta: xmlns:context="..." oznacza „w tym pliku dozwolone są tagi z prefiksem context:". To prawie jak import w Javie, tylko dla tagów XML. A xsi:schemaLocation="..." — to podpowiedź dla walidacji/IDE: które schematy XSD opisują dozwolone tagi i atrybuty. Nie musisz czytać XSD (pożałuj siebie), musisz umieć rozpoznawać, że plik podłącza namespace context.
Druga myśl ważniejsza: pojawienie się context: prawie zawsze mówi, że fragment XML — to nie „czysty ręczny wiring", lecz most do świata adnotacji. I teraz omówimy dwa najczęstsze tagi, dla których to wszystko w ogóle się pojawia.
2. context:annotation-config: adnotacje dla XML-beanów
Do tej pory XML był dość „uczciwy": widzisz <bean>, widzisz constructor-arg, widzisz ref — i rozumiesz wiring. Ale legacy projekty często żyły w trybie hybrydowym: beany deklarowano w XML, a zależności wewnątrz klas oznaczano adnotacjami @Autowired, @Value, @PostConstruct. Jeśli kontenerowi nie powiedzieć „przetwarzaj te adnotacje", adnotacje pozostaną po prostu ładnymi komentarzami dla przyszłych archeologów.
Właśnie to robi <context:annotation-config/>: włącza infrastrukturę, która rozumie adnotacje wewnątrz już zarejestrowanych beanów. Kluczowy niuans: ten tag nie jest potrzebny do samego faktu istnienia XML-beana. <bean> i bez niego pozostaje <bean>. Potrzebny jest po to, żeby Spring zobaczył @Autowired, @Value, @PostConstruct i inne adnotacje wewnątrz klasy.
Jeśli powiązać to z tym, co już przerabialiśmy w module o pipeline kontenera, to context:annotation-config z sensu — to „dodać do kontekstu potrzebne post-processory, żeby adnotacje zaczęły działać". Wiesz już: bez BeanPostProcessor i towarzyszy magii nie ma, jest tylko smutny new.
Spójrzmy na mały przykład ze świata ContextFlow. Niech mamy serwis, który jest rejestrowany w XML przez zwykły <bean>, a adnotacje są używane już wewnątrz samej klasy:
package com.example.contextflow.application.service;
import com.example.contextflow.domain.ports.NotificationSender;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
public class LegacyNotificationDispatchService {
private NotificationSender sender; // Zależność przyjdzie przez adnotowany setter
@Autowired // Prosimy Spring o wstrzyknięcie pasującego beana już po stworzeniu obiektu
public void setSender(NotificationSender sender) {
this.sender = sender;
}
@PostConstruct // Sprawdzamy, że przetwarzanie adnotacji naprawdę zadziałało
public void init() {
System.out.println("LegacyNotificationDispatchService ready"); // ... ready
}
public String senderType() {
return sender.getClass().getSimpleName();
}
}
Zwróć uwagę na sprytny moment: serwis jest już zarejestrowany przez <bean>. Pytanie nie o to, czy klasa trafi do kontekstu, lecz o to, czy kontener przetworzy adnotacje wewnątrz niej.
Teraz konfiguracja XML (umownie legacy/legacy-annotation-context.xml), gdzie deklarujemy sender i serwis:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Włączamy infrastrukturę przetwarzania @Autowired/@PostConstruct/@Value itp. -->
<context:annotation-config/>
<!-- Jawnie rejestrujemy bean-implementację portu NotificationSender -->
<bean id="notificationSender"
class="com.example.contextflow.infrastructure.notification.ConsoleNotificationSender"/>
<!-- Jawnie rejestrujemy serwis, a adnotacje wewnątrz niego teraz naprawdę będą przetworzone -->
<bean id="dispatchService"
class="com.example.contextflow.application.service.LegacyNotificationDispatchService"/>
</beans>
Tu context:annotation-config — to krytyczna linijka. Bez niej Spring nadal zobaczy oba <bean> i będzie mógł stworzyć LegacyNotificationDispatchService przez domyślny konstruktor. Ale @Autowired i @PostConstruct wewnątrz klasy pozostaną po prostu adnotacjami: setSender(...) nie zostanie wywołany, init() też nie zadziała. Czyli problem nie w istnieniu XML-beana, lecz we wyłączonej infrastrukturze przetwarzania adnotacji.
Załadujmy ten kontekst i sprawdźmy, że wstrzyknięcie naprawdę nastąpiło:
package com.example.contextflow;
import com.example.contextflow.application.service.LegacyNotificationDispatchService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class LegacyXmlDemo {
public static void main(String[] args) {
// Ładujemy kontekst XML z classpath (resources/legacy/...)
try (var ctx = new ClassPathXmlApplicationContext("legacy/legacy-annotation-context.xml")) {
// Pobieramy serwis po typie — oczekujemy, że zależności są już wstrzyknięte
var service = ctx.getBean(LegacyNotificationDispatchService.class);
System.out.println(service.senderType()); // ConsoleNotificationSender
}
}
}
Ważne jest, żeby nie pomylić sensu annotation-config. Włącza przetwarzanie adnotacji, ale nie robi skanowania. Dlatego jeśli w XML nie ma <bean> dla notificationSender i nie ma innych sposobów jego zarejestrowania, @Autowired nie pomoże: kontener uczciwie powie, że kandydata nie ma. Adnotacje — to nie „stwórz mi zależność z powietrza", to „użyj już zarejestrowanych definicji beanów mądrzej".
3. context:component-scan: skanowanie komponentów
Po poznaniu annotation-config pojawia się naturalna myśl: „Okej, on uczy kontener rozumieć adnotacje. A gdzie jest «znajdź mi komponenty automatycznie», jak robiliśmy wcześniej przez @ComponentScan?" Odpowiedź: robi to <context:component-scan/>. I właśnie ten tag najczęściej spotykamy w prawdziwym legacy XML, bo zamyka dwa zadania jednocześnie: i włącza adnotacje, i rejestruje komponenty po pakietach.
Z sensu <context:component-scan base-package="..."> — to XML-odpowiednik naszego znajomego tematu z Dnia 5: skanowanie classpath. Kontener przebiera przez wskazane pakiety, szuka klasy ze stereotypowymi adnotacjami (@Component, @Service, @Repository, @Configuration), zamienia je na definicje beanów — i dalej żyje zwykłym życiem kontenerowym.
Oto minimalny przykład fragmentu XML, który skanuje pakiet powiadomień:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Skanujemy pakiet: wszystkie klasy @Component-podobne staną się definicjami beanów -->
<context:component-scan base-package="com.example.contextflow.infrastructure.notification"/>
</beans>
Teraz wyobraźmy sobie, że w tym pakiecie leży komponent (uproszczone, żeby zobaczyć najważniejsze):
package com.example.contextflow.infrastructure.notification;
import org.springframework.stereotype.Component;
@Component("legacyConsoleSender") // Jawnie podajemy nazwę beana (przyda się do @Qualifier)
public class LegacyConsoleNotificationSender {
// Pusty przykład: ważny jest sam fakt rejestracji komponentu przez component-scan
}
Po component-scan ta klasa zostanie zarejestrowana jako bean (z nazwą legacyConsoleSender). I dalej każdy inny bean — zarówno z XML, jak i z innego skanowanego pakietu — będzie mógł go pobrać po typie lub po nazwie, jak już umiemy.
Jest ważny niuans, który warto zapamiętać w formie „krótkiej tabeli zdrowego rozsądku". W legacy konfiguracjach często spotkasz albo annotation-config, albo component-scan, albo oba jednocześnie. Żeby szybko zrozumieć, co się dzieje, warto trzymać w głowie takie porównanie:
| Pytanie | context:annotation-config | context:component-scan |
|---|---|---|
| Szuka klas w pakietach? | Nie | Tak |
| Robi z znalezionych klas definicje beanów? | Nie | Tak |
| Włącza przetwarzanie @Autowired, @PostConstruct, @Value itp.? | Tak | Tak (zwykle niejawnie) |
| Komu pomaga? | Tym beanom, które już trafiły do kontekstu (przez <bean>, import, skanowanie itp.) | I zarejestruje nowych, i „włączy adnotacje" dla nich |
| Typowy legacy case | „Beany są zadeklarowane w XML, ale wewnątrz klas są adnotacje" | „Projekt już przeniósł się na komponenty, ale punkt wejścia to nadal XML" |
Jeśli widzisz w jednym pliku jednocześnie i <context:component-scan/>, i <context:annotation-config/>, to najczęściej to po prostu „historyczne śmieci", pozostawione po kilku migracjach. Zazwyczaj to nie psuje aplikacji, ale utrudnia czytanie: wydaje się, że są tu dwa różne mechanizmy, choć jeden już zawiera drugi z sensu.
Żeby utrwalić mental model, warto wyobrazić sobie to jako mini-pipeline:
flowchart TD
A["XML załadowany"] --> B["component-scan rejestruje znalezione komponenty"]
A --> C["jawne bean rejestrują się jako definitions"]
B --> D["kontener włącza przetwarzanie adnotacji"]
C --> D
D --> E["tworzenie obiektów i wstrzykiwanie zależności"]
Nie ma się tu czego bać: to ta sama dwufazowa historia, którą wielokrotnie omawialiśmy. Najpierw pojawiają się definicje (z <bean> i ze skanowania), potem następuje tworzenie obiektów i wiring, przy czym „przetwarzanie adnotacji" — to część infrastruktury kontenera na tej drodze.
4. Czytanie legacy XML z context:*
W praktyce problem nowicjusza nie polega na zapamiętaniu dwóch tagów. Problem jest inny: otwierasz XML i nie rozumiesz, skąd w ogóle biorą się beany. Część jest zadeklarowana jawnie, część przyjeżdża ze skanowania, część może być wciągana przez klasy konfiguracyjne, a gdzieś w kącie jest ukryty @Qualifier, który jest przywiązany do nazwy beana. W tym momencie chcesz zrobić ctrl+f po słowie „magia" i pójść pić herbatę. Ale można prościej.
Dobry sposób czytania takiego XML — nie próbować rozumieć wszystkiego „według kolejności linii", lecz odtwarzać mapę źródeł. Najpierw patrzysz na górę pliku i odpowiadasz sobie na dwa pytania: czy jest podłączony namespace context, i czy w pliku jest component-scan. Jeśli component-scan jest, wypisujesz base-package i od razu rozumiesz: część beanów nie będzie widoczna w XML, trzeba ich szukać w kodzie przez @Component/@Service/@Configuration.
Dalej przeglądasz wszystkie jawne <bean> i wypisujesz pary id -> class. To nadal najbardziej „czytelna" warstwa: jest bezpośrednio przed tobą. Po tym sprawdzasz, czy jest włączony annotation-config. Jeśli jest włączony (lub jest component-scan, który zazwyczaj to implikuje), powinieneś być gotowy zobaczyć, że część wiringu jest ukryta wewnątrz klas przez adnotacje. Czyli XML mówi „w kontekście jest taka-to klasa", a szczegóły wstrzykiwania zależności znajdziesz już w kodzie Java.
Jeśli chcesz sformalizować to całkowicie „bez heroizmu", oto mały schemat, który pomaga się nie zgubić:
flowchart TD
A["Otworzyłeś legacy XML"] --> B{"Jest context:component-scan?"}
B -- "Tak" --> C["Zapisałeś base-package i szukasz @Component w kodzie"]
B -- "Nie" --> D["Wszystkich beanów szukamy w bean wewnątrz pliku"]
C --> E{"Jest context:annotation-config (lub skanowanie)?"}
D --> E
E -- "Tak" --> F["Adnotacje wewnątrz klas wpływają na wiring/lifecycle"]
E -- "Nie" --> G["Adnotacje mogą być nieaktywne: wiring tylko z XML"]
To nie „uniwersalna prawda o wszystkich projektach Spring", ale jako reguła do czytania małych fragmentów legacy działa świetnie. Przestajesz panikować i zaczynasz metodycznie ustalać, gdzie dokładnie opisana jest każda zależność: w XML czy w adnotacjach w kodzie.
5. Mini-przykład ContextFlow
Przywiążmy wszystko do naszego projektu ContextFlow, żeby nie pozostało teorią o abstrakcyjnym Springu z książek z 2009 roku. Wyobraźmy sobie, że mamy mały fragment legacy, odpowiadający tylko za powiadomienia. Chcemy, żeby ten kawałek można było czytać osobno i żeby był „izolowaną wyspą", a nie próbą opisania całej aplikacji w XML (to już nie legacy, to kara).
Oto jeden z prawdziwych wzorców, który spotkasz: XML robi skanowanie po wąskim pakiecie i przy tym pozostawia jeden-dwa infrastrukturalne beany jako jawne <bean>. Na przykład senderzy są znajdowani przez component-scan, a serwis-wrapper pozostaje jawny:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Skanujemy tylko pakiet legacy, żeby nie wciągać do kontekstu zbędnych rzeczy -->
<context:component-scan base-package="com.example.contextflow.infrastructure.notification.legacy"/>
<!-- Ten bean deklarujemy jawnie, a wstrzyknięcie zależności będzie przez adnotacje -->
<bean id="dispatchService"
class="com.example.contextflow.application.service.LegacyNotificationDispatchService"/>
</beans>
Co tu jest ważne przy czytaniu:
Tag component-scan oznacza, że w pakiecie com.example.contextflow.infrastructure.notification.legacy gdzieś żyją komponenty. Ich nazw i liczby nie zobaczysz w XML. Znaczy to, że gdy zobaczysz w kodzie Java zależność od NotificationSender, musisz pamiętać: kandydat może przyjechać ze skanowania.
A dispatchService jest zadeklarowany jawnie. Jeśli używa konstruktora z @Autowired (jak w przykładzie wyżej), to krytyczne, żeby infrastruktura adnotacyjna była włączona. I tu znowu ważne: component-scan zwykle włącza przetwarzanie adnotacji z sensu. Dlatego osobnego <context:annotation-config/> nie dodawaliśmy.
Teraz przykład „drugiego typu", który często spotykamy w starszych konfiguracjach: żadnego skanowania, wszystkie beany są wymienione ręcznie, ale adnotacje są włączone, bo klasy używają @Autowired/@PostConstruct:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Tu nie ma skanowania, więc włączamy przetwarzanie adnotacji osobnym tagiem -->
<context:annotation-config/>
<!-- Wszystkie beany są wymienione ręcznie: wygodne, ale łatwo się zmęczyć -->
<bean id="notificationSender"
class="com.example.contextflow.infrastructure.notification.ConsoleNotificationSender"/>
<bean id="dispatchService"
class="com.example.contextflow.application.service.LegacyNotificationDispatchService"/>
</beans>
Gdy czytasz taki fragment, masz teraz bardzo czytelną myśl: „wszystko jest wymienione ręcznie, ale szczegóły wiringu mogą być w adnotacjach wewnątrz klas". Jest to szczególnie przydatne, gdy w legacy kodzie jednocześnie spotykane są i <constructor-arg ref="...">, i @Autowired na polach. Tak, wygląda to jak „dwie religie w jednym pliku", ale tak często bywa w prawdziwych firmach: migracja szła kawałkami, a nie idealną rewolucją według harmonogramu.
I ostatnia uwaga, która pomaga zachować jasną głowę. context:component-scan nierzadko znajduje nie tylko „zwykłe komponenty", ale i klasy @Configuration (bo one też są komponentami). Dlatego plik XML może być punktem wejścia, który wciąga do kontekstu cały kawałek konfiguracji Java przez skanowanie. Jeśli to zobaczyłeś — nie dziw się. Znasz już mental model: najpierw definicje, potem tworzenie. @Configuration po prostu generuje dodatkowe definicje, i tyle.
6. Typowe błędy przy pracy z context:*
Błąd nr 1: uważać, że context:annotation-config robi skanowanie.
To bardzo częsta pułapka: student widzi słowo „annotation" i myśli, że kontener „pójdzie szukać adnotacji po pakietach". Ale annotation-config nie szuka klas. Włącza przetwarzanie adnotacji u tych beanów, które już trafiły do kontekstu. Jeśli w XML nie ma <bean> i nie ma <context:component-scan>, to adnotacje same z siebie niczego nie zarejestrują.
Błąd nr 2: nie rozumieć, że component-scan sprawia, że część beanów jest niewidoczna w XML.
Gdy w pliku pojawia się <context:component-scan>, XML przestaje być pełną „mapą beanów". Część beanów będzie żyć tylko w kodzie Java jako klasy @Component. Nowicjusz często próbuje znaleźć wszystkie zależności wewnątrz XML i się frustruje. Prawidłowa strategia — zarejestrować base-package i od razu traktować to jako „część wiringu jest opisana w kodzie".
Błąd nr 3: skanowanie zbyt szerokiego pakietu i łapanie „dlaczego tu nagle pojawił się ten bean".
Jeśli w legacy XML stoi coś w stylu base-package="com.example", konfiguracja staje się nieprzewidywalna: do kontekstu mogą trafić aspekty, komponenty testowe, eksperymentalne klasy, a potem będziesz dwie godziny szukać, dlaczego masz NoUniqueBeanDefinitionException nie wiadomo skąd. Już to omawialiśmy w dniu o skanowaniu: reguła „skanuj wąsko i sensownie" jest równie ważna w Java config i w XML.
Błąd nr 4: duplikowanie context:annotation-config obok context:component-scan bez rozumienia sensu.
Często nie psuje to aplikacji, ale tworzy szum. Czytający plik zaczyna myśleć, że to dwa niezależne mechanizmy, które trzeba trzymać w głowie. W większości przypadków wystarczy jeden component-scan, bo on i skanowanie robi, i infrastrukturę adnotacyjną włącza. Jeśli w projekcie stoją oba, to najczęściej „historyczny ślad migracji", a nie istotny szczegół.
Błąd nr 5: zapominać, że adnotacje bez infrastruktury — to tylko naklejki.
Gdy czytasz legacy kod, łatwo pominąć, że w XML nie ma ani annotation-config, ani component-scan. Wtedy @Autowired, @PostConstruct, @Value i inne adnotacje mogą nie działać tak, jak oczekujesz. W efekcie widzisz w klasie ładny @PostConstruct, a aplikacja zachowuje się jakby go nie było. To nie mistyka — to po prostu wyłączona infrastruktura.
GO TO FULL VERSION