CodeGym /Kursy /Spring Core /Jak wartość z Environment trafia do beana

Jak wartość z Environment trafia do beana

Spring Core
Poziom 12, Lekcja 3
Dostępny

1. Od klucza w .properties do beana

Plik jest już podłączony do Environment przez @PropertySource i na tym etapie klucze stały się częścią ogólnego obrazu konfiguracyjnego aplikacji. Teraz pytanie staje się bardzo praktyczne: jak konkretna wartość trafia do klasy, która jej używa? Tu mamy dwa codzienne szlaki — krótki @Value i bardziej jawne czytanie przez Environment.

Z zewnątrz często wygląda to tak: napisałeś klucz w contextflow.properties, postawiłeś @Value i wartość jakoś sama przyjechała. Ale Spring nie jest telepatą. Wykonuje dość zrozumiałą sekwencję kroków: zbiera źródła ustawień, tworzy Environment, znajduje klucz, podstawia wartość, a potem wstrzykuje do beana. Jeśli trzymasz ten szlak w głowie, debugowanie staje się spokojniejsze, a @Value przestaje być magiczną naklejką.

Upraszczając, ścieżka wygląda tak:

flowchart TD
  A["Property sources
JVM props, env vars, .properties"] --> B["Environment
przechowuje i szuka właściwości"] B --> C["Placeholder resolution
${key} -> 'value'"] C --> D["Injection point
constructor param / @Bean param"] D --> E["Bean instance
gotowy do pracy"]

Tu ważne jest uchwycenie myśli: @Value — to nie „czytanie pliku". To instrukcja dla kontenera, żeby w konkretne miejsce podstawił wartość znalezioną przez Environment i reguły placeholder resolution. Dlatego w tej lekcji będziemy patrzeć na dwa style, które są naprawdę potrzebne w codziennym życiu: krótki i wygodny @Value, oraz bardziej jawne, „wszystko widzę rękami" podejście przez Environment.

2. @Value jako krótki kabel

@Value często lubi się za kompaktowość: napisałeś jedną linijkę — i już można drukować nazwę aplikacji w konsoli, nie ciągnąc do kodu czytania plików i parsowania. Ale @Value ma charakter: jeśli stosować go bez dyscypliny, klasa zamienia się w choinkę z anotacjami, a konfiguracja — w „magiczne stringi" po całym projekcie. Dlatego naszym celem — używać @Value tak, żeby pomagało czytelności, a nie psuło jej.

Zacznijmy od najbardziej typowego przykładu: ScenarioRunner chce znać nazwę aplikacji, żeby ładnie drukować start.

package com.example.contextflow.application.scenario;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ScenarioRunner {
    private final String appName;

    public ScenarioRunner(
            // @Value czyta wartość przez Environment i placeholder resolution
            // :ContextFlow — to domyślna wartość, jeśli klucz nie jest podany
            @Value("${contextflow.app-name:ContextFlow}") String appName
    ) {
        // Iniekcja przez konstruktor czyni zależność (ustawienie) częścią kontraktu klasy
        this.appName = appName;
    }

    public void run() {
        // Wartość przyszła z konfiguracji jeszcze przy starcie aplikacji
        System.out.println("Starting " + appName); // Starting ContextFlow
    }
}

Tu od razu kilka „właściwych" rzeczy. Wartość jest wstrzykiwana do konstruktora, czyli staje się częścią kontraktu klasy: żeby złożyć ScenarioRunner, potrzeba nazwy. Przy tym mamy domyślną wartość ContextFlow, więc aplikacja będzie mogła startować nawet jeśli klucz zapomniano podać (choć w prawdziwym projekcie dla nazwy zazwyczaj to ok, a dla innych parametrów — niekoniecznie).

Dokładnie tak samo można wstrzykiwać dwie-trzy proste wartości do serwisu. Na przykład ReportingService może znać stringowy format raportu i kod lokalizacji (na razie stringowy — do typowanego bindingu jeszcze nie doszliśmy).

package com.example.contextflow.application.reporting;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class ReportingService {
    private final String reportFormat;
    private final String defaultLocale;

    public ReportingService(
            // Krótki wariant: wartości czytane bezpośrednio w miejscu użycia
            @Value("${contextflow.report.format:text}") String reportFormat,
            @Value("${contextflow.locale.default:pl}") String defaultLocale
    ) {
        // Zapisujemy do pól final: ustawienie powinno pojawić się raz przy tworzeniu i nie "wędrować" po obiekcie
        this.reportFormat = reportFormat;
        this.defaultLocale = defaultLocale;
    }
}

Uwaga dla zdrowego rozsądku: @Value doskonale działa, gdy wartości jest jedna-dwie i naprawdę „żyją" blisko miejsca użycia. Gdy wartości robi się 8–10, to już sygnał: albo klasa za dużo wie, albo czas przenieść część czytania ustawień na granicę konfiguracyjną (do @Configuration), albo projektujesz nie serwis, lecz „kombajn uniwersalny".

3. Wartości obowiązkowe i domyślne

Najgorszym błędem w konfiguracji jest nie padnięcie przy starcie, lecz „ciche nieprawidłowe zachowanie". Aplikacja jakby działa, ale wysyła powiadomienia nie tam, pisze pliki nie do tego folderu, a raporty „z jakiegoś powodu znikają". Dlatego ważne jest od samego początku rozróżnianie: które klucze są obowiązkowe, a które można spokojnie zastąpić domyślną wartością. Domyślna wartość — to nie sposób na „żeby na pewno nie padało", lecz świadoma decyzja: „w tym środowisku bezpieczne jest tak".

Jeśli wartość jest obowiązkowa, piszemy bez :default. Na przykład niech domyślny kanał powiadomień będzie obowiązkowym ustawieniem (w projekcie szkoleniowym można zrobić i domyślną wartość, ale teraz warto zobaczyć fail-fast).

package com.example.contextflow.application.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    private final String defaultChannel;

    public NotificationDispatchService(
            // Bez :default — klucz jest obowiązkowy. Brak klucza -> aplikacja padnie przy starcie (fail-fast)
            @Value("${contextflow.notifications.default-channel}") String defaultChannel
    ) {
        this.defaultChannel = defaultChannel;
    }
}

Jeśli klucza nie ma, kontener nie będzie mógł stworzyć beana — i padnie przy starcie. To boli dokładnie raz, za to potem nie spędzasz wieczoru na dochodzeniu „dlaczego to wysyła nie tam".

A teraz przykład, gdzie domyślna wartość jest na miejscu. Powiedzmy, że mamy limit ponownych prób wysyłki (w prawdziwym życiu logika retries jest bardziej skomplikowana, ale do ilustracji wystarczy).

package com.example.contextflow.application.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class RetryPolicy {
    private final int retryLimit;

    public RetryPolicy(
            // Domyślna wartość 3 — świadome bezpieczne ustawienie „na wszelki wypadek"
            @Value("${contextflow.notifications.retry-limit:3}") int retryLimit
    ) {
        // Spring sam skonwertuje string z konfiguracji na int (jeśli wartość jest poprawna)
        this.retryLimit = retryLimit;
    }
}

Tak, tu Spring już podstawia string i konwertuje na int. Nie wchodzimy w mechanikę conversion, po prostu utrwalamy: dla prymitywów i prostych typów Spring zazwyczaj radzi sobie sam i to normalne w ramach poziomu podstawowego.

4. Gdzie stawiać @Value

Gdy po raz pierwszy widzisz @Value, ręka ciągnie do zrobienia tak, jak często robi się z @Autowired: „no dobra, wrzucę do pola, szybciej przecież". Szybciej — tak. Lepiej — nie. Field injection wartości ukrywa ustawienia, psuje ideę „konstruktor pokazuje zależności", komplikuje testowanie i zmusza klasę do bycia mutowalną. A to jeszcze idealna gleba dla sytuacji „nie rozumiem, skąd wzięła się u mnie ta wartość".

Oto przykład, który technicznie działa, ale jako teaching default się nie nadaje:

package com.example.contextflow.application.scenario;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppBannerPrinter {
    // Iniekcja przez pole: zależność ukryta, obiekt trudniej testować, pojawia się stan mutowalny
    @Value("${contextflow.app-name:ContextFlow}")
    private String appName;

    public void print() {
        System.out.println("== " + appName + " =="); // == ContextFlow ==
    }
}

Główny problem tu nawet nie w anotacji, lecz w tym, że teraz „kontrakt" klasy jest rozmyty: patrząc na konstruktor, nie rozumiesz, że ten obiekt potrzebuje ustawienia. A oto wariant, który czyta się uczciwie:

package com.example.contextflow.application.scenario;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppBannerPrinter {
    private final String appName;

    public AppBannerPrinter(
            // Iniekcja przez konstruktor: od razu widać, że obiekt potrzebuje ustawienia
            @Value("${contextflow.app-name:ContextFlow}") String appName
    ) {
        this.appName = appName;
    }
}

Teraz zależność/ustawienie widoczne od razu. I tak, final — to nie dekoracja, lecz ochrona przed przypadkową mutacją: „ustawienie przyszło raz przy tworzeniu i dalej nie wędruje po obiekcie jak kot po klawiaturze".

Jeszcze jeden bardzo wygodny wariant (szczególnie dla infrastruktury) — stawianie @Value na parametrze metody @Bean. Szczególnie dobrze działa w modułowych configach: na przykład ReportingConfig może być konfiguracyjnym fragmentem reporting-części, który górny AppConfig dołącza do ogólnego składania. Wtedy czytanie właściwości zachodzi na granicy konfiguracyjnej, a twoja klasa pozostaje czystym POJO bez znajomości @Value.

package com.example.contextflow.config.reporting;

import com.example.contextflow.support.lifecycle.ReportOutputManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ReportingConfig {

    @Bean
    public ReportOutputManager reportOutputManager(
            // Czytamy ustawienie na granicy konfiguracyjnej (w @Configuration)
            @Value("${contextflow.report.output-dir:build/reports}") String outputDir
    ) {
        // Do klasy biznesowej/infrastrukturalnej trafia już gotowy parametr
        return new ReportOutputManager(outputDir);
    }
}

Ten styl jest często bardzo „dorosły": klasy biznesowe nie wiedzą, skąd przyszły ustawienia, a konfiguracja jawnie pokazuje, które klucze są używane przy składaniu.

5. Environment: kiedy lepiej czytać jawnie

Jako mapa źródeł i precedence Environment jest sam w sobie użyteczny. Tu ważniejsze jest bardziej praktyczne pytanie: kiedy czytać wartość jawnie, a kiedy wystarczy @Value. @Value jest dobry, gdy wartości jest jedna-dwie i chcesz podstawić ją bezpośrednio w miejsce użycia. Ale czasem lepiej zrobić krok w tył i powiedzieć: „Chcę sam odczytać ustawienie, sprawdzić je, zalogować, złożyć z kilku kawałków i dopiero potem stworzyć obiekt". W tym momencie Environment staje się bardzo wygodny. Jest nieco bardziej szczegółowy, ale widzisz moment czytania i możesz zachowywać się jak dorosły inżynier, a nie człowiek z zestawem magicznych stringów.

Najprostszy przykład — klasa, która oblicza ścieżkę dla raportów. Nie wchodzimy w złożone scenariusze, po prostu czytamy string i zamieniamy na Path.

package com.example.contextflow.infrastructure.reporting;

import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class ReportPathResolver {
    private final Path outputDir;

    public ReportPathResolver(Environment environment) {
        // Environment — to infrastrukturalne API Springa do dostępu do właściwości
        // Drugi parametr — domyślna wartość, jeśli klucz nie zostanie znaleziony
        String dir = environment.getProperty("contextflow.report.output-dir", "build/reports");

        // Tu jawnie robimy konwersję na Path (a nie rozrzucamy tego po kodzie)
        this.outputDir = Paths.get(dir);
    }

    public Path outputDir() {
        return outputDir;
    }
}

Zwróć uwagę: Environment nie czytamy w każdej metodzie generateReport(). Czytamy raz przy tworzeniu beana i zapisujemy wynik. To utrzymuje serwisy „stateless" i przewidywalne: konfiguracja przyszła przy starcie, dalej obiekt pracuje stabilnie.

Tak samo można czytać właściwości bezpośrednio w @Configuration — często wygląda to jeszcze logiczniej, bo konfiguracja i tak odpowiada za składanie.

I tak samo CoreConfig tutaj — to nie drugi entry point aplikacji, lecz jeszcze jeden modułowy config wewnątrz ogólnego AppConfig.

package com.example.contextflow.config.core;

import com.example.contextflow.support.lifecycle.ReportOutputManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class CoreConfig {

    @Bean
    public ReportOutputManager reportOutputManager(Environment environment) {
        // Czytanie właściwości zachodzi w dokładnie jednym miejscu — w konfiguracji
        String dir = environment.getProperty("contextflow.report.output-dir", "build/reports");

        // Na wyjściu — zwykły obiekt, nie powiązany ze Springiem
        return new ReportOutputManager(dir);
    }
}

I tak, Environment przychodzi tu jako zwykła zależność. Spring umie go dać, bo Environment — to część „infrastruktury kontekstu".

@Value i Environment: kryteria wyboru

W życiu nie istnieje „rób zawsze tylko tak". Ale istnieją zdroworozsądkowe kryteria, które oszczędzają czas i sprawiają, że kod jest czytelniejszy. @Value i Environment — to nie konkurenci, lecz dwa narzędzia z jednej skrzynki. Najważniejsze — nie używać gwoździa jak śrubokręta i nie robić z młotka śrubokręta.

Mała tabelka, która zazwyczaj pomaga podjąć decyzję:

Pytanie Częściej pasuje @Value Częściej pasuje Environment
Ile wartości trzeba odczytać? 1–2 3+ lub jest logika wyboru
Gdzie czytamy? blisko miejsca użycia na granicy konfiguracyjnej
Czy potrzebna jest jawna obsługa/logowanie? rzadko często
Czy chcemy trzymać klasy jako „czyste POJO"? można, ale jest anotacja wygodnie: czytanie w @Configuration
Czy chcemy unikać „annotation soup"? tak, jeśli wartości jest mało tak, szczególnie jeśli ustawień jest dużo

Tłumacząc na język ludzki: @Value — to „krótki kabel" od konfiguracji do miejsca użycia. Environment — to „panel sterowania", gdzie możesz akaratnie zebrać zasilanie, postawić bezpieczniki i dopiero potem zasilić urządzenie.

6. Mikro-inkrement ContextFlow: config w konstruktorze

Teraz zbierzemy wszystko w mały, ale bardzo charakterystyczny fragment ContextFlow. Bierzemy ten sam contextflow.properties, który jest już podłączony do górnego AppConfig, i patrzymy tylko na linie ważne dla tego scenariusza:

# Fragment tego samego contextflow.properties, który jest już podłączony do AppConfig
contextflow.app-name=ContextFlow
contextflow.notifications.default-channel=console
contextflow.report.output-dir=build/reports
contextflow.locale.default=pl

Tu default-channel działa jako klucz wyboru strategii w Map<String, NotificationSender>, dlatego wartość lower-case wygodnie trzymać taką samą jak nazwa odpowiadającego beana.

Tu ważne, że klucze są czytelne i „żyją" pod jednym prefiksem contextflow.*. To nie kaprys, lecz dyscyplina: za kilka tygodni podziękujesz sobie, bo konfiguracja nie zamieni się w wysypisko name, mode, path, value2.

Teraz przykład: serwis wysyłki powiadomień wybiera implementację po nazwie kanału. Nie robimy skomplikowanego routingu, po prostu pokazujemy, że default-channel teraz przychodzi z zewnątrz.

package com.example.contextflow.application.service;

import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    private final String defaultChannel;
    private final Map<String, NotificationSender> senders;

    public NotificationDispatchService(
            // Ustawienie steruje tym, który nadawca zostanie wybrany domyślnie
            @Value("${contextflow.notifications.default-channel:console}") String defaultChannel,
            // Spring złoży Map po nazwach beanów: klucz = bean name, wartość = implementacja
            Map<String, NotificationSender> senders
    ) {
        this.defaultChannel = defaultChannel;
        this.senders = senders;
    }

    public void dispatch(String message) {
        // Bierzemy nadawcę po kluczu z konfiguracji
        senders.get(defaultChannel).send(message);
    }
}

A żeby Map<String, NotificationSender> było przewidywalne, dajemy czytelne bean names nadawcom (to było już w temacie o kilku implementacjach, tu po prostu z tego korzystamy).

package com.example.contextflow.infrastructure.notification;

import org.springframework.stereotype.Component;

@Component("console") // Jawnie ustalamy nazwę beana, żeby zgadzała się z wartością z konfiguracji
public class ConsoleNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("[CONSOLE] " + message); // [CONSOLE] Order created
    }
}

I wreszcie niech nasz ScenarioRunner drukuje informacje startowe, żeby na własne oczy zobaczyć, że konfiguracja naprawdę działa.

package com.example.contextflow.application.scenario;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ScenarioRunner {
    private final String appName;

    public ScenarioRunner(
            // Wartość można nadpisać przez contextflow.properties / env vars / JVM props
            @Value("${contextflow.app-name:ContextFlow}") String appName
    ) {
        this.appName = appName;
    }

    public void run() {
        System.out.println("Starting " + appName); // Starting ContextFlow
    }
}

Cała wartość tego kroku polega na tym, że teraz zmiana nazwy aplikacji lub kanału powiadomień — to nie „idziemy edytować klasę Java", lecz „zmieniamy konfigurację uruchomienia". Kod stał się mniej przywiązany do środowiska. I to dokładnie cel, dla którego Spring wprowadza warstwę konfiguracyjną: oddzielić „jak działa program" od „z jakimi wartościami uruchamia się dziś".

7. Typowe błędy przy pracy z @Value i Environment

Błąd nr 1: używanie @Value w polach jako główny styl.
Technicznie to działa i nawet wygląda krócej. Ale w efekcie ustawienia stają się ukryte, klasa traci przejrzystość kontraktu, pojawia się więcej stanu mutowalnego i przestajesz rozumieć, jakie parametry są potrzebne obiektowi do stworzenia. Dla kursu (i dla normalnego nawyku inżynierskiego) lepiej trzymać wartości w konstruktorze lub na granicy konfiguracyjnej.

Błąd nr 2: „zatykanie" każdego brakującego klucza domyślną wartością.
Domyślna wartość — to nie plaster „żeby nie padało". Jeśli stawiasz :default na wszystkim po kolei, zamieniasz błędy konfiguracyjne w ciche bugi. Lepiej niech aplikacja uczciwie padnie przy starcie, niż będzie działać „jakoś" i psuć dane lub pisać pliki w nieoczekiwane miejsca.

Błąd nr 3: wstrzykiwanie Environment do każdego serwisu i czytanie właściwości w metodach biznesowych.
Tak łatwo wpaść w styl „każda metoda sama sobie config". To sprawia, że zachowanie jest mniej przewidywalne i rozmywa granice odpowiedzialności. Dobry wzorzec — odczytać wartości raz przy tworzeniu beana (w konstruktorze lub @Configuration) i potem pracować ze zwykłymi polami.

Błąd nr 4: zbyt dużo @Value w jednej klasie (annotation soup).
Pięć-sześć @Value w jednym konstruktorze — i czytelność zaczyna spadać. To zazwyczaj sygnał, że klasa za dużo robi lub że czas przenieść konfiguracyjne czytanie do @Configuration i przekazywać do klasy już złożone, sensowne parametry.

Błąd nr 5: „magiczne stringi" kluczy bez dyscypliny nazewnictwa.
Jeśli gdzieś piszesz appName, gdzieś contextflow.app.name, a gdzieś contextflow.app-name, gwarantowanie urządzisz sobie quest po szukaniu „dlaczego się nie podstawia". Umowa co do prefiksu contextflow.* i staranne nazwy kluczy — to nudne, ale bardzo praktyczne. W programowaniu nudne zazwyczaj oznacza „niezawodne".

1
Zadanie
Spring Core, poziom 12, lekcja 3
Niedostępne
Wstrzykiwanie wartosci przez konstruktor za pomoca @Value z wartoscia domyslna
Wstrzykiwanie wartosci przez konstruktor za pomoca @Value z wartoscia domyslna
1
Zadanie
Spring Core, poziom 12, lekcja 3
Niedostępne
Jawne odczytywanie w³aœciwoœci przez Environment w metodzie @Bean
Jawne odczytywanie w³aœciwoœci przez Environment w metodzie @Bean
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION