CodeGym /Kursy /Spring REST & MVC /Profile i nadpisywanie konfiguracji

Profile i nadpisywanie konfiguracji

Spring REST & MVC
Poziom 28, Lekcja 3
Dostępny

1. Profile Spring Boot

Gdy projekt jest mały, wydaje się, że konfiguracja to coś w rodzaju „jednej linijki, którą można poprawić i zapomnieć". Ale gdy tylko zaczniesz robić prawdziwe ficzery, np. załączniki z lokalnym storage, nagle okazuje się, że ten sam kod chce się uruchamiać różnie: ktoś chce przechowywać pliki w ./data, ktoś w ./tmp, a na CI w ogóle nie można pisać gdzie popadnie. I tu zaczyna się klasyczny dramat „dlaczego u mnie działa, a u ciebie nie".

Wyobraź sobie, że napisałeś LocalAttachmentStorage, który zapisuje pliki na dysk. Lokalnie chcesz, żeby pliki nie zaśmiecały repozytorium i trzymasz je w ./tmp/attachments. W innym środowisku chcesz przechowywać je w ./data/attachments, bo ten katalog trafia do backupu lub po prostu jest wygodniejszy. API przy tym się nie zmienia: te same endpointy, te same DTO, te same statusy. Zmienia się tylko „gdzie leżą pliki" i „jakie są limity uploadu".

Jeśli rozwiązać zadanie „na prost", szybko dojdziesz do złych wariantów: ręcznie edytować application.yml przed każdym uruchomieniem, trzymać trzy różne pliki i kopiować z jednego do drugiego, commitować ścieżki maszyno-specyficzne, albo w ogóle wrócić do hardkodu w kodzie — „żeby na pewno działało". Profile w Spring Boot są wymyślone właśnie po to, żebyś mógł powiedzieć: „są wartości bazowe, a dla konkretnego trybu uruchomienia dodaję mały patch, który zmienia tylko to, co trzeba".

Profil jako „patch" do application.yml

Słowo „profil" u nowicjuszy często wywołuje błędny obraz: jakby to był „jeszcze jeden projekt" albo „inny tryb działania aplikacji, gdzie wszystko inaczej". W praktyce profil w Spring Boot jest o wiele prostszy i użyteczniejszy: to sposób nałożenia ponad bazowe ustawienia dodatkowej warstwy, która nadpisuje część wartości. Wygodna metafora — „patch" albo „naklejka nad bazową etykietą".

Bazowy application.yml to domyślny strój aplikacji: schludny, neutralny, bez przywiązania do konkretnej maszyny. Profil local to twój „domowy dres": wygodnie debugować, można ustawić większe limity, prostszy katalog, bardziej gadatliwe logi. Ale to ten sam człowiek, ta sama aplikacja, te same endpointy. Nie zmieniamy kontraktu API profilem, zmieniamy tylko „w jakich butach biec" i „jaką kurtkę założyć".

Technicznie Spring Boot rozwiązuje dwa zadania: musi wiedzieć, który profil jest aktywny, i musi załadować dodatkowe ustawienia dotyczące tego profilu. Profil może być jeden (local), może być kilka (local,debug), ale w projekcie edukacyjnym trzymamy to maksymalnie prosto: bazowy application.yml definiuje baseline projektu, application-local.yml — lokalny patch, a env vars i argumenty uruchomieniowe zostają dla nadpisań maszyno-specyficznych.

2. Bazowy application.yml i profilowy application-local.yml

Żeby local-override nie zamieniał się w osobny świat konfiguracyjny, dla Task Tracker API trzymamy jeden kanoniczny layout: bazowy application.yml definiuje domyślne zachowanie projektu, a application-local.yml zmienia tylko to, co trzeba lokalnie. Tak od razu widać, gdzie jest baseline repozytorium, a gdzie zaczyna się wygoda konkretnego uruchomienia.

Tak może wyglądać twój bazowy application.yml, który commitowany jest zawsze jako jedyne źródło prawdy dla domyślnego zachowania:

# application.yml
spring:
  mvc:
    problemdetails:
      # Bazowe ustawienie: włączamy ProblemDetail dla jednolitych błędów
      enabled: true
  servlet:
    multipart:
      # Bazowe limity (dla wszystkich, jeśli nie są nadpisane)
      max-file-size: 10MB
      max-request-size: 12MB

app:
  attachments:
    # Domyślny katalog, odpowiedni dla „ogólnego" uruchomienia
    storage-dir: ./data/attachments

A oto profilowy plik dla uruchomienia local:

# application-local.yml
spring:
  servlet:
    multipart:
      # Lokalnie można zwiększyć limity, żeby nie przeszkadzały w developmencie
      max-file-size: 20MB
      max-request-size: 25MB

app:
  attachments:
    # Lokalnie trzymamy w tmp, żeby nie zaśmiecać katalogu roboczego projektu
    storage-dir: ./tmp/attachments

Czyta się to prosto: „domyślnie przechowujemy załączniki w ./data/attachments, a lokalnie — w ./tmp/attachments; bazowe limity 10 MB/12 MB, lokalnie — 20 MB/25 MB". Przy tym nie kopiujesz całej bazowej konfiguracji do pliku profilowego, a nadpisujesz tylko to, co rzeczywiście jest inne.

W praktyce zespoły często robią tak: application.yml commituje się zawsze, a application-local.yml może być albo commitowany, albo osobisty i leżeć w .gitignore. W projekcie edukacyjnym wygodniej zazwyczaj commitować wspólny application-local.yml, a zupełnie osobiste rzeczy ustawiać przez env vars — do tego podejdziemy za chwilę.

Ten sam patch można zapisać i wewnątrz jednego application.yml przez --- i spring.config.activate.on-profile. Spring Boot to obsługuje, ale dla obecnego projektu osobny application-local.yml czyta się prościej: bazowy snapshot i local-override nie mieszają się w jednym pliku.

3. Uruchomienie i nadpisywanie

Gdy layout jest już ustalony, dalej wszystko jest dość nudne — i to jest tu plusem. Po prostu aktywujemy local przy uruchomieniu, a jeśli jedna maszyna potrzebuje jeszcze jednego prywatnego nadpisania, dodajemy env var ponad profilem.

Włączanie profilu przy uruchomieniu

Profile stają się użyteczne tylko gdy umiesz nimi zarządzać bez szamanizmu. Dobra wiadomość: włączyć profil w Spring Boot można na kilka prostych sposobów i wszystkie sprowadzają się do jednego klucza — spring.profiles.active. Wybierasz jeden, najwygodniejszy dla twojego trybu pracy i nie skaczysz między dziesięcioma wariantami „w zależności od nastroju".

Najprostszy „przyciskowy" sposób — przez konfigurację uruchomienia w IDE: w IntelliJ IDEA dodajesz zmienną środowiskową SPRING_PROFILES_ACTIVE=local lub wskazujesz aktywny profil w odpowiednim polu. Wygodne tym, że jest naoczne: otwierasz Run Configuration — widzisz, jaki profil jest włączony.

Jeśli uruchamiasz przez Gradle, możesz przekazać argumenty do bootRun. W projekcie edukacyjnym wygląda to zazwyczaj tak:

# Uruchomienie aplikacji przez Gradle z jawnym włączeniem profilu
./gradlew bootRun --args='--spring.profiles.active=local'

Jeśli uruchamiasz już zbudowany jar, logika jest taka sama, tylko argument trafia do procesu JVM aplikacji:

# Uruchomienie jar z aktywnym profilem local
java -jar build/libs/task-tracker.jar --spring.profiles.active=local

Przydatna praktyczna rada: gdy po raz pierwszy konfigurujesz profile, bardzo wygodnie jest przy starcie aplikacji wypisać aktywne profile do logów, żeby nie zgadywać „a czy to na pewno się włączyło?". W projekcie edukacyjnym można zrobić mały komponent-logger. Nie jest związany z logiką biznesową, więc jego miejsce jest w warstwie konfiguracyjnej lub infrastrukturalnej.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class ActiveProfilesLogger implements ApplicationRunner {
    private final Environment env;

    public ActiveProfilesLogger(Environment env) {
        // Environment — źródło konfiguracji, z niego można czytać aktywne profile
        this.env = env;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Wypisujemy profile, żeby przy starcie nie zgadywać, co faktycznie się włączyło
        System.out.println("Active profiles: " + String.join(",", env.getActiveProfiles()));
        // Przykładowy oczekiwany wynik:
        // Active profiles: local
    }
}

Tak, System.out.println to nie najbardziej eleganckie „bojowe" podejście, ale w projekcie edukacyjnym akurat pomaga zobaczyć wynik od razu. Później będziesz oczywiście logować przez normalny logger, ale sens pozostanie ten sam: najpierw upewnij się, że profil jest włączony, a dopiero potem kłóć się z YAML.

Nadpisywanie przez zmienne środowiskowe

Profile rozwiązują zadanie „mamy przewidywalny tryb lokalny", ale nie rozwiązują zadania „każdy deweloper ma swoją maszynę". Właśnie tu wchodzą do gry nadpisywania przez zmienne środowiskowe. Ich główna zaleta: możesz zmienić wartość nie dotykając repozytorium w ogóle. To idealny sposób na powiedzenie: „w moim środowisku storage leży w innym miejscu i to jest normalne".

Spring Boot potrafi pobierać wartości ze zmiennych środowiskowych i mapować je na właściwości. Reguła, którą warto zapamiętać w praktycznej formie: kropki i myślniki zamieniają się na podkreślenia, a nazwa staje się wielką literą. Dlatego app.attachments.storage-dir zamienia się w APP_ATTACHMENTS_STORAGE_DIR, a spring.servlet.multipart.max-file-size zamienia się w SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE.

Oto przykład typowego uruchomienia, gdzie aktywujemy profil local i jednocześnie nadpisujemy katalog storage oraz limit pliku:

# 1) Włączamy profil local
export SPRING_PROFILES_ACTIVE=local
# 2) Nadpisujemy konkretną właściwość aplikacji (katalog przechowywania)
export APP_ATTACHMENTS_STORAGE_DIR=./runtime/attachments
# 3) Nadpisujemy właściwość frameworka (limit rozmiaru pliku)
export SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=30MB

W Windows w PowerShell składnia jest inna, ale idea ta sama: zmienna środowiskowa to „ostatnie słowo", które wypowiadasz konfiguracji przed uruchomieniem aplikacji.

Dlaczego to jest wygodne w naszym Task Tracker API? Bo katalogi to bardzo osobista sprawa. Jeden deweloper chce trzymać pliki w ./tmp, drugi — w C:\work\data, trzeci — w ~/dev/task-tracker-attachments. Jeśli będziesz próbować rozwiązać to profilami i commitować różne ścieżki, dostaniesz „wojnę konfiguracyjną". Jeśli rozwiążesz to zmiennymi środowiskowymi — wszyscy są szczęśliwi, a repozytorium pozostaje czyste.

Priorytety konfiguracji i debugowanie

Gdy zaczniesz używać i profili, i env vars, i domyślnych wartości w @ConfigurationProperties, pojawia się uzasadnione pytanie: „jeśli ta sama właściwość jest ustawiona w trzech miejscach — która zostanie wzięta?". To pytanie nie filozoficzne, a absolutnie praktyczne: bez rozumienia priorytetów będziesz tracić godziny na „zmieniłem, dlaczego nie działa".

W wersji edukacyjnej można trzymać prosty model mentalny: konfiguracja to warstwy i im „bliżej uruchomienia", tym wyższy priorytet. Bazowy application.yml daje wartość domyślną. Plik profilowy nadpisuje część wartości. Zmienne środowiskowe nadpisują ponad profilem. Argumenty wiersza poleceń zazwyczaj mają jeszcze wyższy priorytet i mogą przebić nawet env vars.

Źródło wartości Przykład Priorytet (kto „przebija" kogo)
Domyślna w kodzie private String storageDir = "./data/attachments"; najniższy
application.yml (base)
app.attachments.storage-dir: ./data/attachments
wyżej niż domyślna
application-local.yml
./tmp/attachments
wyżej niż base
Env vars
APP_ATTACHMENTS_STORAGE_DIR=...
wyżej niż pliki
Command line args
--app.attachments.storage-dir=...
zazwyczaj najwyższy

Jeśli łapiesz sytuację „zmieniłem YAML, a wartość się nie zmienia", prawie zawsze przyczyna jest jedna z dwóch: albo uruchamiasz nie ten profil, albo wartość jest przebita przez env var/argument. Dlatego gdy coś działa dziwnie, nie trzeba zaczynać od mistyki. Zacznij od diagnostyki: sprawdź aktywny profil, a potem sprawdź, czy nie masz zmiennej środowiskowej nadpisującej tę samą właściwość.

Dla utrwalenia można wyobrazić sobie to jako bardzo prosty schemat blokowy: Spring Boot idzie od góry do dołu i „zbiera" konfigurację, ale przy konflikcie wygrywa bardziej priorytetowe źródło.

flowchart TD
    %% Idea: niżej — bardziej priorytetowe źródła, one „przesłaniają" górne
    A["Domyślne w kodzie"] --> B["application.yml"]
    B --> C["application-local.yml"]
    C --> D["Zmienne środowiskowe"]
    D --> E["Argumenty wiersza poleceń"]
    %% Finalne złożenie wartości, które widzi twój kod w @ConfigurationProperties
    E --> F["Ostateczna wartość w @ConfigurationProperties"]

Na koniec tego dnia przydatne jest umieć odpowiedzieć sobie na kilka pytań bez podpowiedzi: czy potrafisz wyjaśnić, dlaczego storage-dir został pobrany dokładnie taki; czy rozumiesz, jak włączyć profil local w jednym konkretnym uruchomieniu; czy potrafisz nadpisać jeden parametr przez env var, nie dotykając YAML; czy potrafisz szybko udowodnić sobie, że profil faktycznie jest aktywny, a nie „wydaje mi się, że aktywny".

4. Typowe błędy przy pracy z profilami i nadpisywaniami

Błąd №1: kopiowanie całej bazowej konfiguracji do profilu.
Tak zazwyczaj dzieje się „z przyzwyczajenia": deweloper tworzy application-local.yml, bierze cały application.yml, wkleja i zmienia jedną linię. Po tygodniu bazowa konfiguracja się zmieniła, a profilowa — nie. Wychodzą dwa światy, które się rozbiegają i debugowanie zamienia się w zgadywankę. Profil powinien zawierać tylko różnice, inaczej przestaje być patchem i staje się „drugą rzeczywistością".

Błąd №2: sprawienie, żeby profil zmieniał sens kontraktu API.
Bardzo chce się zrobić „lokalnie status może być dowolny, a na produkcji nie", albo „lokalnie wyłączymy walidację, żeby nie przeszkadzała", albo „lokalnie zwracamy błędy jako ciąg, a na produkcji ProblemDetail". To niszczy samą ideę kontraktu: klient nie powinien zależeć od tego, jaki profil ma dziś serwer. Profil dotyczy środowiska uruchomienia, a nie reguł domeny, nie dozwolonych przejść statusów i nie kształtu DTO. Jeśli wartość wpływa na kontrakt, jej miejsce jest w kodzie i dokumentacji, a nie w application-local.yml.

Błąd №3: zapominanie, który profil jest aktywny, i sprawdzanie tego metodą „narzekania na życie".
Typowa sytuacja: jesteś pewny, że włączyłeś local, ale naprawdę aplikacja wystartowała bez profilu i dlatego storage-dir został pobrany z base. Albo odwrotnie: profil jest gdzieś włączony w IDE, zapomniałeś i potem dziwisz się, dlaczego na laptopie wszystko działa „nie tak jak na serwerze". Leczy się to prosto: w developmencie zawsze wypisuj aktywny profil, a w IDE trzymaj osobne Run Configurations „Default" i „Local", żeby nie przełączać ręcznie.

Błąd №4: wkładanie maszyno-zależnej ścieżki bezwzględnej do bazowego application.yml.
Bazowa konfiguracja to ta, którą mają jednakowo czytać wszyscy. Jeśli piszesz tam /Users/alex/... czy D:\attachments, zamieniasz repozytorium w „konfigurację dla jednej maszyny". Do base-konfiguracji lepiej stosować ścieżkę względną jak ./data/attachments. Jeśli ktoś potrzebuje ścieżki bezwzględnej — niech ustawia ją przez env var lub przez lokalny profil, który nie jest commitowany.

Błąd №5: brak świadomości, że env vars mogą „przekrzyczeć" YAML, i dlatego nie wiara własnym oczom.
Scenariusz wygląda komicznie: zmieniasz application.yml, restartujesz, a wartość się nie zmienia. Zaczyna się podejrzenie o spisek Spring Boot. W rzeczywistości najczęściej masz gdzieś już ustawione APP_ATTACHMENTS_STORAGE_DIR i ono wygrało. W tym momencie przydatne jest nie kłócenie się ze Springiem, ale spokojne przejrzenie zmiennych środowiskowych i pamiętanie o kolejności priorytetów. Spring Boot zachowuje się tu bardzo konsekwentnie — to my po prostu zapomnieliśmy, że sami zostawiliśmy nadpisanie.

1
Zadanie
Spring REST & MVC, poziom 28, lekcja 3
Niedostępne
Profil `local` dla oddzielnego katalogu przechowywania
Profil `local` dla oddzielnego katalogu przechowywania
1
Zadanie
Spring REST & MVC, poziom 28, lekcja 3
Niedostępne
Nadpisywanie przez zmienną środowiskową na profilu
Nadpisywanie przez zmienną środowiskową na profilu
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION