CodeGym /Kursy /Spring Boot /Stereotypy i component scanning

Stereotypy i component scanning

Spring Boot
Poziom 2, Lekcja 2
Dostępny

1. Adnotacje stereotypowe: kontener bez telepatii

Jeśli wyobrazić sobie kontener Spring jako bardzo odpowiedzialnego menedżera, to jest gotowy składać ci aplikację: tworzyć obiekty, podstawiać zależności, pilnować, żeby wszystko startowało. Ale jest problem – telepatii nie ma. Twój projekt może zawierać setki klas: modele domenowe, narzędzia, helpery testowe, szkice „do usunięcia później" i nawet legendarny NewNewFinal2Service.

Kontener potrzebuje prostej odpowiedzi na pytanie: które klasy uważać za „części aplikacji", którymi trzeba zarządzać? Gdyby Spring próbował „zrobić beanem absolutnie wszystko", wyszłoby dziwnie. Po pierwsze, nie wszystkie klasy mają sens jako obiekty długiego życia. Po drugie, wiele klas powinno być tworzonych sytuacyjnie – na przykład obiekty DTO lub tymczasowe struktury. Po trzecie, łatwo byłoby dostać konflikt: masz klasę Money lub CourseCard, a Spring nagle próbuje złożyć ją jako serwis. To jak mianować kubek „odpowiedzialnym za księgowość": kubek może i dobry, ale rola wyraźnie nie jego.

Stąd pojawia się ważna „społeczna" część Spring: oznaczasz klasy adnotacjami, które mówią kontenerowi: „ta klasa jest komponentem aplikacji". Spring odczytuje te znaczniki, zbiera listę kandydatów i rejestruje je jako beany.

Od razu ostrożnie utrwalamy jedną myśl: adnotacja stereotypowa nie dodaje logiki biznesowej. Nie robi klasy mądrzejszą. Jedynie mówi: „kontenerze, proszę, zwróć na mnie uwagę". To jak plakietka na konferencji: plakietka nie robi cię ekspertem, ale pozwala ochronie wpuścić cię do środka.

2. @Component: minimalny marker „to jest część aplikacji"

W Spring jest podstawowa adnotacja stereotypowa – @Component. Oznacza najprostszą rzecz: „ta klasa jest komponentem, zarejestruj ją jako bean". Jeśli nie jesteś pewien, jaką „profesję" ma twoja klasa, ale rozumiesz, że powinna żyć w kontenerze i uczestniczyć w DI, @Component to normalny start.

Ważne jest rozumienie, że @Component zwykle stawia się na klasy, które naprawdę są „pomocniczymi detalami" aplikacji: normalizatory, proste mappery, wewnętrzne walidatory, generatory wartości, małe narzędzia, które wygodnie trzymać jako beany. Przy tym nie próbujemy zrobić z kontenera śmietnika: beanem powinno być to, czym naprawdę trzeba zarządzać.

Oto mini-przykład w duchu naszego przyszłego catalog-service: normalizator slugów. Jest mały, ale może być używany w różnych miejscach, i to właśnie przypadek, gdy miło jest trzymać go w kontenerze jako wielokrotnie używany komponent.

package com.example.catalogservice.catalog.support;

import org.springframework.stereotype.Component;

@Component // Mówimy Spring: tę klasę trzeba zobaczyć przy skanowaniu i zarejestrować jako bean
public class SlugNormalizer {

    public String normalize(String slug) {
        // Przykład „pomocniczej" logiki: nie reguła biznesowa, a narzędziowa normalizacja wejścia
        return slug.trim().toLowerCase();
    }
}

Teraz ta klasa jest kandydatem na bean. Ale – i to jest najważniejsze „ale" dzisiejszego wykładu – stanie się beanem tylko jeśli Spring w ogóle ją znajdzie. A znaleźć może ją przez component scanning, o którym porozmawiamy za chwilę.

Jeszcze jeden użyteczny moment: bean to nie „klasa". Bean to obiekt, stworzony przez kontener. Adnotacja jedynie pomaga kontenerowi zrozumieć, że obiekt trzeba stworzyć. Mówiąc umownie, SlugNormalizer.class to po prostu bajtkod i metadane, a SlugNormalizer jako bean to konkretna instancja, która leży w ApplicationContext i może być „wydana" lub „wstrzyknięta" jako zależność.

3. @Service, @Repository, @Controller: role komponentów

Gdy projekt jest mały, można by żyć na samym @Component. Ale bardzo szybko czytanie kodu staje się ciężkie: co z tego jest serwisem? co repozytorium? co warstwa web? co po prostu helper? Dlatego Spring oferuje bardziej „mówiące" stereotypy: @Service, @Repository, @Controller.

Z inżynierskiego punktu widzenia to nadal sposób rejestracji beana – w skrócie, „warianty komponentu". Z ludzkiego punktu widzenia to ogromna pomoc: otwierasz klasę i od razu widzisz, jaką rolę gra. To szczególnie ważne dla Junior dewelopera: mózg i tak jest zajęty tym, żeby nie pomylić ApplicationContext z „kontekstem w głowie", więc dodatkowa jasność w kodzie to prawdziwa troska o psychikę.

Porównajmy te adnotacje jak „plakietki zawodów":

Adnotacja Do czego zwykle używana Co daje (poza rejestracją)
@Component Ogólny komponent bez wyraźnej roli Po prostu „to jest bean"
@Service Warstwa serwisowa, logika aplikacyjna Czytelność: „tu jest serwis"
@Repository Dostęp do danych (nawet jeśli dane in-memory) Czytelność i (w pełnym Spring) użyteczne efekty infrastrukturalne
@Controller Warstwa web (kontrolery MVC) Czytelność i udział w infrastrukturze webowej

Teraz ważne dla nas jest nie web i nie baza, a właśnie logika ról. W naszym catalog-service te role wyglądają naturalnie: repozytorium przechowuje i zwraca dane katalogu, serwis decyduje „jak" to robić, a kontroler jest tu nie ze względu na szczegóły HTTP, ale żeby od razu była widoczna osobna warstwa webowa. Weźmy znowu mini-graf: CourseCatalogRepository to kontrakt dostępu do danych katalogu, a InMemoryCourseCatalogRepository – jego konkretna implementacja in-memory.

Mini-szkielety takich klas mogą wyglądać tak.

Serwis:

package com.example.catalogservice.catalog.service;

import com.example.catalogservice.catalog.repository.CourseCatalogRepository;
import org.springframework.stereotype.Service;

@Service // Rola: logika aplikacyjna (warstwa serwisowa)
public class CourseCatalogService {

    public CourseCatalogService(CourseCatalogRepository repository) {
        // DI przez konstruktor: Spring sam przekaże bean repozytorium,
        // który implementuje potrzebny kontrakt
        // Tu celowo nie ma logiki: to „szkielet", żeby zobaczyć rolę i zasadę wstrzykiwania
    }
}

Konkretna implementacja repozytorium in-memory (na razie tylko zaślepka):

package com.example.catalogservice.catalog.repository;

import java.util.List;
import org.springframework.stereotype.Repository;

@Repository // Rola: dostęp do danych (na razie konkretna implementacja in-memory kontraktu)
public class InMemoryCourseCatalogRepository implements CourseCatalogRepository {

    @Override
    public List<String> findAllSlugs() {
        // Zaślepka: później pojawią się tu prawdziwe dane (np. kolekcja, baza danych itd.)
        return List.of("spring-boot", "spring-core");
    }
}

Kontroler (na razie pusty, tylko żeby zobaczyć rolę):

package com.example.catalogservice.catalog.web;

import org.springframework.stereotype.Controller;

@Controller // Rola: warstwa web (punkt wejścia dla HTTP/MVC), na razie bez metod
public class CourseCatalogController {
    // Puste celowo: teraz ważny jest tylko marker roli
}

Tu szczególnie ważne jest, żeby nie wpaść w pułapkę: „aha, skoro @Service, to musi tam być logika biznesowa". Nie. Adnotacja nie zmusza cię do pisania dobrej logiki. Jedynie sprawia, że rola klasy jest jawna i daje Spring powód do zarejestrowania beana.

Jeszcze jeden ważny szczegół: te adnotacje nie istnieją dla ozdoby. W prawdziwych projektach konwencje działają jak język zespołu. Gdy widzisz @Repository, oczekujesz warstwy dostępu do danych. Gdy widzisz @Controller, oczekujesz punktu wejścia warstwy web. Gdy widzisz @Service, oczekujesz „centrum logiki aplikacyjnej", a nie „klasy z pięcioma metodami statycznymi na każdą okazję". I takie oczekiwania to też część architektury.

4. Component scanning: jak Spring szuka komponenty po pakietach

Dotarliśmy do mechanizmu, który sprawia, że wszystkie te adnotacje są użyteczne: component scanning. Odpowiada on na praktyczne pytanie: „jak kontener w ogóle dowiaduje się, że mam CourseCatalogService lub SlugNormalizer, i że trzeba je zarejestrować?"

Component scanning to proces, w którym Spring przy starcie aplikacji przegląda klasy w określonych pakietach i szuka wśród nich kandydatów na beany. Kandydatami stają się klasy oznaczone odpowiednimi adnotacjami, np. @Component, @Service, @Repository, @Controller. Potem Spring rejestruje te klasy w kontenerze, a następnie tworzy instancje i łączy zależności.

Upraszczając do „obrazka na serwetce", proces wygląda mniej więcej tak:

flowchart TD
    A["Uruchomienie aplikacji"] --> B["Określamy base package (od czego skanować)"]
    B --> C["Skanujemy klasy w pakietach i podpakietach"]
    C --> D["Znajdujemy klasy z adnotacjami stereotypowymi"]
    D --> E["Rejestrujemy bean definitions w kontenerze"]
    E --> F["Tworzymy beany i wykonujemy wiring zależności"]
    F --> G["ApplicationContext gotowy"]

Zwróć uwagę na kluczowy moment: skanowanie odbywa się nie „po całym systemie plików", a po pakietach. W Javie jest to naturalne, bo struktura pakietów zwykle odpowiada strukturze katalogów w src/main/java. Spring orientuje się na nazwę pakietu, a nie na to, „w którym folderze lubisz trzymać klasy".

I tu pojawia się bardzo praktyczna prawda, z której wyrosła ogromna liczba bugów na wczesnych etapach nauki: jeśli twoja klasa leży poza strefą skanowania, dla Spring nie istnieje. Może świetnie się kompilować. IDE może ją widzieć. Możesz nawet wywołać ją ręcznie przez new. Ale kontener jej nie zarejestruje i DI na niej nie zadziała.

Żeby poczuć różnicę, użyteczne jest trzymanie w głowie trzech stanów klasy:

  • klasa istnieje w kodzie;
  • klasa jest oznaczona jako komponent;
  • klasa jest znaleziona przez skanowanie i zarejestrowana jako bean.

Dopóki nie są spełnione punkty 2 i 3 – lub nie użyto innego sposobu rejestracji – kontener po prostu nie będzie wiedział, że to „część aplikacji".

5. Bazowy pakiet: wpływ położenia klasy main

Słowo „base package" brzmi jak coś nudnego, ale w rzeczywistości to jeden z głównych dźwigni tego, czy Spring „widzi" twój kod. W typowej aplikacji Spring Boot bazowy pakiet skanowania określany jest bardzo prostą regułą: Spring zaczyna skanowanie od pakietu, w którym leży klasa main aplikacji – ta sama klasa z @SpringBootApplication.

W naszym kursie chcemy, żeby catalog-service miał czytelną strukturę pakietów. Mniej więcej taką – na razie bez szczegółów, tylko jako punkt orientacyjny:

com.example.catalogservice
├─ CatalogServiceApplication
├─ catalog
│  ├─ repository
│  ├─ service
│  └─ web
└─ config

Jeśli CatalogServiceApplication leży w pakiecie com.example.catalogservice, to skanowanie „zobaczy" wszystko, co leży niżej w drzewie pakietów: com.example.catalogservice.catalog.*, com.example.catalogservice.config.* i tak dalej.

Oto normalny „korzenny" main-class:

package com.example.catalogservice;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // Ta adnotacja włącza autokonfigurację i component scanning od pakietu com.example.catalogservice
public class CatalogServiceApplication {
    // Ważne: położenie tej klasy wyznacza „parasol" base package dla skanowania
}

A oto typowa pułapka początkującego: umieszczasz klasę main zbyt głęboko, np. w com.example.catalogservice.app, bo „to przecież aplikacja, niech będzie w app". W rezultacie bazowy pakiet skanowania staje się com.example.catalogservice.app, a pakiet com.example.catalogservice.catalog okazuje się sąsiednim, a nie zagnieżdżonym. To znaczy component scanning już tam nie dotrze.

package com.example.catalogservice.app;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // Base package stanie się com.example.catalogservice.app, i sąsiednie pakiety nie będą skanowane
public class CatalogServiceApplication {
    // Ta wersja wygląda „prawie tak samo", ale wynik dla component scanning będzie zupełnie inny
}

Czym to się kończy w praktyce? Bardzo przewidywalnie. Piszesz @Service:

package com.example.catalogservice.catalog.service;

import org.springframework.stereotype.Service;

@Service
public class CourseCatalogService {
}

A aplikacja przy starcie zachowuje się tak, jakby serwisu nie było. Jeśli gdzieś jest potrzebny jako zależność, dostaniesz błąd w stylu „potrzebny bean tego typu, ale nie znaleziono". W prawdziwych logach komunikat będzie dłuższy, ale sens mniej więcej taki:

... required a bean of type 'CourseCatalogService' that could not be found

I to nie „Spring się zepsuł". To Spring uczciwie mówi: „nie widziałem tej klasy przy skanowaniu, więc nie zarejestrowałem beana".

Najzdrowszy wniosek z tej sekcji: klasa main powinna leżeć wystarczająco wysoko w hierarchii pakietów, żeby być „parasolem" nad całą aplikacją. Dla naszego catalog-service to pakiet com.example.catalogservice, jak w docelowej strukturze projektu. Im wcześniej przyzwyczaisz się do tej reguły, tym mniej będziesz mieć poczucia, że „Spring to magia i loteria".

Technicznie Spring pozwala konfigurować strefy skanowania ręcznie, ale dla początkującego to częściej nie rozwiązanie, a sposób na skomplikowanie sobie życia. W projekcie edukacyjnym znacznie prościej i lepiej trzymać klasę main w korzeniowym pakiecie.

6. Diagnostyka i samokontrola

Na tym etapie nauki chciałoby się, żeby błąd wyglądał tak: „Drogi dewelopero, zapomniałeś adnotacji, popraw linię 12". Niestety – lub na szczęście, jeśli lubisz przygody – Spring częściej raportuje inaczej: „nie mogę stworzyć beana X, bo potrzebuje beana Y, a Y nie znaleziono". Dla początkującego brzmi to jak zagadka sfinksa, ale w rzeczywistości diagnostyka jest całkiem przyziemna.

Najczęściej przyczyny są dwie. Albo klasa nie jest oznaczona adnotacją stereotypową, albo leży poza strefą component scanning. Czasem to jeszcze zwykła literówka w pakiecie lub sytuacja, gdy stworzyłeś klasę, ale nie trafiła do budowania – na przykład nie ten source root – ale to zazwyczaj widać po IDE.

Do debugowania użyteczne jest pamiętanie, że ApplicationContext to „rejestr beanów". Jeśli bean jest zarejestrowany, kontekst może go wydać. Ale tu ważne jest, żeby nie nauczyć się złego stylu: getBean(...) to narzędzie diagnostyki i demonstracji, a nie styl pisania kodu aplikacyjnego.

Oto mały kawałek kodu pokazujący sam princyp. W prawdziwym projekcie nie będziesz tak pisać kodu biznesowego – to właśnie „sprawdzenie, że bean istnieje":

import org.springframework.context.ApplicationContext;

// ctx – twój ApplicationContext (np. uzyskany w teście lub z punktu startu aplikacji)
SlugNormalizer normalizer = ctx.getBean(SlugNormalizer.class); // Jeśli beana nie ma – problem w rejestracji/skanowaniu
System.out.println(normalizer.normalize("  Spring-Boot ")); // spring-boot

Jeśli ta linia pada z błędem „nie ma takiego beana", znaczy to albo że SlugNormalizer nie był zarejestrowany, albo uruchamiasz aplikację nie tym kontekstem, albo klasa jest poza skanowaniem. I wtedy wracasz do prostej listy kontrolnej w głowie – bez drukowania na papierze, jesteśmy w IT: adnotacja jest na miejscu? pakiet pod base package? klasa main w właściwym pakiecie?

Jeszcze jedna bardzo przyziemna reguła: jeśli Spring pisze, że „nie znalazł beana", to znaczy, że naprawdę go nie znalazł. To nie wskazówka, nie filozofia i nie test charakteru. To po prostu fakt.

7. Typowe błędy: stereotypy i scanning

Błędy w tym temacie często wyglądają jak „u mnie wszystko poprawnie, ale Spring nie widzi". W rzeczywistości prawie zawsze gdzieś naruszona jest jedna z reguł: klasa nie jest oznaczona, klasa nie jest znajdowana przez skanowanie lub przypadkowo zepsułeś strukturę pakietów. Poniżej – najczęstsze pułapki, na które natrafiają początkujący, a czasem nawet nie początkujący, a po prostu zmęczeni.

Błąd nr 1: „Stworzyłem klasę, więc Spring musi ją znaleźć".
To bardzo ludzkie oczekiwanie: skoro klasa jest w projekcie, kontener ją zobaczy. Ale Spring nie rejestruje beanów z powietrza. Jeśli na klasie nie ma adnotacji stereotypowej – lub innego sposobu rejestracji – dla kontenera to po prostu klasa na classpath, nic więcej. W rezultacie DI nie działa i dostajesz „bean not found". Leczy się to prawie zawsze jedną myślą: „kontener zarządza tylko obiektami, które są jawnie zarejestrowane".

Błąd nr 2: „Postawiłem @Service, a i tak nie działa – znaczy adnotacja się zepsuła".
Adnotacja tu zwykle nie jest winna. Najczęściej problem leży w strefie skanowania: klasa jest oznaczona, ale leży poza base package. W rezultacie Spring jej nie rejestruje i dalej wszystko pada w łańcuchu. Ten bug jest szczególnie podstępny, gdy przypadkowo umieścisz klasę main zbyt głęboko i część pakietów staje się „sąsiednią", a nie zagnieżdżoną. Wtedy połowa aplikacji jest widoczna dla Spring, a połowa – nie, i masz wrażenie, że „magia raz działa, raz nie".

Błąd nr 3: „Oznaczę wszystko @Component, żeby na pewno działało".
Tak rzeczywiście często „zadziała", ale czytelność projektu zacznie szybko cierpieć. Gdy wszystkie klasy mają tę samą plakietkę, tracisz semantykę. @Service, @Repository, @Controller istnieją nie dlatego, że Spring lubi mnożyć adnotacje, ale dlatego, że ludzie lubią rozumieć kod. Dobry styl: @Component – do ogólnych, @Service – do serwisów, @Repository – do warstwy danych, @Controller – do warstwy web.

Błąd nr 4: „Umieszczę klasę w pakiecie external.*, żeby było ładnie".
Samo słowo external w pakiecie nic złego nie robi, ale często jest sygnałem: „klasa poza naszą główną strukturą". I łatwo dostać sytuację, gdy pakiet jest poza component scanning. Wtedy klasa nie jest znaleziona, bean nie jest stworzony, DI nie działa. W projekcie edukacyjnym prawie zawsze lepiej trzymać komponenty wewnątrz korzeniowego pakietu aplikacji, pod parasolem klasy main, żeby skanowanie było przewidywalne.

Błąd nr 5: „Skoro kontener Spring jest inteligentny, będę wszędzie nosić ApplicationContext i wyciągać beany ręcznie".
Technicznie jest to możliwe, i czasem początkujący tak „naprawia" problem: zamiast skonfigurować skanowanie, zaczyna robić context.getBean(...) w klasach aplikacyjnych. To zamienia kontener w globalny punkt dostępu – service locator – i szybko łamie sam sens DI. Prawidłowe podejście jest prawie zawsze odwrotne: zrobić zależności jawnymi, przez konstruktor, a nie ukrywać ich za „magicznym wydawaniem" z kontekstu.

1
Zadanie
Spring Boot, poziom 2, lekcja 2
Niedostępne
Komponent w pakiecie podstawowym
Komponent w pakiecie podstawowym
1
Zadanie
Spring Boot, poziom 2, lekcja 2
Niedostępne
Role `@Repository`, `@Service` i `@Controller`
Role `@Repository`, `@Service` i `@Controller`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION