Jak to działa

Starter REST

Tu jesteś
Java - Mapa kariery Tu jesteś Wzorce projektowe Git 3 SQL
Relacyjne bazy danych niezmiennie od lat
są podstawą w budowie systemów
informatycznych. Na rynku istnieją
oczywiście również bazy typu NoSQL,
ale te zwykle są stosowane do
dedykowanych rozwiązań, jak na przykład
przechowywanie ogromnych ilości
danych w celu szybkiego przeszukiwania.
4 JPA - Hibernate 5 REST 6 Spring
REST, co oznacza Representational State Transfer, jest stylem architektonicznym wykorzystywanym w rozwoju usług internetowych. Został opracowany przez Roya Fieldinga w jego rozprawie doktorskiej w 2000 roku. REST wykorzystuje zestaw dobrze zdefiniowanych operacji (takich jak GET, POST, PUT, DELETE) oraz zasobów, które są reprezentowane przez URL-e. Głównymi cechami REST są:

REST, czyli Representational State Transfer

REST to styl, którego najważniejszym elementem w tworzeniu aplikacji webowych jest zasób (resource). Co może być zasobem? Tak naprawdę wszystko to, co w rzeczywistości definujemy jako obiekt. Może nim być, np. użytkownik (user), albo wydarzenie (event), albo dowolna inna reprezentacja podmiotu występującego w rzeczywistości. W naszej aplikacji także korzystamy z różnych zasobów, na przykład: element (item), kategoria elementu (item category), typ elementu (item type) oraz wiele innych.

Drugim ważnym aspektem formuły REST jest zdefiniowanie sposobu dostępu do wspomnianych zasobów. W tym celu korzystamy z bezstanowego protokołu HTTP, który dostarcza kilka istotnych metod umożliwiających wykonywanie zdalnych operacji na zasobach, m.in GET, POST, PUT, DELETE. Zarówno o tym, jak i o kilku dodatkowych sprawach pomówimy szerzej w kolejnym punkcie.

REST w aplikacji webowej

Załóżmy, że naszym głównym celem jest budowa aplikacji webowej. Zanim to zrobimy warto abyśmy rozpoczęli od zapoznania się z całym procesem wymiany danych z "lotu ptaka".

Krok 1  Strona internetowa z kodem HTML w przeglądarce

Proces działania aplikacji rozpoczyna się w przeglądarce. Tak więc, po wpisaniu przez użytkownika adresu strony przeglądarka wysyła żądanie na serwer w celu uruchomieniu procesu przetwarzania danych. URL na jaki wysyłane jest żądanie może być zdefiniowany na różne sposoby:
  • Bezparametrowo:   http://localhost:8080/items
  • Z parametrem typu query param:   http://localhost:8080/items?amount=10
  • Z parametrem type path variable:   http://localhost:8080/items/1

Wysyłanie żądania bardzo często jest wykonywane także już po załadowaniu strony, podczas obsługi zdarzenia użytkownika (np. po kliknięciu w przycisk submit formularza albo podczas kliknięcia w link). W ten sposób punktem wyjścia dla użytkownika jest fragment HTML danego przycisku, formularza czy też innego komponentu, z którego nastąpi wygenerowanie zdarzenia.


Krok 2  Wykorzystanie kodu JavaScript do wysłania żądania (requestu HTTP)

Po wykonaniu przez użytkownika akcji na danym komponencie następuje wykonanie kodu JavaScript, który zwykle wstępnie przygotowuje dane przesyłane z komponentu HTML oraz uruchamia mechanizmy, których zadaniem jest przygotowanie i wysłanie requesta na serwer.


Krok 3  Wykonanie obsługi żądania HTTP odbywa się za pomocą kilku powszechnie znanych metod: GET, HEAD, PUT, POST, DELETE, PATCH.

Appa Notka. Zanim przejdziemy do omawiania charakterystyki metod HTTP, zacznijmy od wyjaśnienia pojęcia, które jest mocno związane z tworzeniem systemów informatycznych, w tym aplikacji webowych. Pojęciem tym jest akronim CRUD. Definiuje on cztery podstawowe operacje, które możemy wykonać na danych:
  • Create - zapis danych
  • Read - odczyt danych
  • Update - modyfikacja danych
  • Delete - usunięcie danych
Tworząc aplikację prawie zawsze używamy pełnego zestawu tych operacji.
Metody HTTP są wręcz stworzone do tego, aby za ich pomocą wykonywać CRUD-owe operacje. Co zatem musimy zrobić gdy chcemy wysłać żądanie zgodne z efektem jakiego oczekujemy? Otóż musimy zbudować odpowiedni URL oraz wybrać pasujący rodzaj metody.

Appa Notka. URL najlepiej jest konstruować zgodnie z dobrymi praktykami. Mówią one, że w przypadku prostych operacji CRUD powinniśmy stosować liczbę mnogą dla zasobu, który chcemy zmodyfikować. Tak więc, jeśli chcemy dodać, zmienić, odczytać lub usunąć jakiś item w aplikacji, URL powinien wyglądać tak:

<<PROTOKÓŁ>>://<<HOST>>/items

Na przykład:

http://localhost:8080/items

Użycie liczby pojedynczej nie jest zalecane ze względu na jednolitość definicji dla zasobu. O ile podanie liczby pojedynczej (/item) pasuje dla odczytu, usunięcia lub modyfikacji pojedynczego zasobu, o tyle już pobranie wszystkich zasobów i tak musiałoby się skończyć użyciem liczby mnogiej (/items). Nie chcielibyśmy raczej podawać (/item) i spodziewać się pobrania wielu zasobów.

Reasumując, chcąc zachować spójną konwencję, warto używać liczby mnogiej dla wszystkich przypadków. Pasuje ona zarówno do obsługi wszystkich zasobów, jak i pojedynczego zasobu.
No dobrze. To teraz zobaczmy jakie metody HTTP mamy do dyspozycji i do jakich operacji CRUD one pasują:
  • GET - Pobiera zasób, a więc realizuje operację typu Read (CRUD):
    Pobranie wszystkich itemów: /items
    Pobranie wybranego itema: /items/5

    Powyższe dwa przykłady obrazują proste pobranie albo wszystkich albo pojedynczego itema (o id równemu 5). Natomiast poniżej mamy już sposób podejścia do tematu, gdy chcemy pobrać itema będącego w relacji. Mamy kategorie itemów i tutaj najpierw odwołujemy się do kategorii (id: 2), a następnie do wszystkich itemów z tej kategorii oraz do konkretnego itema (id: 5) z tej kategorii:

    Pobranie wszystkich itemów z konkretnej kategorii: /categories/2/items
    Pobranie wybranego itema z danej kategorii: /categories/2/items/5

    To jest dobry sposób na modelowanie URL-i w przypadku relacji.
  • HEAD - Pobiera metadane o zasobie, a więc w pewien sposób realizuje operację typu Read (CRUD):
    Konstrukcja URL-i (poniżej) wygląda podobnie, jak w przypadku samej metody GET.

    Pobranie nagłówków dla wszystkich itemów: /items
    Pobranie nagłówków dla wybranego itema: /items/5
    Pobranie nagłówków dla wszystkich itemów z konkretnej kategorii: /categories/2/items
    Pobranie nagłówków dla wybranego itema z danej kategorii: /categories/2/items/5

    Skąd takie podejście? Metoda HEAD nie służy do pobierania zasobu jako takiego, ale jest wykorzystywana do pobrania nagłówków, które zostaną przesłane, jeśli później wykonamy metodę GET na tym samym URL-u. Czyli metoda ta działa na zasadzie:
    "Przetestuj mi zasób, a ja zdecyduję, czy wysłać po niego żądanie metodą GET"
    W ten sposób możemy dowiedzieć się na przykład, jaki jest rozmiar zasobu, który chcemy pobrać (nagłówek Content-Length). To może być dla nas ważne, jeśli z jakiegoś powodu nie chcemy pobierać danych większych niż zakładany przez nas rozmiar.

    Metoda HEAD posiada specyficzną własność. W trakcie jej użycia niedozwolone jest przekazywanie body żądania.
  • PUT - Przesyła dane w celu modyfikacji zasobu, a więc realizuje operację typu Update (CRUD):
    Modyfikacja wybranego itema: /items/5

    Body: {"id": 5, "name": "New name", "comment": "Twój komentarz", ...} <--- wszystkie pola

    Modyfikacja wybranego itema poprzez modyfikację jego *zasobu podrzędnego: /items/5/name

    Body: "New name"

    Modyfikacja wybranego itema z danej kategorii: /categories/2/items/5

    Body: {"id": 5, "name": "New name", "comment": "Twój komentarz", ...} <--- wszystkie pola

    Modyfikacja wybranego itema z danej kategorii poprzez modyfikację jego zasobu podrzędnego: /categories/2/items/5/name

    Body: "New name"

    * Pole itema name traktujemy jako zasób. Przy takim podejściu każde kolejne pole musi mieć również swojego dedykowanego URL-a, co delikanie mówiąc nie jest najlepszym pomysłem. Takie rozwiązanie stosujemy tylko w uzasadnionych przypadkach (o tym nieco dalej).

    Metoda PUT posiada pewną własność, co razem z metodą GET i HEAD czyni je wyjątkowymi. Metody te są idempotentne. Oznacza to, że nawet kilkukrotne wykonanie tej samej metody dla identycznych danych przekazywanych w żądaniu doprowadzi do osiągnięcia tego samego rezultatu, np. pobranie zasobu metodą GET zawsze zwróci dokładnie taki sam zasób (o ile się nie zmienił po stronie źródła danych), a kilkukrotne wykonanie metody PUT zawsze pozostawi po sobie zasób z takimi samymi danymi jak po pierwszym wykonaniu.

  • PATCH - Przesyła dane w celu modyfikacji części zasobu, a więc także realizuje operację typu Update (CRUD):
    Modyfikacja wybranego itema: /items/5

    Body: {"name": "Some name"} <--- wybrane pola

    Modyfikacja wybranego itema z danej kategorii: /categories/2/items/5

    Body: {"name": "Some name"} <--- wybrane pola

    Metoda PATCH jest mało popularna, ponieważ ludzie z natury bywają leniwi i nie chcą sobie zbytnio komplikować życia. Skoro PUT służy do update'u, to czy to ma znaczenie, że aktualizujemy całość obiektu, czy też tylko fragment (na przykład pole ze statusem)? W teorii nie ma to wielkiego znaczenia, ale warto trzymać porządek i jednak podkreślić rodzajem metody, że jest ona przeznaczona tylko do częściowego update'u.

  • POST - Przesyła dane w celu stworzenia zasobu, a więc realizuje operację typu Create (CRUD):
    Stworzenie itema: /items

    Body: {"name": "New name", "comment": "Twój komentarz", ...} <--- wszystkie pola

    Stworzenie itema w ramach wybranej kategorii: /categories/2/items

    Body: {"name": "New name", "comment": "Twój komentarz", ...} <--- wszystkie pola

    Metoda jest co prawda używana do tworzenia zasobu, ale w specyficznych przypadkach nic nie stoi na przeszkodzie, aby służyła również do jego modyfikacji. Pamiętajmy jednak, że z założenia metoda ta nie zapewnia nam idempotentności. Stąd jeśli potrzebujemy wykonać modyfikację danych, która po każdym wykonaniu tego samego żądania ma zmienić stan zasobu, to wówczas POST nada się do tego doskonale.

  • DELETE - Umożliwia usunięcie zasobu, a więc realizuje operację typu Delete (CRUD):
    Usunięcie wybranego itema: /items/5
    Usunięcie wybranego itema z danej kategorii: /categories/2/items/5

    Alternatywnie, kiedy chcemy odwołać się do pola jako do zasobu (zasobu podrzędnego):

    Usunięcie zasobu podrzędnego (name) z wybranego itema: /items/5/name
    Usunięcie zasobu podrzędnego (name) wybranego itema z danej kategorii: /categories/2/items/5/name

    Wtedy w kodzie będziemy po prostu czyścić wartość w danym polu.

    Metoda DELETE jest idempotentna.

  • OPTIONS - Umożliwia wyświetlenie informacji o dozwolonych opcjach komunikacji dla podanego adresu URL lub serwera
    Metoda ta jest specyficzna, ponieważ w ogóle nie odnosi sie do zasobu. Podajemy ją tutaj dla porządku, ponieważ też jest metodą HTTP i jest przydatna w kontekście komunikacji opartej o REST. Niektóre przeglądarki wysyłają żądanie typu OPTIONS przed właściwym żądaniem, gdyż w ten sposób mogą sprawdzić czy dany serwer jest odpowiednio skonfigurowany. Jest to szczególnie ważne w kontekście zabezpieczeń. W ten sposób przeglądarka może się dowiedzieć czy dany serwer akceptuje na przykład CORS, czyli Cross-origin resourse sharing). Metoda OPTIONS jest idempotentna.

    Przykład odpowiedzi:
    HTTP/1.1 204 No Content
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: https://foo.example
    Access-Control-Allow-Methods: POST, GET, OPTIONS
    Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
    Access-Control-Max-Age: 86400
    Vary: Accept-Encoding, Origin
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive              
    

Tak to wygląda, gdy realizujemy typowe operacje CRUD-owe. Sprawa nieco się komplikuje, gdy nasze żądanie ma zrealizować coś mniej szablonowego. Na przykład, jeśli chcemy uruchomić żądaniem proces usuwania pliku na serwerze, wówczas możemy przekształcić uruchamianie akcji usuwania w nazwę pola zasobu. Zamieniamy nazwę "starting" ("uruchamianie") na udawane pole boolean started w wyimaginowanym zasobie files i updateujemy je za pomocą metody PATCH:

PATCH /files/7

Body: {"started": true}


Możemy też taką akcję potraktować jako modyfikację zasobu z zasobem podrzędnym, czyli w naszym przykładzie uruchomienie potraktujemy jako zasób started, który tutaj jest zasobem podrzędnym files. Wtedy uruchomienie będzie realizowane metodą PUT (skoro started jest zasobem, to podmieniamy całość zasobu - nieważne że to de facto prosta wartość):

PUT /files/7/started

Body: true


Klasycznym problemem jest również implementacja operacji wyszukiwania zwracającego wynik w postaci wielu różnych zasobów. W takich i podobnych sytuacjach dopuszcza się użycie prostego zapisu nieukierunkowanego na zasób:

/search

Warto to jednak opatrzyć (w kodzie lub dokumentacji) stosownym komentarzem.
Krok 4  Mapowanie zawartości żądania do obiektu Java

Tak pokazaliśmy w poprzednim punkcie, przesyłanie danych czasem wykonujemy w ramach URL, a czasem w body żądania. W tym drugim przypadku dane są przesyłane w formacie zgodnym z określonym typem zawartości (content-type). Lista dostępnych typów zawartości jest długa, ale najczęściej używane są:

  • Format JSON:   application/json
  • Format obiektu w wielu częściach, np. plik użytkownika:   multipart

W przypadku aplikacji webowych, na samym końcu po otrzymaniu żądania przez serwer, następuje mapowanie pól z requestu na pola w obiekcie. W Springu wydarzy się to automatycznie dzięki mechanizmom frameworka (piszemy o tym w Kursie Springa w rozdziale Spring MVC - Metody obslugi zadan HTTP (Handler Methods), a dokładniej - w punkcie opisującym adnotację @RequestBody).


Krok 5  Przyjęcie danych przez serwer i rozpoczęcie przetwarzania

Po wykonaniu mapowania danych z żądania serwer rozpoczyna uruchamianie mechanizmów do obsługi tych danych, w szczególności ich przetworzenia (używamy do tego osobnej warstwy kodu Krok 6 ), co zwykle kończy się wykonaniem operacji typu CRUD.

Krok 7  Odczyt lub zapis w źródle danych

Powyższe operacje są wykonywane na dowolnym źródle danych. Najczęściej jest to baza danych ale może to być też zwykły plik, bądź usługa danych taka jak webservice (typu REST lub SOAP). Pamiętajmy, że przetwarzanie danych może być także związane tylko pośrednio z operacjami CRUD, np. odczyt danych może nastąpić na początku procesu, a dalej odbędzie się ich przetworzenie za pomocą dowolnego algorytmu i dopiero tak przygotowane dane będą gotowe do wykorzystania. Kombinacje są tutaj dowolne i zależą od wymagań biznesowych.


Krok 8  Zakończenie przetwarzania danych

Wynikiem przetworzenia danych może być tylko finalizacja wykonania konkretnych algorytmów albo też, dodatkowo, zwrócenie danych przez warstwę przetwarzającą do warstwy wyżej, w celu dalszego ich wykorzystania (najczęściej wysłania ich w ramach odpowiedzi).


Krok 9  Wysłanie odpowiedzi do przeglądarki

Po przetworzeniu danych proces wraca ze wspomnianą odpowiedzią (HTTP response) z serwera do przeglądarki. Pamiętajmy, że w zależności od typu operacji (algorytmów przetwarzających) wykonanej na serwerze odpowiedź może zawierać dane lub nie. Przykładowo, gdy wykonujemy metodę GET w celu pobrania danych, wówczas w odpowiedzi dostaniemy właśnie te dane. W przypadku gdy tworzymy nowy zasób, wykonując metodę POST, otrzymamy jego id, a gdy wykonamy metodę DELETE, odpowiedź będzie pusta. Jeśli dane są przekazywane, to zostaną one przepisane z obiektu do odpowiedzi HTTP. Krok 10

Każda odpowiedź HTTP zawiera status odpowiedzi. Jest to uniwersalny kod, który zawsze oznacza coś konkretnego. Kodów jest wiele, ale można wśród nich wydzielić kilka podstawowych grup:
  • 1xx - Kody informacyjne
  • 2xx - Kody powodzenia
  • 3xx - Kody przekierowania
  • 4xx - Kody błędów klienta
  • 5xx - Kody błędów serwera
Najpopularniejsze i najczęściej używane kody to:
  • 200 (OK) - operacja na zasobie przebiegła pomyślnie
  • 201 (Created) - utworzono nowy zasób (np. po wywołaniu metody POST)
  • 204 (No content) - serwer wykonał operacje pomyślnie, ale nie zwraca danych
  • 304 (Not modified) - zasób nie podlegał zmianie według warunku określonego w żądaniu
  • 400 (Bad Request) - zapytanie o zasób zostało błędnie skonfigurowane (np. błędnie podano parametru w URL)
  • 401 (Unauthorized) - nieautoryzowany dostęp do zasobu (wymaga uwierzytelnienia)
  • 404 (Not Found) - zasób nie został odnaleziony
  • 405 (Method Not Allowed) - podana metoda (GET, POST, etc.) nie jest dozwolna dla danego zasobu
  • 500 (Internal Server Error) - wewnętrzny błąd serwera, niespodziewane zachwianie poprawnego wykonania procesu

Podobnie jak w przypadku żądania, tak samo w przypadku odpowiedzi ustawiamy typ zawartości. Najczęściej spotykanymi typami są:
  • Format JSON:   application/json
  • Format danych graficznych:   image/png , image/jpeg
  • Format HTML:   text/html
  • Format tekstu:   text/plain
  • Format arkusza stylów:   text/css

Krok 11  Odebranie odpowiedzi w przeglądarce

Kolejnym etapem po przesłaniu odpowiedzi jest jej odebranie w przeglądarce za pomocą kodu JavaScript (w dedykowanej warstwie), a następnie udostępnienie tej odpowiedzi w celu przygotowania widoku.


Krok 12  Formatowanie i prezentacja

Na samym końcu, po odebraniu danych, są one odpowiednio formatowanie i pokazywane użytkownikowi w przeglądarce przy pomocy zdefiniowanego layoutu.

REST w aplikacji webowej na bazie Springa

Kontroler Springa wystawia adresy URL związane z konkretnymi metodami Java. Mówimy, że wystawia on REST API. W aplikacji webowej mamy wiele takich kontrolerów. Każdy z nich jest dedykowany konkretnemu zagadnieniu aplikacji. I tak na przykład zarządzanie użytkownikami jest zwykle realizowane przez UserController albo UserApi. Wszystko zależy od przyjętej konwencji nazewniczej.

Przyjęło się, że w ramach REST API url-e zaczynają się od /api. Dalej można albo podać od razu nazwę podmiotu: /users, albo też po drodze użyć jeszcze numeru wersji (jeśli przewidujemy kilka różnych wersji, zaimplementowanych na różne sposoby): /v1. Całość będzie zatem może przyjmować takie formy:

<<PROTOKÓŁ>>://<<HOST>>/api/users
<<PROTOKÓŁ>>://<<HOST>>/api/v1/users

Appa Notka. Właśnie tak tworzymy REST API w ramach Kursu Aplikacji Webowej. W kursie każdy scenariusz (na przykład rejestracja użytkownika, czy też dodawanie itemów) jest realizowany w oparciu o dedykowane kontrolery restowe takie jak UserController, ItemController, CategoryController itp. Zobacz Starter projektu aplikacji, aby dowiedzieć się więcej.

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE