CodeGym /Kursy /Spring REST & MVC /Endpointy w Spring MVC

Endpointy w Spring MVC

Spring REST & MVC
Poziom 5, Lekcja 1
Dostępny

1. Endpoint w Spring MVC jako część kontraktu

Gdy początkujący po raz pierwszy widzi kontroler Spring, łatwo poczuć, jakby adnotacje były po prostu sposobem „jakoś przypisać URL do metody". Ale w rzeczywistości robisz coś ważniejszego: opisujesz publiczny interfejs swojego serwisu. Endpoint — to nie tylko metoda Java, lecz powiązanie „metoda HTTP + ścieżka" — i właśnie ono będzie żyło długo i stabilnie, nawet jeśli przepiszesz połowę wewnętrznego kodu.

Żeby poczuć różnicę, wyobraź sobie, że metoda Java — to „wewnętrzny przycisk" w twoim urządzeniu, a endpoint — to przycisk na panelu z zewnątrz, który klienci faktycznie naciskają. Klientowi nie jest ważne, jak nazywa się twoja metoda listTasks() — i tak jej nigdy nie zobaczy. Ważne dla klienta jest to, że może zrobić GET /api/v1/tasks i dostać zrozumiałą odpowiedź.

W tym kursie świadomie traktujemy endpointy jako kontrakt, dlatego interesują nas dwie rzeczy: jak deklarować endpoint w kodzie i jak robić to czytelnie. Czytelnie — to znaczy, że otworzyłeś klasę, przebiegłeś wzrokiem po adnotacjach i już rozumiesz, jakie trasy są obsługiwane. Niemal jak mapa metra: linie i stacje muszą być widoczne, inaczej podróże zamieniają się w quest.

Mała „tabela kontraktu", żeby mieć oparcie w głowie:

Część kontraktu Przykład Jak wyrażamy w Spring MVC
Metoda HTTP GET @GetMapping (albo
@RequestMapping(method=GET)
)
Ścieżka /api/v1/tasks @RequestMapping("/api/v1/tasks"),
@GetMapping("/summary")
itp.
Gdzie żyje endpoint kontroler + metoda @RestController + metoda z adnotacją mappingu

O ciele żądania, serializacji JSON i kodach statusu celowo nie mówimy teraz. W tej lekturze ważniejsze jest nauczenie się deklarowania tras tak, żeby po kodzie kontrolera widać było budowę API.

2. @RestController: tryb API

Gdy piszesz backend API, zazwyczaj chcesz, żeby metoda kontrolera zwracała dane, które staną się ciałem odpowiedzi HTTP. I tu zaczyna się zamieszanie: w Spring są „kontrolery dla stron" (gdy zwracasz nazwę szablonu HTML), i są „kontrolery dla API" (gdy zwracasz dane). @RestController właśnie mówi Springowi: „to API-kontroler, nie próbuj robić z niego generatora HTML".

Wewnątrz Springa jest to rozwiązane dość elegancko: @RestController — to w zasadzie „zbiorowa adnotacja", która oznacza „kontroler + wartość zwracana = ciało odpowiedzi". Nie musisz pamiętać szczegółów implementacji, wystarczy zapamiętać myśl: jeśli budujesz API, które przede wszystkim oddaje JSON, to podstawowa adnotacja kontrolera — @RestController.

Najmniejszy „ping", który pokazuje, że endpoint jest zadeklarowany i działa, wygląda tak:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // Mówimy Springowi: to REST API-kontroler, wartość zwracana — ciało odpowiedzi
class PingController {

    @GetMapping("/api/v1/ping") // Kontrakt: GET /api/v1/ping
    public String ping() {
        return "pong"; // To stanie się ciałem odpowiedzi HTTP
    }
}

Jeśli do twojej aplikacji przyjdzie żądanie GET /api/v1/ping, Spring dopasuje je do tej metody i wywoła ping(). A string "pong" trafi do ciała odpowiedzi HTTP (tak, po prostu jako string — to normalne dla demonstracji, choć w prawdziwym API częściej będzie JSON).

Praktyczny niuans, który często „strzela" początkującym: jeśli przypadkowo postawisz @Controller zamiast @RestController, Spring może zacząć traktować string "pong" jako nazwę widoku (view name), czyli jako „nazwę szablonu HTML". I będziesz siedzieć i myśleć: „Dlaczego zwracam pong, a serwer szuka jakiegoś pong.html?". To klasyczna sytuacja, gdy wydaje się, że „Spring się zepsuł", a tymczasem po prostu włączyłeś inny tryb pracy.

Jeszcze jeden ważny moment: Spring musi znaleźć twój kontroler. Zwykle dzieje się to dzięki skanowaniu komponentów: jeśli twój @SpringBootApplication jest w pakiecie głównym com.example.tasktracker, Spring zobaczy klasy niżej w drzewie pakietów, na przykład com.example.tasktracker.api.controller. Dlatego od samego początku warto trzymać kontrolery w jednym zrozumiałym miejscu, a nie rozrzucać po całym projekcie „jak pliki na pulpicie".

3. @RequestMapping na klasie: wspólny prefiks

Gdy projektujesz zasób tasks, logiczne jest, że wszystkie operacje na nim zaczynają się od wspólnego prefiksu /api/v1/tasks. Jeśli będziesz pisać ten prefiks w każdej metodzie kontrolera, wyjdzie dużo powtórzeń, a powtórzenia — to prawie zawsze przyszłe błędy. Raz literówka, raz zapomniane /api/v1, i nagle połowa API „uciekła" pod inną ścieżkę.

Właśnie do tego służy @RequestMapping na poziomie klasy: ustawia wspólny prefiks ścieżki dla wszystkich metod wewnątrz kontrolera. Bardzo wygodny model mentalny: klasa kontrolera — to jak „folder", a metody — jak „pliki" w środku. Folder ustawia bazową ścieżkę, a każda metoda dodaje „ogon".

Dla mechaniki wspólnego prefiksu weźmy demonstracyjny DemoTaskController: tu ważne, żeby zobaczyć właśnie ścieżkę na poziomie klasy, bez reszty projektowej otoczki.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // Tryb REST: wartości zwracane przez metody — ciało odpowiedzi
@RequestMapping("/api/v1/tasks") // Bazowy prefiks dla wszystkich endpointów tego kontrolera
class DemoTaskController {

    @GetMapping // GET na bazową ścieżkę kontrolera: /api/v1/tasks
    public String listTasks() {
        return "tasks"; // Dla demonstracji zwracamy string
    }
}

Tu ważne jest zobaczenie dwóch rzeczy.

Pierwsza: @RequestMapping("/api/v1/tasks") na klasie mówi, że kontroler jako całość odpowiada za zasób tasks w wersji /api/v1.

Druga: @GetMapping bez argumentów oznacza „GET na bazową ścieżkę kontrolera". Czyli wynikowy endpoint — GET /api/v1/tasks. Nie piszemy "/api/v1/tasks" po raz drugi, i to nie lenistwo, lecz dyscyplina: im mniej powtarzającego się tekstu w kontrakcie, tym mniejsze ryzyko zrobienia /tasks/tasks.

Przy okazji, @RequestMapping ma kilka wariantów zapisu, które czasem wprowadzają w błąd, ale znaczeniowo są bliskie:

// Wszystkie trzy zapisy poniżej są równoważne: to ta sama ścieżka, tylko różne nazwy parametru
@RequestMapping("/api/v1/tasks")
@RequestMapping(path = "/api/v1/tasks")
@RequestMapping(value = "/api/v1/tasks")

W projekcie edukacyjnym lepiej wybrać jeden styl i się go trzymać. Zwykle najprostszy — bez zbędnych słów: @RequestMapping("/api/v1/tasks").

4. @GetMapping: czytelna deklaracja GET

@RequestMapping — to „szwajcarski scyzoryk": można nim zadeklarować endpoint prawie dla czegokolwiek. Ale problem ze szwajcarskimi scyzorykami polega na tym, że jeśli ciągle używasz tylko noża, łyżki i widelca, wyjmowanie za każdym razem „uniwersalnej rzeczy na 27 funkcji" jest trochę głośne.

W Spring MVC jest cała rodzina specjalizowanych adnotacji mappingu, które czynią kod krótszym i jaśniejszym. Dla operacji GET używa się @GetMapping. To nie „inny mechanizm", lecz po prostu bardziej czytelny sposób na powiedzenie: „ta metoda obsługuje GET".

Porównaj dwa warianty. Oba działają tak samo:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // REST API-kontroler
class PingController {

    @GetMapping("/api/v1/ping") // Krótki i czytelny zapis kontraktu: GET /api/v1/ping
    public String ping() {
        return "pong"; // Trafi do body odpowiedzi
    }
}

i bardziej „uniwersalny", ale bardziej hałaśliwy:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController // REST API-kontroler
class PingController {

    @RequestMapping(path = "/api/v1/ping", method = RequestMethod.GET) // To samo, ale przez uniwersalny @RequestMapping
    public String ping() {
        return "pong"; // Ciało odpowiedzi
    }
}

W drugim wariancie zużywamy więcej znaków na powiedzenie tego samego. A w kodzie kontraktowym, który czyta się częściej niż pisze, zbędne znaki — to nie ozdoba, lecz śmieć (czasem bardzo drogi).

Żeby obraz był kompletny, oto mała tabela „rodziny":

Metoda HTTP Adnotacja mappingu Intuicyjny sens (bez zagłębiania)
GET @GetMapping czytanie zasobu/kolekcji
POST @PostMapping tworzenie
PUT @PutMapping pełna zamiana
PATCH @PatchMapping częściowa zmiana
DELETE @DeleteMapping usuwanie

W ramach dzisiejszego dnia interesuje nas właśnie @GetMapping, bo zaczynamy od prostych odczytów. Ale ważne, żeby już teraz przyzwyczaić się do idei: w Spring MVC przyjęte jest wyrażanie kontraktu webowego deklaratywnie — a więc maksymalnie czytelnie.

5. Składanie pełnej ścieżki endpointu

Gdy używasz @RequestMapping na klasie i adnotacji mappingu na metodzie, wynikowa ścieżka powstaje przez sklejenie tych dwóch części. To bardzo wygodne, ale ta wygoda ma efekt uboczny: jeśli pomylisz, co ustawiasz na klasie, a co na metodzie, łatwo złożysz ścieżkę, która formalnie jest poprawna, ale logicznie absurdalna.

Utrwalmy tę mechanikę w jednej tabeli. Powiedzmy, że mamy:

@RequestMapping("/api/v1/tasks")
class DemoTaskController { ... }

Wtedy:

Adnotacja na metodzie Wynikowa ścieżka Jak to czytać
@GetMapping /api/v1/tasks „lista zadań" (kolekcja)
@GetMapping("/summary") /api/v1/tasks/summary „lista zadań, ale w specjalnej reprezentacji"
@GetMapping("/{taskId}") /api/v1/tasks/{taskId} „jedno zadanie po id" (placeholder w ścieżce)

Zwróć uwagę: w stringach z adnotacjami na metodzie dodajemy tylko „ogon". Dlatego "/summary", a nie "/api/v1/tasks/summary".

Teraz pokażmy anty-wzorzec, który spotyka się dosłownie u co drugiego początkującego (nic strasznego, to niemal obowiązkowy etap dojrzewania):

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // REST API-kontroler
@RequestMapping("/api/v1/tasks") // Bazowa ścieżka zasobu tasks
class BadDemoTaskController {

    @GetMapping("/tasks") // Błąd: powielamy segment i dostajemy /api/v1/tasks/tasks
    public String listTasks() {
        return "wrong"; // Demonstracja: będzie odpowiadać, ale kontrakt stanie się mniej czysty
    }
}

W efekcie wyjdzie ścieżka /api/v1/tasks/tasks. Technicznie serwer „nie zepsuł się" — uczciwie wstał i będzie obsługiwał ten URL. Ale kontrakt API stał się gorszy: mniej przewidywalny, mniej czytelny i na pewno wywoła u klienta pytanie: „A dlaczego tasks dwa razy?".

Czasem wydaje się, że „no i co, klient może wywołać". Może. Ale właśnie takie drobiazgi razem zamieniają API w zestaw przypadkowych rzeczy. A my uczymy się robić API takie, żeby można je było zrozumieć bez telepatii.

Jeśli chcesz wizualizacji, możesz trzymać w głowie taki schemat:

flowchart TB
    A["@RequestMapping('/api/v1/tasks') DemoTaskController"] --> B["@GetMapping listTasks()"]
    A --> C["@GetMapping('/summary') summary()"]
    A --> D["@GetMapping('/{taskId}') details()"]

Ważne tu nie to, że nauczyłeś się Mermaid. Ważne, że widzisz: klasa ustawia „korzeń drzewa", a metody — gałęzie.

6. Dwa style deklaracji ścieżek

W Spring MVC można spotkać oba style i oba są technicznie dopuszczalne. Ale z punktu widzenia czytelności kontraktu (a to nasza główna waluta w tym kursie) są bardzo różne.

Pierwszy styl — „pełna ścieżka w każdej metodzie". Wygląda tak:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // REST API-kontroler
class DemoTaskController {

    @GetMapping("/api/v1/tasks") // Pełna ścieżka zadeklarowana tu: GET /api/v1/tasks
    public String listTasks() {
        return "tasks"; // Uproszczona odpowiedź dla demonstracji
    }
}

Plus polega na tym, że ścieżka widoczna jest w całości bezpośrednio w metodzie. Minus w tym, że jeśli masz 5–10 metod, dostajesz 5–10 powtórzeń /api/v1/tasks i wszystkie te stringi trzeba utrzymywać w identycznej formie.

Drugi styl — „wspólny prefiks na klasie". Wygląda tak:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // REST API-kontroler
@RequestMapping("/api/v1/tasks") // Wspólny prefiks dla wszystkich metod
class DemoTaskController {

    @GetMapping // Tu widać operację, a ścieżka składa się automatycznie: GET /api/v1/tasks
    public String listTasks() {
        return "tasks"; // Uproszczona odpowiedź dla demonstracji
    }
}

Ten wariant jest zazwyczaj łatwiejszy do utrzymania i łatwiej czyta się jako mapa API. Raz zobaczyłeś, że klasa jest o tasks, i dalej pozostaje ci rozróżnianie operacji wewnątrz zasobu.

W projekcie edukacyjnym Task Tracker API będziemy trzymać się drugiego stylu, bo lepiej wspiera myślenie zasobowe: jeden kontroler — jeden zasób, a ścieżka zasobu ustawiana jest na klasie.

Jeszcze jeden praktyczny moment: @RequestMapping można stawiać zarówno na klasach, jak i na metodach. Ale „średnia po szpitalu" to bardzo dobry nawyk: na klasie — @RequestMapping jako bazowa ścieżka zasobu, a na metodach — bardziej konkretne adnotacje (@GetMapping i inne). Tak mniejsze ryzyko przypadkowego stworzenia endpointu reagującego na „wszystkie metody naraz" bez twojej świadomości.

7. Czytelny kontrakt: nazwy i struktura URI

Gdy pracujesz w zespole, kontrolery czyta się częściej niż pisze. Czytają je koledzy, którzy dodają funkcje. Czytają je testerzy (czasem dosłownie oczami). Czyta je twoje przyszłe ja, gdy za dwa miesiące zapomnisz, co dokładnie miałeś na myśli. Dlatego kontroler powinien wyglądać jak schludny szyld, a nie jak płótno „i tak ujdzie".

Zaczyna się od prostego: kontroler nazywa się wg zasobu w liczbie pojedynczej, na przykład TaskController, a ścieżka zasobu — w liczbie mnogiej, na przykład /tasks. Wygląda jak drobiazg, ale właśnie z takich drobiazgów buduje się API, które można odgadnąć bez dokumentacji.

Dalej — grupowanie. Nie warto wrzucać do jednego kontrolera „wszystkiego, co ma związek z zadaniami, komentarzami, tagami i pogodą w Tokio". Nawet jeśli umiesz, nawet jeśli jest ci nudno. Jeden kontroler — jedna strefa odpowiedzialności. Na dzisiaj wystarczy nam zasób tasks.

Przykład kontrolera, który czyta się jak mała mapa API:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // REST API-kontroler
@RequestMapping("/api/v1/tasks") // Zasób: tasks (kolekcja)
class DemoTaskController {

    @GetMapping // GET /api/v1/tasks — pobierz listę
    public String listTasks() {
        return "list"; // Odpowiedź demonstracyjna
    }

    @GetMapping("/summary") // GET /api/v1/tasks/summary — alternatywna reprezentacja
    public String summary() {
        return "summary"; // Odpowiedź demonstracyjna
    }
}

Na razie zwracane są stringi, bo naszym celem jest zobaczenie struktury endpointów. Ale już teraz widać, jakie trasy są dostępne. I to główne kryterium sukcesu na bieżącym kroku: otworzyłeś kontroler i rozumiesz, co on robi, bez czytania „wnętrzności" metod.

I jeszcze jeden mikro-nawyk, który bardzo pomaga: nie robić nazw metod zbyt „technicznymi". Nazwa listTasks() jest lepsza niż getMappingForTasks() (tak, to też się zdarza). Kontroler — to warstwa kontraktu, a kontrakt powinien mówić słowami „co robimy", a nie „jak technicznie jest zbudowane".

8. Typowe błędy przy deklaracji endpointów

Błąd №1: używać @Controller zamiast @RestController i dziwić się „gdzieś się podziało ciało odpowiedzi".
Jeśli piszesz API, które przede wszystkim oddaje JSON, i zwracasz string albo obiekt, oczekujesz, że to stanie się ciałem odpowiedzi. Ale @Controller historycznie jest bardziej o stronach MVC i zwracany string może być traktowany jako nazwa widoku. W efekcie nie dostajesz tego, czego oczekujesz, i wydaje się, że „Spring wariuje". Lekarstwo jest proste: dla REST API używaj @RestController.

Błąd №2: powielać segmenty ścieżki i przypadkowo dostać /api/v1/tasks/tasks.
Dzieje się to, gdy na klasie jest już @RequestMapping("/api/v1/tasks"), a na metodzie dodajesz @GetMapping("/tasks"). Formalnie wszystko działa, ale kontrakt staje się mniej czysty i mniej przewidywalny. Dobra zasada — na klasie trzymać bazową ścieżkę zasobu, a na metodzie dodawać tylko ogon ("/summary"), albo w ogóle nic nie dodawać dla korzenia zasobu.

Błąd №3: mieszać w jednym kontrolerze różne zasoby „bo tak szybciej".
Dziś dodałeś do TaskController jeszcze i GET /api/v1/tags, „bo to też w pobliżu". Jutro dorzuciłeś komentarze. Pojutrze kontroler zamienia się w wysypisko niepowiązanych endpointów. Cena za „szybciej teraz" — „boleśnie zawsze". Kontroler musi być zgrupowany wokół jednego zasobu, inaczej czytanie kontraktu staje się ciężkie.

Błąd №4: robić czasownikowe ścieżki w stylu /api/v1/getTasks albo /api/v1/tasks/listAll.
Umówiliśmy się myśleć zasobami: tasks — to kolekcja. Do czytania kolekcji mamy GET. Jeśli dodajesz czasownik do ścieżki, pogarszasz czytelność i zaczynasz ześlizgiwać się w styl RPC. Ścieżka — o zasobie, metoda — o operacji.

Błąd №5: deklarować mapping zbyt ogólny i nieświadomie „łapać wszystko po kolei".
Czasem początkujący stawia @RequestMapping("/api/v1/tasks") na metodzie i zapomina ograniczyć metodę HTTP, a potem dziwi się, że endpoint reaguje nie tak, jak oczekiwał. Uniwersalny @RequestMapping wymaga ostrożności: jeśli chcesz GET — używaj @GetMapping. To i jaśniejsze, i bezpieczniejsze dla kontraktu.

1
Zadanie
Spring REST & MVC, poziom 5, lekcja 1
Niedostępne
Wspólny prefiks zasobu books
Wspólny prefiks zasobu books
1
Zadanie
Spring REST & MVC, poziom 5, lekcja 1
Niedostępne
Dwa zasoby — dwa kontrolery
Dwa zasoby — dwa kontrolery
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION