CodeGym /Kursy /Spring Boot /@Configuration i @Bean: beany

@Configuration i @Bean: beany

Spring Boot
Poziom 2, Lekcja 3
Dostępny

1. Kiedy adnotacje stereotypowe nie wystarczają

Component scanning rzeczywiście przypomina bajkę: stawiasz @Service, uruchamiasz aplikację i wszystko „samo". Ale dość szybko pojawia się obiekt, którego nie można lub nie chce się oznaczać adnotacją. Najczęściej jest to klasa z JDK lub biblioteki zewnętrznej, której nie kontrolujesz. I tu bajka zamienia się w krótki thriller: „Gdzie mój bean?"

Wyobraźmy sobie, że w naszym edukacyjnym catalog-service chcemy uzyskiwać „aktualny czas" nie bezpośrednio przez Instant.now(), a przez Clock. To zwykła klasa z Javy, i jest bardzo wygodna do testów i przewidywalnego zachowania. Piszemy serwis, który prosi Clock w konstruktorze – zgodnie z regułami DI wszystko jest piękne i przejrzyste:

package com.example.catalogservice.catalog.service;

import java.time.Clock;
import org.springframework.stereotype.Service;

@Service
public class CourseCatalogService {

    private final Clock clock;

    public CourseCatalogService(Clock clock) {
        // Zależność przychodzi z zewnątrz – łatwiej testować i zmieniać źródło czasu
        this.clock = clock;
    }
}

A potem Spring uczciwie mówi: „Chętnie pomogę, ale… Clock nie widziałem, nie tworzyłem i w ogóle słyszę po raz pierwszy". I dostajesz błąd przy starcie: kontener nie może złożyć grafu zależności.

I to jest normalne: Spring to nie telepatia. Nie jest zobowiązany zgadywać, że potrzebujesz Clock.systemUTC(), a nie Clock.systemDefaultZone().

W tym momencie pojawiają się dwa ważne narzędzia Spring Core, które dziś omawiamy:

  • @Configuration – klasa opisująca „jak składać" część aplikacji;
  • @Bean – metoda-fabryka, która jawnie rejestruje obiekt jako bean.

Żeby łatwiej wyobrazić sobie różnicę, oto mały schemat:

flowchart TD
    A["Klasa z @Service/@Component"] -->|component scanning| C["BeanDefinition"]
    B["@Bean-metoda w @Configuration"] -->|jawna rejestracja| C
    C --> D["ApplicationContext: przechowuje beany i powiązania"]

Stereotypy to ścieżka „Spring, znajdź mi te klasy po znacznikach". @Bean to ścieżka „Spring, oto konkretny obiekt, zrób z niego bean".

2. @Configuration: klasa-przepis dla kontenera

Jeśli adnotacje stereotypowe to „plakietki pracowników", to @Configuration to „schemat etatowy i schemat składania zespołu". Nie mówimy Spring „poszukaj kogoś podobnego", a mówimy: „Tu jawnie opisuję, jakie obiekty są potrzebne aplikacji i jak je tworzyć". Jest to szczególnie przydatne, gdy rejestrujesz klasy biblioteczne lub chcesz kontrolować proces tworzenia obiektu.

Technicznie @Configuration to zwykła klasa Java, bez magicznego DSL, która żyje wewnątrz twojego projektu, ale pełni infrastrukturalną rolę: opisuje beany. Wewnątrz niej zwykle znajdują się metody z @Bean.

Najbardziej zrozumiały przykład – rejestracja Clock:

package com.example.catalogservice.config;

import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TimeConfiguration {

    @Bean
    Clock systemClock() {
        // Jawnie wybieramy źródło czasu: UTC wygodne dla odtwarzalnych testów i logów
        return Clock.systemUTC();
    }
}

Zwróć uwagę na ważny moment: Clock to nie nasza klasa. Nie możemy i nie powinniśmy iść do JDK i dopisywać tam @Component. Dlatego tworzymy własną klasę konfiguracyjną i opisujemy tworzenie beana przez metodę fabrykującą.

Jeszcze jeden praktyczny niuans, który często jest pomijany: klasa konfiguracyjna też musi być znaleziona przez Spring. Znaczy musi leżeć w strefie component scanning – pod tym korzeniowym pakietem, od którego aplikacja skanuje swoje komponenty. Dlatego w catalog-service będziemy trzymać konfiguracje w pakiecie jak com.example.catalogservice.config.

3. @Bean: fabryka obiektów kontrolowana przez Spring

Gdy stawiasz @Bean na metodzie, mówisz kontenerowi: „Wynik tej metody to bean. Stwórz go i zarządzaj nim". Ważne jest postrzeganie @Bean nie jako „adnotacja dla ozdoby", a jako oficjalny punkt wejścia obiektu do świata Spring. Po tym obiekt żyje w ApplicationContext i może być wstrzykiwany do innych beanów przez DI.

Zwykle @Bean używa się dla dwóch typów rzeczy. Pierwszy – klasy zewnętrzne, jak Clock. Drugi – twoje klasy, ale z niestandardową logiką tworzenia: np. chcesz tworzyć obiekt przez metodę fabrykującą, zwrócić implementację przez interfejs lub skonstruować obiekt z wcześniej przygotowanymi parametrami.

Zróbmy małe sprawdzenie: upewnijmy się, że Clock naprawdę stał się beanem i jest dostępny z kontekstu. To przykład nie „jak robić w produkcji", a po prostu demonstracja faktu: kontekst przechowuje beany.

package com.example.catalogservice;

import java.time.Clock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class CatalogServiceApplication {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(CatalogServiceApplication.class, args);

        // Demonstracja: ręcznie pobieramy bean z kontekstu (w kodzie produkcyjnym tak się zwykle nie robi)
        Clock clock = context.getBean(Clock.class);
        System.out.println(clock.getZone()); // Z
    }
}

clock.getZone() wypisuje Z, bo zarejestrowaliśmy Clock.systemUTC().

Jeszcze jeden szczegół pomagający „rozwidzieć magię": bean ma nazwę. Domyślnie pochodzi z nazwy metody. W naszym przypadku bean będzie się nazywał systemClock. Czasem to wygodne – np. do diagnostyki lub gdy świadomie pracujesz z nazwami.

Jeśli koniecznie chcesz podać nazwę jawnie, można tak – pokażę krótko, bez zagłębiania się w „wojny nazewnicze":

package com.example.catalogservice.config;

import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TimeConfiguration {

    @Bean("utcClock")
    Clock systemClock() {
        // Nazwa beana podana ręcznie (przydatne, gdy świadomie pracujesz z kilkoma kandydatami)
        return Clock.systemUTC();
    }
}

Ale w normalnym życiu – i w naszym kursie też – polegamy głównie na wstrzykiwaniu po typie, a nie na łańcuchach z nazwami.

4. DI w @Bean: parametry-zależności

W tym miejscu zwykle pojawia się przyjemne „klik": okazuje się, że DI działa nie tylko w konstruktorach @Service, ale i bezpośrednio w metodach @Bean. To znaczy @Bean to nie tylko „stwórz obiekt", a „stwórz obiekt w kontekście grafu zależności". Spring potrafi podstawić do parametrów metody inne beany tak samo, jak podstawia je do konstruktora.

Wyobraźmy sobie, że chcemy zrobić serwis bez @Service – po prostu zwykłą klasę Java – a zarejestrować go jawnie przez @Bean. Na przykład żeby lepiej kontrolować montaż lub żeby pokazać studentom, że „bean ≠ klasa".

Sam serwis – zwykła klasa Java:

package com.example.catalogservice.catalog.service;

import java.time.Clock;

public class CourseCatalogService {

    private final Clock clock;

    public CourseCatalogService(Clock clock) {
        // Wszystko potrzebne do działania przychodzi przez konstruktor
        this.clock = clock;
    }
}

Teraz rejestrujemy go przez @Configuration:

package com.example.catalogservice.config;

import com.example.catalogservice.catalog.service.CourseCatalogService;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CatalogServiceConfiguration {

    @Bean
    CourseCatalogService courseCatalogService(Clock clock) {
        // Spring sam podstawi bean Clock do parametru metody
        return new CourseCatalogService(clock);
    }
}

Spójrz na sygnaturę: courseCatalogService(Clock clock). Nie tworzymy Clock wewnątrz serwisu przez Clock.systemUTC() i nie ukrywamy zależności. Prosimy o Clock jako zależność, a Spring podstawia potrzebny bean.

To bardzo ważny inżynierski nawyk: wszystko, czego obiekt potrzebuje do działania, powinno przychodzić z zewnątrz. Nieważne, przez co dokładnie – konstruktor komponentu czy parametry metody fabrykującej @Bean.

Jeśli opisać to jako mini-graf, wyjdzie niemal szkolny schemat, ale jest w nim siła:

flowchart LR
    Clock["Clock (bean)"] --> Svc["CourseCatalogService (bean)"]
    Conf["@Configuration"] -->|opisuje| Clock
    Conf -->|opisuje| Svc

Klasa konfiguracyjna nie zastępuje kontenera. Jedynie mówi kontenerowi: „Oto takie obiekty mamy, i oto jak się składają".

5. Mini-przykład dla catalog-service

Żeby temat nie pozostał „w próżni", przywiążmy go do naszego projektu przewodniego catalog-service. Nawet w małym edukacyjnym serwisie bez bazy i bez Security pojawiają się obiekty infrastrukturalne, które wygodnie jest trzymać jako beany. To daje jednolity sposób pracy z nimi, poprawia testowalność i zmniejsza pokusę „napiszę new gdzieś z boku, żeby po prostu działało".

Zróbmy klasę konfiguracyjną, która rejestruje Clock i na przykład DateTimeFormatter do formatowania dat w jednym miejscu. Obie klasy to nie nasze, ale aplikacja często ich potrzebuje:

package com.example.catalogservice.config;

import java.time.Clock;
import java.time.format.DateTimeFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonInfrastructureConfiguration {

    @Bean
    Clock systemClock() {
        // Jednolite źródło „aktualnego czasu" dla całej aplikacji
        return Clock.systemUTC();
    }

    @Bean
    DateTimeFormatter isoDateFormatter() {
        // Jednolity formatter – żeby nie mnożyć „własnych reguł" po całym kodzie
        return DateTimeFormatter.ISO_LOCAL_DATE;
    }
}

Teraz każdy komponent – lub bean stworzony przez @Bean – może poprosić o Clock i DateTimeFormatter przez konstruktor. Na przykład zróbmy mały „pomocnik" do ładnego formatowania daty uruchomienia kursu. Potrzebuje zewnętrznego DateTimeFormatter, i to właśnie przypadek, gdzie @Bean wygodnie działa też dla twoich klas:

package com.example.catalogservice.catalog.support;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class LaunchDateFormatter {

    private final DateTimeFormatter formatter;

    public LaunchDateFormatter(DateTimeFormatter formatter) {
        // Zależność – zewnętrzny formatter, a nie „zaszyty na sztywno" wewnątrz klasy
        this.formatter = formatter;
    }

    public String format(LocalDate date) {
        // Cały sens klasy – jednolite formatowanie dat
        return formatter.format(date);
    }
}

I rejestrujemy go:

package com.example.catalogservice.config;

import com.example.catalogservice.catalog.support.LaunchDateFormatter;
import java.time.format.DateTimeFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CatalogSupportConfiguration {

    @Bean
    LaunchDateFormatter launchDateFormatter(DateTimeFormatter isoDateFormatter) {
        // Parametr metody to zależność, którą podciągnie Spring
        return new LaunchDateFormatter(isoDateFormatter);
    }
}

To wygląda jak „ręczny montaż", ale kluczowa różnica od ręcznego montażu w main() z pierwszego wykładu polega na tym, że teraz wszystko to żyje w kontenerze. To znaczy dostajesz scentralizowane tworzenie obiektów, jednolity graf zależności i możliwość spokojnego używania DI dalej w projekcie.

6. Tematyczne konfiguracje zamiast „god-config"

Gdy deweloper po raz pierwszy rozumie @Configuration i @Bean, często pojawia się pokusa: „O! To znaczy mogę zebrać całą aplikację w jednym miejscu". I po kilku dniach w projekcie pojawia się plik AppConfiguration.java na 800 linii, gdzie obok żyją zegary, formattery, serwisy, kontrolery, „tymczasowe kostyle na chwilkę" i jedna tajemnicza private static final String DO_NOT_TOUCH = "działa".

Żeby nasz catalog-service pozostał czytelny, zasada jest bardzo prosta: konfiguracja powinna być tematyczna. Nie „cała konfiguracja świata", a „konfiguracja czasu", „konfiguracja katalogu", „konfiguracja komponentów pomocniczych". To nie architektoniczny patos, a zwykła troska o psychikę przyszłego czytelnika, włącznie z tobą za dwa tygodnie.

Przykład zdrowego kierunku struktury – na razie bardzo uproszczony, bez przyszłych warstw kursu:

com.example.catalogservice/
|-- CatalogServiceApplication
`-- config/
    |-- CommonInfrastructureConfiguration
    |-- CatalogServiceConfiguration
    `-- CatalogSupportConfiguration

Dlaczego to dobrze działa właśnie na poziomie Junior:

1) szybko znajdziesz, gdzie jest tworzony Clock – w konfiguracji infrastruktury – zamiast grać w find in files po całym projekcie;

2) klasy konfiguracyjne pozostają małe i naprawdę da się w nich czytać kod, a nie tylko płakać;

3) łatwiej rozumieć odpowiedzialność: @Configuration to „montaż obiektów", a nie miejsce na logikę biznesową.

Jeszcze jedna użyteczna zasada: metoda @Bean powinna być krótka i przewidywalna. Jeśli wewnątrz niej zaczyna się złożona logika – pętle, zapytania do sieci, odczyt plików, obliczenia – to najczęściej po prostu przeniosłeś logikę biznesową nie tam, gdzie trzeba. Konfiguracja to nie kuchnia na bigos gotowany 6 godzin. To raczej ekspres do kawy: nacisnąłeś przycisk – dostałeś kubek. Szybko i stabilnie.

7. Typowe błędy przy pracy z @Configuration i @Bean

Błędy z @Configuration i @Bean zazwyczaj nie są związane z „nie tą adnotacją", a z nieprawidłowym obrazem w głowie: człowiek myśli, że Spring to zestaw naklejek, a kontener to po prostu biblioteka. Dlatego poniżej nie „wstydliwe wpadki początkującego", a bardzo popularne pułapki, na które natrafiają prawie wszyscy, łącznie z osobami, które potem będą pisać „10 lat doświadczenia komercyjnego".

Błąd nr 1: zapomnieć, że klasa konfiguracyjna też musi trafić do component scanning.
Możesz idealnie napisać @Configuration i @Bean, ale umieścić konfigurację w pakiecie, który nie jest skanowany, lub po prostu stworzyć ją w „bocznym" module/folderze – i Spring go nie zobaczy. Z zewnątrz wygląda to tak, jakby @Bean „nie zadziałał", ale w rzeczywistości kontener po prostu nie wie, że ta klasa istnieje.

Błąd nr 2: tworzyć @Configuration przez new i oczekiwać, że Spring magicznie się podłączy.
Czasem chce się zrobić szybko: new TimeConfiguration().systemClock(). To stworzy Clock, ale będzie to zwykły obiekt, nie bean, i kontener o nim nie będzie wiedział. Najczęstszy problem potem brzmi tak: „Przecież stworzyłem Clock… dlaczego Spring nadal narzeka, że nie ma beana?" Bo kontener widzi tylko to, co jest zarejestrowane wewnątrz kontenera.

Błąd nr 3: ukrywać zależności wewnątrz klas, choć bean już jest.
Klasyka: już zrobiłeś @Bean Clock systemClock(), ale w serwisie i tak napisałeś private final Clock clock = Clock.systemUTC();. Wychodzą dwa różne źródła „aktualnego czasu": jedno z kontenera, drugie własnoręczne. To łamie ideę DI: obiekt znowu „sam sobie złożył" zależność, i z zewnątrz tego nie widać.

Błąd nr 4: zamienianie @Configuration w miejsce na logikę biznesową.
Jeśli w metodzie @Bean zaczynasz filtrować kursy, liczyć statystyki lub generować HTML – tak, i to się zdarza – mieszasz montaż aplikacji z logiką aplikacji. W rezultacie konfiguracja staje się nieprzewidywalna i ciężka, start aplikacji spowalnia, a debugowanie tego jest boleśniejsze niż patrzenie na merge-konflikt w YAML.

Błąd nr 5: przypadkowo zarejestrować dwa beany tego samego typu i dziwić się niejednoznaczności.
Dziś jeszcze nie omawiamy mechanizmów wyboru beana przy kilku kandydatach, ale ogólna zasada jest użyteczna już teraz: jeśli stworzysz dwa beany Clock – np. jeden UTC, drugi systemDefault – Spring nie będzie mógł „zgadnąć", który dokładnie wstrzyknąć do CourseCatalogService, i aplikacja padnie przy starcie. To nie złośliwość Spring, a uczciwe wymaganie: wybór powinien być jednoznaczny.

1
Zadanie
Spring Boot, poziom 2, lekcja 3
Niedostępne
Jawna rejestracja `Clock` przez `@Bean`
Jawna rejestracja `Clock` przez `@Bean`
1
Zadanie
Spring Boot, poziom 2, lekcja 3
Niedostępne
Zwykła klasa Java zarejestrowana przez `@Bean`
Zwykła klasa Java zarejestrowana przez `@Bean`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION