Blog JSystems - uwalniamy wiedzę!

Szukaj
Działająca aplikacja Spring Boot w przeglądarce: katalog książek z kartami, kategoriami i cenami, wyrenderowany na serwerze przez Thymeleaf
Nasza aplikacja demonstracyjna w akcji. To katalog książek wyrenderowany po stronie serwera przez Spring Boot i szablony Thymeleaf. Dane pochodzą z komponentu BookstoreService. Kliknij zrzut, aby powiększyć.

Jeśli uczysz się programowania w Javie, prędzej czy później natrafisz na dwa słowa, które padają niemal w każdej ofercie pracy: Spring i Spring Boot. Brzmią podobnie, często występują razem, a mimo to nie znaczą tego samego. W tym przewodniku wyjaśnimy od zera, co to jest Spring Framework, co to jest Spring Boot i czym te dwie rzeczy się różnią. Nie na diagramach z internetu, tylko na prawdziwej aplikacji, którą specjalnie na potrzeby tego artykułu zbudowaliśmy i uruchomiliśmy: mała księgarnia z REST-owym adresem (interfejsem programistycznym zwracającym dane) i strona HTML renderowana na serwerze. Wszystkie zrzuty pochodzą z tej działającej aplikacji.

W pigułce: Spring Framework to szkielet (framework) do budowy aplikacji w Javie - daje mechanizmy takie jak wstrzykiwanie zależności, obsługa żądań sieciowych czy transakcje. Spring Boot to nadbudowa nad Spring Framework, która te mechanizmy sama konfiguruje i pozwala uruchomić aplikację jednym poleceniem. Krótko: Framework daje klocki, a Boot składa je za Ciebie i od razu uruchamia.

Wolisz od razu zajrzeć w działający kod? Cały projekt tej aplikacji (Spring Boot, Maven, Java 21) jest publicznie na GitHubie. Sklonuj go i uruchom u siebie jednym poleceniem, żeby przejść ten przewodnik krok po kroku na własnym komputerze - wszystkie zrzuty w artykule pochodzą dokładnie z tego projektu:

git clone https://github.com/konwertyta/spring-boot-bookstore-demo.git
cd spring-boot-bookstore-demo
mvn spring-boot:run
Pobranie i uruchomienie aplikacji demonstracyjnej. Po starcie otwórz w przeglądarce http://localhost:8085 (strona HTML) oraz http://localhost:8085/api/books (te same dane jako JSON).

Co to jest Spring Framework?

Spring Framework to szkielet do budowy aplikacji w języku Java, który bierze na siebie techniczną czarną robotę, żebyś Ty mógł skupić się na logice biznesowej. Słowo szkielet (z angielskiego framework) oznacza gotowy fundament aplikacji: zestaw mechanizmów i zasad, w które wpinasz swój kod, zamiast pisać wszystko od podstaw. Spring zajmuje się między innymi tworzeniem obiektów i łączeniem ich ze sobą, obsługą żądań przychodzących z sieci, dostępem do bazy danych czy transakcjami (czyli pilnowaniem, żeby zestaw operacji na bazie wykonał się w całości albo wcale).

Kluczowe jest to, że Spring jest modularny. Nie jest jednym wielkim blokiem, który musisz wziąć w całości. To zestaw współpracujących modułów, z których bierzesz tylko te, których potrzebujesz. Sercem całości jest tak zwany kontener (o nim za chwilę), a wokół niego narastają moduły do sieci, danych czy testów.

Infografika modułów Spring Framework: rdzeń z kontenerem IoC, Beans i Context oraz SpEL, a wokół moduły Web/MVC, Data Access, AOP i Test
Spring Framework to modularny zestaw. W środku jest rdzeń z kontenerem, który tworzy obiekty i łączy je ze sobą, a wokół niego moduły do obsługi WWW, dostępu do danych, programowania aspektowego i testów.

Krótka historia Springa

Spring powstał jako odpowiedź na przekombinowanie wczesnych technologii Javy korporacyjnej. Jego korzenie sięgają kodu, który Rod Johnson opublikował w 2002 roku, a pierwsza wersja frameworka ukazała się w 2004. Chodziło o jedno: żeby budowa aplikacji była prosta, a zwykły obiekt Javy (tak zwany POJO, czyli Plain Old Java Object - obiekt bez narzuconych zależności od frameworka) nie musiał dziedziczyć po dziwnych klasach ani wypełniać stert konfiguracji. Przez ponad dwie dekady Spring urósł do najpopularniejszego szkieletu w świecie Javy po stronie serwera.

Wstrzykiwanie zależności, czyli serce Springa

Żeby zrozumieć Springa, trzeba zrozumieć jeden pomysł: wstrzykiwanie zależności (z angielskiego dependency injection). Brzmi poważnie, a chodzi o coś prostego. Wyobraź sobie, że kontroler obsługujący żądania potrzebuje serwisu z logiką. Bez Springa musiałbyś sam napisać new BookstoreService() w każdym miejscu, w którym ten serwis jest potrzebny, i sam pilnować, ile takich obiektów powstaje. Ze Springiem jest odwrotnie: to Spring tworzy obiekt i sam podaje go tam, gdzie jest potrzebny. Ty tylko mówisz, czego potrzebujesz.

To odwrócenie odpowiedzialności nazywa się odwróceniem sterowania (z angielskiego Inversion of Control, w skrócie IoC). Nie Ty tworzysz i spinasz obiekty, tylko robi to za Ciebie kontener Springa - to znaczy ta część frameworka, która zarządza cyklem życia obiektów. Same obiekty tworzone i zarządzane przez kontener nazywamy ziarnami (z angielskiego beans). Zobaczmy to na animacji z naszej księgarni:

Animacja wstrzykiwania zależności w Springu: kontener IoC tworzy ziarno BookstoreService i wstrzykuje je do dwóch kontrolerów
Wstrzykiwanie zależności krok po kroku. Kontener Springa tworzy jeden obiekt BookstoreService i wstrzykuje go do kontrolera REST oraz do kontrolera widoku. Ten sam obiekt jest współdzielony w całej aplikacji.

W naszej aplikacji serwis to zwykła klasa oznaczona adnotacją (czyli specjalnym znacznikiem) @Service. Adnotacja mówi Springowi: to jest komponent, zajmij się nim. Dzięki temu Spring sam utworzy jego obiekt i będzie go pilnował.

@Service
public class BookstoreService {

    private final List<Book> catalog = List.of(
        new Book(1L, "Thinking in Java", "Bruce Eckel", 2006, new BigDecimal("149.00"), "Java"),
        new Book(2L, "Spring in Action", "Craig Walls", 2022, new BigDecimal("189.00"), "Spring"));

    public List<Book> findAll() {
        return catalog;
    }

    public List<Book> findByCategory(String category) {
        return catalog.stream()
                .filter(b -> b.category().equalsIgnoreCase(category))
                .toList();
    }
}
BookstoreService: zwykła klasa z logiką, oznaczona jako komponent Springa adnotacją @Service.

A teraz najważniejsze. Kontroler nie tworzy serwisu ręcznie. On tylko przyjmuje go w konstruktorze, a Spring sam podstawia gotowy obiekt. To właśnie jest wstrzykiwanie zależności przez konstruktor - dziś zalecany sposób w Springu:

@RestController
@RequestMapping("/api/books")
public class BookRestController {

    private final BookstoreService service;

    // Nie piszemy 'new BookstoreService()'. Spring sam podaje gotowy
    // obiekt serwisu przez konstruktor - to jest wstrzykiwanie zależności.
    public BookRestController(BookstoreService service) {
        this.service = service;
    }

    @GetMapping
    public List<Book> list(@RequestParam(required = false) String category) {
        return category == null ? service.findAll() : service.findByCategory(category);
    }
}
Kontroler REST przyjmuje BookstoreService w konstruktorze. Nie ma tu ani jednego 'new' - gotowy obiekt podaje Spring.

Dlaczego to takie ważne? Bo dzięki temu klasy nie są ze sobą na sztywno zrośnięte. Serwis można podmienić (na przykład na wersję testową) bez dotykania kontrolera, a cały kod łatwiej się testuje i utrzymuje. To jest realna wartość Springa, a nie sama adnotacja.

Co to jest Spring Boot i czym różni się od Spring Framework?

Sam Spring Framework jest potężny, ale na starcie bywa wymagający: trzeba samodzielnie poskładać konfigurację, wybrać i podpiąć serwer, dobrać pasujące do siebie wersje bibliotek. W 2014 roku pojawił się Spring Boot, który tę barierę zdejmuje. Spring Boot to nadbudowa nad Spring Framework, która sama dobiera rozsądną konfigurację, ma wbudowany serwer WWW i pozwala uruchomić aplikację jednym poleceniem.

Kluczowe słowo to nadbudowa, a nie zamiennik. Spring Boot nie zastępuje Spring Framework - on na nim stoi i go wykorzystuje. Kiedy piszesz aplikację w Spring Boot, cały czas korzystasz z mechanizmów Spring Framework (choćby z opisanego wyżej wstrzykiwania zależności). Boot jedynie oszczędza Ci ręcznej roboty konfiguracyjnej.

Infografika porównująca Spring Framework i Spring Boot: po lewej zestaw mechanizmów do samodzielnego złożenia, po prawej te same klocki już złożone i gotowe do uruchomienia
Spring Framework a Spring Boot to nie konkurenci. Framework daje mechanizmy (klocki), a Boot sprawia, że te mechanizmy ruszają od ręki: sam konfiguruje, ma wbudowany serwer i uruchamia się jednym poleceniem.

Startery: koniec z ręcznym dobieraniem bibliotek

Pierwsza wygoda Spring Boot to startery. Starter to jedna zależność, która pod spodem wciąga cały zestaw dobranych i przetestowanych razem bibliotek. Zamiast ręcznie zbierać kilkanaście bibliotek i modlić się, żeby ich wersje do siebie pasowały, dopisujesz jeden wiersz.

Infografika startera Spring Boot: jedna zależność spring-boot-starter-web rozwija się w Spring MVC, Tomcat, Jackson i Validation
Idea startera. Jedna zależność spring-boot-starter-web wciąga komplet potrzebny do aplikacji WWW: Spring MVC, wbudowany serwer Tomcat, bibliotekę Jackson do JSON-a i mechanizm walidacji danych.

Tak wygląda fragment pliku pom.xml (pliku, w którym narzędzie Maven opisuje zależności projektu) z naszej księgarni. Trzy startery dają komplet: aplikację WWW, szablony HTML i podgląd stanu aplikacji. Zwróć uwagę, że przy zależnościach nie ma numerów wersji - dobiera je za nas nadrzędny projekt startowy Spring Boot:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.0</version>
</parent>

<dependencies>
    <!-- Web + REST + wbudowany serwer Tomcat -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Szablony HTML renderowane na serwerze -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!-- Podgląd stanu aplikacji: health, metryki -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>
Fragment pom.xml: trzy startery Spring Boot. Wersje bibliotek dobiera nadrzędny spring-boot-starter-parent, więc nie musimy ich podawać ręcznie.

Spring Initializr: generujemy szkielet projektu

Skąd wziąć gotowy projekt z odpowiednim pom.xml? Nie trzeba go pisać ręcznie. Służy do tego Spring Initializr (start.spring.io) - internetowy generator startowych projektów Spring Boot. Wybierasz narzędzie do budowania (my użyliśmy Mavena), język, wersję Javy, wpisujesz nazwę projektu i zaznaczasz zależności. Generator pakuje gotowy projekt do pobrania.

Zrzut ekranu Spring Initializr z wybranym Mavenem, Java 21, metadanymi projektu bookstore i czterema dodanymi zależnościami po prawej stronie
Spring Initializr z ustawieniami naszego projektu: Maven, Java 21, nazwa pl.jsystems / bookstore oraz zależności Spring Web, Thymeleaf, Spring Boot Actuator i Validation. Kliknięcie Generate pobiera gotowy szkielet.

Po lewej stronie ustawiasz typ projektu i dane, po prawej dokładasz zależności (to właśnie startery). Przycisk Generate pobiera archiwum z kompletnym, gotowym do uruchomienia szkieletem. To standardowy pierwszy krok każdego projektu w Spring Boot.

Pierwsza aplikacja: budujemy i uruchamiamy

W wygenerowanym projekcie jest jedna klasa startowa. To od niej wszystko się zaczyna. Adnotacja @SpringBootApplication mówi Springowi: to jest aplikacja Spring Boot, włącz swoje mechanizmy i poszukaj moich komponentów. Metoda main uruchamia cały framework.

@SpringBootApplication
public class BookstoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(BookstoreApplication.class, args);
    }
}
Cała klasa startowa aplikacji Spring Boot. Adnotacja @SpringBootApplication włącza mechanizmy Springa, a SpringApplication.run podnosi aplikację.

Projekt budujemy Mavenem. Polecenie mvn clean package kompiluje kod i pakuje wszystko do jednego pliku JAR - razem z bibliotekami i wbudowanym serwerem w środku. To właśnie dzięki temu aplikację da się później uruchomić jednym poleceniem, bez instalowania osobnego serwera.

Zrzut terminala z budowaniem projektu Spring Boot przez Maven: kompilacja pięciu plików źródłowych, pakowanie do JAR i komunikat BUILD SUCCESS
Budowanie aplikacji poleceniem mvn clean package. Maven kompiluje kod i pakuje go do jednego pliku JAR. Na końcu widać BUILD SUCCESS.

Teraz uruchamiamy aplikację poleceniem java -jar. I tu pojawia się znak rozpoznawczy Spring Boot - charakterystyczny napis Spring w konsoli, a pod nim log startu. Widać w nim, że wbudowany serwer Tomcat wstał na porcie 8085, że nasz komponent zgłosił sześć książek w katalogu, i że cała aplikacja wstała w niecałe sześć sekund:

Zrzut terminala ze startem aplikacji Spring Boot: baner Spring w wersji 3.4.0, uruchomienie wbudowanego serwera Tomcat na porcie 8085 i komunikat Started BookstoreApplication
Uruchomienie aplikacji poleceniem java -jar. Charakterystyczny baner Spring Boot, a pod nim log startu: wbudowany Tomcat na porcie 8085, komunikat naszego serwisu o sześciu książkach i informacja, że aplikacja wstała.

Auto-konfiguracja: magia, która magią nie jest

Skąd Spring Boot wiedział, że ma uruchomić serwer Tomcat na porcie i przygotować obsługę żądań WWW, skoro nigdzie tego nie konfigurowaliśmy? To zasługa auto-konfiguracji. Działa ona według prostej zasady: Boot patrzy, jakie biblioteki masz na ścieżce klas (to znaczy wśród zależności projektu) i na tej podstawie sam włącza rozsądne, domyślne ustawienia.

Animacja auto-konfiguracji Spring Boot: od startera na ścieżce klas, przez sprawdzenie warunku, po gotową aplikację z ustawionym serwerem i komponentami
Auto-konfiguracja krok po kroku. Starter wciąga biblioteki, Spring Boot sprawdza, co jest na ścieżce klas, i gdy widzi Spring MVC oraz Tomcat, sam konfiguruje działającą aplikację WWW.

To nie jest magia - to zwykłe sprawdzanie warunków. Spring Boot potrafi pokazać, co i dlaczego skonfigurował. Wystarczy uruchomić aplikację z opcją --debug, a w konsoli pojawi się raport oceny warunków. Widać w nim, że obsługa żądań i serwer Tomcat zostały włączone, bo odpowiednie klasy są na ścieżce klas (sekcja Positive matches). I odwrotnie: konfiguracje pod inne serwery, jak Jetty, Netty czy Undertow, zostały pominięte, bo tych bibliotek u nas nie ma (sekcja Negative matches):

Zrzut terminala z raportem oceny warunków Spring Boot: sekcja dopasowań pozytywnych z Tomcatem i obsługą żądań oraz sekcja dopasowań negatywnych z pominiętymi serwerami Jetty, Netty i Undertow
Raport auto-konfiguracji z opcji --debug. Positive matches: obsługa żądań i serwer Tomcat włączone, bo ich klasy są na ścieżce klas. Negative matches: Jetty, Netty i Undertow pominięte, bo tych bibliotek nie ma.

REST w praktyce: dane jako JSON

Aplikacja działa, więc sprawdźmy, co potrafi. Pokazany wcześniej kontroler był oznaczony adnotacją @RestController. To znaczy, że zwraca dane, a nie stronę HTML. Adnotacja @GetMapping podpina metodę pod adres, a Spring sam zamienia zwróconą listę obiektów na JSON - lekki, tekstowy format wymiany danych, który rozumie niemal każdy język programowania i każda przeglądarka.

Odpytajmy nasz adres /api/books narzędziem curl. Za pierwszym razem prosimy o cały katalog, a za drugim dokładamy parametr ?category=Spring. Ten parametr trafia do metody przez adnotację @RequestParam i zawęża wynik tylko do książek z kategorii Spring - bez pisania ani linijki dodatkowego kodu obsługi adresu:

Zrzut terminala z dwoma wywołaniami curl do REST-owego adresu książek: pełny katalog w formacie JSON oraz wynik przefiltrowany po kategorii Spring
Odpytanie adresu /api/books narzędziem curl. Pierwsze wywołanie zwraca cały katalog jako JSON, drugie z parametrem category=Spring zawęża wynik do dwóch książek. Dane składa komponent BookstoreService.

Jak Spring obsługuje żądanie (Spring MVC)

Skąd Spring wie, że żądanie spod adresu /api/books ma trafić akurat do naszej metody? Odpowiada za to Spring MVC - moduł do obsługi żądań sieciowych. Sercem jest w nim jeden komponent o nazwie DispatcherServlet, czyli centralny dyspozytor. Każde żądanie przychodzące do aplikacji przechodzi najpierw przez niego, a on kieruje je do właściwej metody kontrolera. Prześledźmy tę drogę:

Animacja przepływu żądania w Spring MVC: przeglądarka, DispatcherServlet, kontroler i serwis, a następnie powrotna droga odpowiedzi JSON
Droga żądania w Spring MVC. Żądanie z przeglądarki trafia do centralnego dyspozytora DispatcherServlet, ten kieruje je do właściwego kontrolera, kontroler prosi serwis o dane, a odpowiedź w formacie JSON wraca do przeglądarki.

Ten sam DispatcherServlet, który widzieliśmy w logu startu, obsługuje wszystkie żądania - i te zwracające dane (JSON), i te zwracające strony HTML. Różnica jest tylko w tym, co kontroler postanowi oddać.

Strona HTML po stronie serwera (Thymeleaf)

Katalog książek, który widziałeś na samej górze artykułu, to nie JSON, tylko gotowa strona HTML. Zbudował ją Thymeleaf - silnik szablonów, który po stronie serwera wstawia dane do szablonu HTML i oddaje przeglądarce gotową stronę. Tym razem kontroler jest oznaczony zwykłym @Controller (a nie @RestController) i zwraca nazwę szablonu:

@Controller
public class ViewController {

    private final BookstoreService service;

    public ViewController(BookstoreService service) {
        this.service = service;
    }

    @GetMapping("/")
    public String catalog(Model model) {
        model.addAttribute("books", service.findAll());
        return "catalog";   // nazwa szablonu: templates/catalog.html
    }
}
Kontroler widoku. Dokłada dane do modelu i zwraca nazwę szablonu 'catalog'. Resztę, czyli złożenie strony, robi Thymeleaf.

W samym szablonie używamy atrybutów Thymeleaf. Atrybut th:each przechodzi w pętli po liście książek i powiela kartę, a th:text wstawia w wyznaczone miejsca konkretne wartości. To dlatego jedna karta w szablonie zamienia się w sześć kart na gotowej stronie:

<article class="card" th:each="b : ${books}">
    <span class="badge" th:text="${b.category}">Kategoria</span>
    <h2               th:text="${b.title}">Tytuł książki</h2>
    <p  class="author" th:text="${b.author}">Autor</p>
    <span th:text="${b.price} + ' zł'">cena</span>
</article>
Fragment szablonu Thymeleaf. Atrybut th:each powiela kartę dla każdej książki, a th:text wstawia tytuł, autora, kategorię i cenę.

Actuator: gotowość produkcyjna

Ostatni element, który dołożyliśmy, to Actuator. To moduł Spring Boot, który wystawia gotowe adresy do podglądu i monitorowania stanu działającej aplikacji: jej kondycji, metryk czy listy komponentów. W praktyce to on odpowiada za produkcyjną dojrzałość aplikacji Spring Boot. Włączamy go i wybieramy, co ma pokazywać, w pliku application.properties:

spring.application.name=bookstore
server.port=8085

# Actuator - wystawiamy punkty monitorujące stan aplikacji
management.endpoints.web.exposure.include=health,info,metrics,beans
management.endpoint.health.show-details=always
Fragment application.properties. Ustawiamy port, włączamy wybrane adresy Actuatora i prosimy o pełne szczegóły kondycji aplikacji.

Najczęściej odpytywany adres to /actuator/health. Zwraca on kondycję aplikacji i jej składowych. W naszym przypadku wszystko jest na zielono - status UP, a pod nim stan miejsca na dysku, testu ping i certyfikatów:

Zrzut terminala z odpowiedzią adresu health Spring Boot Actuator: status UP oraz szczegóły składowych diskSpace, ping i ssl w formacie JSON
Adres /actuator/health z modułu Actuator. Zwraca kondycję aplikacji: status UP oraz stan składowych - miejsca na dysku, testu ping i certyfikatów SSL.

Ekosystem Spring to nie jeden projekt

Spring to dziś cała rodzina projektów zbudowanych na wspólnym fundamencie. Poza samym rdzeniem i Spring Boot warto znać kilka nazw, które prędzej czy później spotkasz:

Infografika ekosystemu Spring z sześcioma projektami: Spring Boot, Spring Data, Spring Security, Spring Cloud, Spring Batch i Spring WebFlux
Ekosystem Spring. Poza Spring Boot to między innymi Spring Data (dostęp do baz), Spring Security (logowanie i uprawnienia), Spring Cloud (aplikacje rozproszone), Spring Batch (przetwarzanie wsadowe) i Spring WebFlux (programowanie reaktywne).

Szczególnie warto zapamiętać Spring Cloud. To zestaw narzędzi do budowy aplikacji rozproszonych, czyli takich, które zamiast jednego dużego programu składają się z wielu małych współpracujących usług (mikroserwisów). To właśnie Spring Boot najczęściej stoi za pojedynczym mikroserwisem. Jeśli chcesz zrozumieć, kiedy w ogóle warto dzielić aplikację na mniejsze części, zajrzyj do naszego osobnego przewodnika monolit kontra mikroserwisy - dobrze uzupełnia to, co pokazaliśmy tutaj.

Podsumowanie

Spring Framework to modularny szkielet do budowy aplikacji w Javie, którego sercem jest wstrzykiwanie zależności: to kontener tworzy obiekty i sam podaje je tam, gdzie są potrzebne. Spring Boot to nadbudowa nad tym szkieletem, która dzięki starterom i auto-konfiguracji sama dobiera ustawienia, ma wbudowany serwer i pozwala uruchomić aplikację jednym poleceniem. W naszej małej księgarni zobaczyliśmy oba w akcji: od wygenerowania projektu, przez budowę i start z charakterystycznym banerem, po REST zwracający JSON, stronę HTML z Thymeleaf i podgląd kondycji przez Actuator.

Najlepszy sposób nauki to praktyka: wygeneruj własny projekt na Spring Initializr, dodaj kontroler z jedną metodą i uruchom aplikację, dokładnie tak jak pokazaliśmy. A jeśli chcesz przejść przez to z trenerem praktykiem i poukładać wiedzę o Springu w dobrej kolejności - od wstrzykiwania zależności, przez Spring MVC, po dostęp do danych i transakcje - mamy dla Ciebie szkolenie.

Chcesz opanować Springa od praktyka?

Szkolenie Spring Framework - szkolenie kompleksowe przeprowadzi Cię przez cały framework na żywym kodzie: wstrzykiwanie zależności, konfigurację, Spring MVC, dostęp do danych i transakcje. Szkolenie ma termin gwarantowany, czyli odbędzie się na pewno.

Baner szkolenia Spring Framework - szkolenie kompleksowe w JSystems, z terminem gwarantowanym
Powiększony zrzut ekranu z artykułu o Spring Framework i Spring Boot

Komentarze (0)

Musisz być zalogowany by móc dodać komentarz. Zaloguj się przez Google

Brak komentarzy...