CodeGym /Kursy /Spring REST & MVC /consumes i produces w Spring MVC

consumes i produces w Spring MVC

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

1. Jawna deklaracja formatu w JSON-first API

Na początku wydaje się, że consumes/produces to jakaś biurokracja na poziomie „zaznacz checkbox, że zaznaczyłeś checkbox". Ale w projektowaniu API detale nagłówków są jak podpis w umowie: dopóki wszystko jest dobrze, nikt go nie zauważa, a gdy coś idzie nie tak – nagle okazuje się, że właśnie on rozstrzyga spór. Jeśli nie deklarujemy formatu jawnie, klient może przysłać ciało żądania w nieoczekiwanej postaci (lub z nieoczekiwanym Content-Type), a my potem długo będziemy się zastanawiać, dlaczego kontroler „jakby się nie wywołuje". Podobnie, jeśli klient prosi o jeden format przez Accept, a serwer daje inny, zamienia się to w „pływający" problem, gdzie każdy jest pewien, że to nie jego wina.

W naszym kursie świadomie budujemy JSON-first API, czyli główny format to application/json. Nie oznacza to, że można przestać myśleć o typach mediów. Wręcz przeciwnie: skoro świadomie wybieramy jeden format, logicznie jest też świadomie deklarować go w kontrakcie endpointów. Wtedy każdy developer, otwierając kontroler, w 10 sekund rozumie: „aha, ten endpoint przyjmuje JSON i zwraca JSON", a nie „no, chyba tak domyślnie". To oszczędza czas przy debugowaniu, czas na tłumaczenie kolegom i czas na korespondencję z klientami, którzy przysyłają text/plain, bo „tak wygodniej".

Content-Type i Accept: nagłówki formatu

Żeby poprawnie używać consumes i produces, trzeba znowu szybko przypomnieć sens dwóch kluczowych nagłówków. To nie „jakaś teoria z RFC", lecz dosłownie język, którym klient i serwer dogadują się, jak czytać i jak pisać body. Content-Type dotyczy ciała żądania: klient mówi serwerowi „oto moje body, jest w takim formacie". Accept dotyczy ciała odpowiedzi: klient mówi serwerowi „chcę dostać odpowiedź w takim formacie".

W JSON-first API wygląda to zazwyczaj tak: klient wysyła Content-Type: application/json, a także (często) dodaje Accept: application/json. Jeśli Accept nie jest podany, wiele klientów zachowuje się domyślnie jakby tam był */* (czyli „pasuje mi każdy format"), i serwer daje to, co potrafi. Ale w prawdziwym życiu obecność jawnego Accept bardzo pomaga: zamienia „nadzieję" w kontrakt.

Mała tabelka, żeby wszystko ułożyło się w głowie porządnie (i żebyś nie mylił tego automatycznie):

Encja Gdzie żyje Kto ustawia O czym mówi Przykład
Content-Type nagłówek HTTP żądania klient „Jaki format ma moje request body" Content-Type: application/json
Accept nagłówek HTTP żądania klient „W jakim formacie chcę response body" Accept: application/json
consumes adnotacja mapping w Spring MVC serwer (my) „Jakie Content-Type jestem gotowy przyjmować w request body" consumes = "application/json"
produces adnotacja mapping w Spring MVC serwer (my) „Jakie formaty jestem gotowy oddawać w response body" produces = "application/json"

Zwróć uwagę na lekką „pułapkę dla początkujących": Content-Type i Accept to nagłówki w żądaniu, a consumes/produces to „reguły" po stronie serwera, według których Spring albo wybierze twoją metodę kontrolera, albo powie „nie, tak się nie umawialiśmy". I właśnie tego zachowania nam dziś potrzeba.

2. consumes i produces jako filtr kontraktu

Jeśli powiedzieć to po ludzku, consumes i produces to część reguł dopasowania endpointu. Czyli Spring nie tylko patrzy na ścieżkę i metodę HTTP (POST /api/v1/tasks), ale też sprawdza: „czy to żądanie pasuje formatem?". I tu consumes/produces włączają dyscyplinę: jeśli żądanie przyszło z Content-Type, którego nie przyjmujesz, Spring może zwrócić 415 Unsupported Media Type. Jeśli żądanie przyszło z Accept, którego nie możesz spełnić, Spring może zwrócić 406 Not Acceptable.

Ważne jest uchwycenie właśnie tego momentu: w takich przypadkach twój kod wewnątrz metody kontrolera może w ogóle się nie wykonać. I to normalne. To nie „Spring się zepsuł". Oznacza to, że żądanie nie pasuje do kontraktu na poziomie ramki HTTP. Twoja logika biznesowa nie ma tu nic do rzeczy: po prostu nie wiesz, jak czytać ciało, lub nie możesz dostarczyć odpowiedzi tak, jak prosi klient.

Można to sobie wyobrazić jak kołowrotek w metrze. Masz bilet – przechodzisz, nie masz – nie przechodzisz. Kołowrotek nie musi dyskutować filozoficznych pytań „a dlaczego tak", jego zadanie – szybko i czytelnie filtrować przepływ. consumes i produces to taki sam kołowrotek, tylko dla formatu body.

Schemat na poziomie „wystarczający dla Junior" może wyglądać tak:

flowchart TD
    A[Żądanie HTTP] --> B[Spring MVC: szukanie pasującej metody handlera]
    B --> C{Ścieżka i metoda pasują?}
    C -->|nie| X["404 / no handler"]
    C -->|tak| D{consumes pasuje do Content-Type?}
    D -->|nie| E[415 Unsupported Media Type]
    D -->|tak| F{produces pasuje do Accept?}
    F -->|nie| G[406 Not Acceptable]
    F -->|tak| H[Wywołanie metody kontrolera]

Na razie nie omawiamy wewnętrznej mechaniki „kto dokładnie czyta body" i „który komponent odpowiada za JSON". Ale już teraz warto mieć w głowie: consumes/produces to część filtrowania żądania przed twoją logiką aplikacyjną, i bezpośrednio dotyczy kontraktu API.

3. Deklarujemy typ mediów w kontrolerze i metodach

produces na poziomie TaskController

Teraz zróbmy praktyczną rzecz w naszym Task Tracker API: na poziomie kontrolera zadeklarujemy, że zwraca JSON. Dlaczego na poziomie kontrolera? Bo wtedy nie będziemy pisać tego samego w każdej metodzie, a kontrakt stanie się czytelny „z zewnątrz": otworzyłeś klasę – od razu wiesz, że tu mamy application/json.

Wyobraź sobie, że kontroler to „folder" z endpointami. Jeśli w tym folderze prawie wszystkie odpowiedzi to JSON, logicznie napisać to raz na folderze, a nie na każdym pliku w środku. Dla kontrolera JSON-first jest to wygodne jeszcze i dlatego, że inny produces potrzebny jest tylko tam, gdzie odpowiedź naprawdę ma inny format. Ale dziś jesteśmy w świecie JSON-first i chcemy to uczynić jawnym.

Minimalny szkielet kontrolera z produces:

package com.example.tasktracker.api.controller;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // Mówimy Spring'owi: to kontroler REST, odpowiedzi będą trafiać do body
@RequestMapping(
        path = "/api/v1/tasks", // Bazowa ścieżka dla wszystkich endpointów zadań
        produces = MediaType.APPLICATION_JSON_VALUE // Domyślnie oddajemy JSON
)
public class TaskController {

    // Tu będą metody handlera (GET/POST/PUT/DELETE) do pracy z zadaniami
}

Robimy tu dwie ważne rzeczy. Po pierwsze, deklarujemy bazową ścieżkę /api/v1/tasks, żeby metody wewnątrz mogły być bardziej zwięzłe. Po drugie, mówimy Spring MVC: „wszystko co wychodzi z tego kontrolera jako response body, będzie oddawane jako JSON". I to nie tylko „żeby klientowi było ładnie". To o tym, żeby nasze API było przewidywalne: nawet jeśli zwrócisz obiekt bezpośrednio, Spring ustawi właściwy Content-Type w odpowiedzi.

consumes dla @RequestBody

Następny logiczny krok – zadeklarować format wejściowy dla tych operacji, gdzie czytamy request body. W naszym projekcie to tworzenie i aktualizacja zadań. Gdy w sygnaturze metody pojawia się @RequestBody, semantycznie mówimy: „klient przysyła ciało, my je czytamy". Znaczy, od razu warto powiedzieć: „czytamy tylko JSON". I właśnie to wyraża consumes = application/json.

Dlaczego to nie tylko „ładność"? Bo inaczej klient może przysłać body bez Content-Type (lub z text/plain), i będziesz się dziwił, dlaczego Spring nie chce zamieniać tego w twój obiekt Java. Z consumes wszystko staje się uczciwe: mamy umowę – JSON, i jeśli klient przynosi nie-JSON, żądanie nie przechodzi.

Przykład POST tworzenia zadania:

import com.example.tasktracker.api.dto.request.TaskCreateRequest;
import java.net.URI;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) // Przyjmujemy tylko JSON w request body
public ResponseEntity<Task> create(@RequestBody TaskCreateRequest request) {
    // request przyszedł z JSON-ciała żądania i już jest sparsowany przez Spring

    Task created = taskService.create(request.getTitle(), request.getDescription());

    // Dla 201 Created dobra praktyka to zwrócenie Location z URL stworzonego zasobu
    URI location = URI.create("/api/v1/tasks/" + created.getId());

    return ResponseEntity
            .created(location) // HTTP 201 + nagłówek Location
            .body(created);    // Ciało odpowiedzi (JSON, bo produces ustawiony na kontrolerze)
}

Zwróć uwagę na niuans: produces tu nie piszemy, bo już zadeklarowaliśmy go na poziomie kontrolera. Jeśli tak jest ci czytelniej, możesz pisać też w metodzie, ale wtedy zaczniesz powtarzać to samo. W projekcie edukacyjnym lepiej trzymać powtórzenia pod kontrolą, żeby kod nie wyglądał jak „tapeta z adnotacji".

Dla PUT i PATCH podejście jest takie samo: jeśli jest @RequestBody, metoda ma format wejściowy, który możemy jawnie zadeklarować:

import com.example.tasktracker.api.dto.request.TaskUpdateRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

@PutMapping(path = "/{taskId}", consumes = MediaType.APPLICATION_JSON_VALUE) // Czytamy JSON z body
public ResponseEntity<Task> replace(@PathVariable String taskId,
                                    @RequestBody TaskUpdateRequest request) {
    // taskId przyszedł z URL, request — z JSON-ciała
    Task updated = taskService.replace(taskId, request.getTitle(), request.getDescription());

    return ResponseEntity.ok(updated); // 200 OK + ciało JSON (produces na kontrolerze)
}

Natomiast DELETE zazwyczaj nie wymaga consumes: usunięcie zwykle nie ma ciała żądania. Z produces też można być ostrożniejszym, bo 204 No Content to sukces bez body. Możesz mieć ogólny produces na kontrolerze i to będzie działać, ale warto pamiętać: 204 to „brak ciała", więc format ciała jest tu mniej istotny niż status.

4. Żądania klientów i 415/406

Teraz przydatny moment, który mocno oszczędza godziny debugowania. Gdy dodałeś consumes/produces, część żądań może zacząć zwracać 415/406, i początkujący developer często odbiera to jako „coś mi się zepsuło w kontrolerze". Ale nie: po prostu dodałeś dyscyplinę kontraktową, i teraz żądania muszą jej odpowiadać.

Przykład poprawnego żądania tworzenia zadania w stylu .http:

# Poprawne żądanie: i Content-Type, i Accept są zgodne z kontraktem kontrolera
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json
Accept: application/json

{
  "title": "Fix login bug",
  "description": "Users cannot sign in after last deploy"
}

Jeśli przypadkowo wyślesz ciało bez Content-Type lub z niewłaściwym typem, na przykład tak:

# Niepoprawny Content-Type: serwer czeka na JSON, a przyszedł text/plain
POST http://localhost:8080/api/v1/tasks
Content-Type: text/plain
Accept: application/json

{"title":"Fix login bug"}

to Spring może zwrócić 415 Unsupported Media Type. I ważne: to nie błąd „twojej logiki biznesowej". To błąd na poziomie „nie dogadaliśmy się, jak czytać body". Twój taskService.create(...) w tym scenariuszu nie jest winien i prawdopodobnie nawet nie został wywołany.

A teraz przykład z Accept. Załóżmy, że klient mówi: „chcę tekst":

# Niepoprawny Accept: klient prosi text/plain, a serwer według kontraktu daje JSON
GET http://localhost:8080/api/v1/tasks
Accept: text/plain

Jeśli kontroler ma produces = application/json, Spring może zwrócić 406 Not Acceptable. To sytuacja „serwer może dać tylko JSON, a klient chce text/plain". W prawdziwym życiu przeglądarki raczej tak nie będą robić, ale niektóre narzędzia i integracje – owszem.

I tu pojawia się bardzo użyteczny inżynierski nawyk: gdy widzisz 415 lub 406, w pierwszej kolejności patrzysz nie na serwis i nie na repozytorium, lecz na nagłówki żądania. To właśnie tam najczęściej siedzi problem.

5. Jak uniknąć „lasu adnotacji"

Teraz – o dyscyplinie w kodzie. Bardzo łatwo popaść w dwie skrajności. Pierwsza – nie deklarować w ogóle nic i liczyć na „domyślnie i tak JSON". Druga – pisać consumes i produces wszędzie, ciągami, czasem z literówkami, i potem bohatersko debugować, dlaczego połowa endpointów się nie dopasowuje.

Zdrowa droga dla projektu edukacyjnego wygląda zazwyczaj tak: produces deklarujemy na poziomie kontrolera (lub wspólnego @RequestMapping), żeby cały zestaw endpointów był w jednym formacie. A consumes stawiamy tylko tam, gdzie naprawdę czytamy body przez @RequestBody. To czyni kontrakt jawnym, ale nie zamienia kodu w firankę z adnotacji.

Przy tym ważne jest unikanie magicznych ciągów. Pisanie "application/json" ręcznie wszędzie to jak ręczne wpisywanie „localhost" w każdej linijce kodu: raz się pomylisz – i już, „jakoś nie działa". Dlatego używaj stałych Spring:

import org.springframework.http.MediaType;

// Jedno miejsce, gdzie deklarujemy typy mediów naszego API
public final class ApiMediaTypes {

    // JSON — bazowy format naszego JSON-first API
    public static final String JSON = MediaType.APPLICATION_JSON_VALUE;

    private ApiMediaTypes() {
        // Zabraniamy tworzenia instancji: to klasa narzędziowa ze stałymi
    }
}

I wtedy w kontrolerze można pisać zwięźle i bez literówek:

import static com.example.tasktracker.api.ApiMediaTypes.JSON;

import org.springframework.web.bind.annotation.PostMapping;

@PostMapping(consumes = JSON)
public ResponseEntity<Task> create(@RequestBody TaskCreateRequest request) {
    // Reszta kodu metody (tworzenie zadania, formowanie odpowiedzi itp.)
}

To nie obowiązkowa technika, ale dobrze pokazuje ogólną myśl: format to część kontraktu, a kontrakt chcemy trzymać starannie i jednolicie, a nie przez kopiowanie i wklejanie.

6. Typowe błędy przy pracy z consumes i produces

Błąd nr 1: mylenie „kto za co odpowiada" między Content-Type, Accept, consumes, produces.
Bardzo częsta sytuacja: developer stawia produces, myśląc że to „my to przyjmujemy", albo stawia consumes, myśląc że to „my to dajemy". W efekcie endpoint nagle zaczyna zwracać 415/406, a osoba szuka problemu w serwisie. Prosta weryfikacja: consumes – to o tym, co serwer czyta (jak strawić request body), a produces – o tym, co serwer pisze (jak sformować response body).

Błąd nr 2: mechaniczne stawianie consumes na GET, gdzie nie ma ciała żądania.
Na poziomie sensu to po prostu szum: deklarujesz format czegoś, czego nie ma. A na poziomie zachowania możesz czasem dostać nieoczekiwane efekty, szczególnie jeśli jakiś klient niespodziewanie wysyła Content-Type tam, gdzie go nie oczekujesz. W projekcie edukacyjnym lepiej trzymać się prostej reguły: consumes pojawia się tam, gdzie jest @RequestBody.

Błąd nr 3: pisanie "application/json" ręcznie w dwudziestu miejscach i raz się mylisz.
Literówka w stylu "applicaton/json" wygląda śmiesznie dokładnie do momentu, gdy nie spędziłeś 40 minut na debugowaniu i nie zrozumiałeś, że endpoint „się nie znajduje", bo consumes nie pasował. Używaj MediaType.APPLICATION_JSON_VALUE lub własnej stałej, i usuniesz całą klasę błędów ze swojego życia.

Błąd nr 4: uważanie 415/406 za „błąd mojego kodu w metodzie kontrolera".
W rzeczywistości często oznacza to, że do metody w ogóle nie dotarłeś. Serwis może być idealny, repozytorium – genialne, a @RequestBody – piękny, ale jeśli żądanie przyszło z niewłaściwym Content-Type lub niezgodnym Accept, kołowrotek nie przepuści. Nawyk patrzenia na nagłówki żądania w takich przypadkach to dorosła, inżynierska oszczędność czasu.

Błąd nr 5: stawianie produces = application/json na endpoincie zwracającym 204 No Content i oczekiwanie „pustego JSONa".
204 oznacza „brak ciała", a nie „ciało jest puste". To subtelna, ale bardzo ważna różnica. Jeśli chcesz zwrócić {} lub jakiś obiekt potwierdzenia – to już będzie 200 OK z body, a nie 204. Nie udawaj, że 204 ma format ciała: formalnie ciała tam być nie powinno.

1
Zadanie
Spring REST & MVC, poziom 7, lekcja 3
Niedostępne
JSON-only endpoint z `consumes` i `produces`
JSON-only endpoint z `consumes` i `produces`
1
Zadanie
Spring REST & MVC, poziom 7, lekcja 3
Niedostępne
JSON-only `GET` i odpowiedź `406 Not Acceptable`
JSON-only `GET` i odpowiedź `406 Not Acceptable`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION