CodeGym /Kursy /Spring Core /Adnotacje w Spring XML

Adnotacje w Spring XML

Spring Core
Poziom 23, Lekcja 2
Dostępny

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.

1
Zadanie
Spring Core, poziom 23, lekcja 2
Niedostępne
`context:annotation-config` dla jawnie zarejestrowanych beanów
`context:annotation-config` dla jawnie zarejestrowanych beanów
1
Zadanie
Spring Core, poziom 23, lekcja 2
Niedostępne
`context:component-scan` do skanowania komponentów
`context:component-scan` do skanowania komponentów
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION