poniedziałek, 19 stycznia 2009

REST In Peace

Pierwsza prezentacja w ramach University to:
REST (in peace) with Java.

Pomimo tego, że już uprzednio spotkałem się z terminem "REST", kilkanaście razy pojawił się przy okazji innych tematów oraz wysłuchałem 2 prezentacji na ten temat, stwierdziłem, że zawsze warto posłuchać.

Cała prezentacja została podzielona na 2 części: opis czym jest REST oraz opis stworzonego przez Marca Portiera i Steven Noelsa frameworku Kauri. Opis teoretyczny, pomimo że prowadzony w niezbyt typowej formie: uczeń dopytuje nauczyciela czym jest REST, bardzo mi się spodobał, a szczególnie przypadło mi do gustu trafne porównanie REST do stylu architektonicznego(np. gotyk) a nie do konkretnej architektury. Zaczęło się od paru słów na temat definiowania czym jest REST i czemu termin ten jest tak mało zrozumiały:

  • REST jako twór opisany przez Fieldinga jest zbyt mało konkretny i pragmatyczny, nie zawiera praktyk, na których można by się wzorować
  • większość dokumentów opisujących REST mówi tylko o tym czego nie powinno się robić, czego lepiej unikać - ale z 2 strony nie opisują co trzeba zrobić aby stworzyć poprawny system oparty o REST
  • REST jako abstrakcja, której nie można kupić
Przy okazji polecono po raz kolejny pozycję:
RESTful Web Services, którą zachwalają chyba wszyscy specjaliści od REST.

Jeśli chodzi o Web Services to sprawa wygląda z teoretycznego punktu widzenia bardzo prosto:

  1. Należy określić co chcemy z danym "bytem" zrobić. Intencja ta może zostać wyrażona za pomocą: metody HTTP (GET, POST, PUT, DELETE) , URI np. GET /path?method=removeThat lub treści przesyłanego komunikatu np. informacja zawarta w komunikacie SOAP
  2. Należy określić na jakim "bycie" wykonujemy daną operację. W tym przypadku informacja ta jest określona albo w URL lub w treści komunikatu, np. w komunikacie SOAP

Jest to analogiczne do świata programowania obiektowego - musimy wiedzieć na jakim obiekcie wywołać jaką metodę.
W ten sposób dochodzimy do 2 a praktycznie do 3 typów Web Services:

  • RPC - w których to sam komunikat zawiera informację na jakim bycie oraz jaką operację wykonujemy - typowe podejście SOAP z pojedynczym URI jako endpointem "rozrzucającym" do dalszych warstw a wszystkie komunikaty przychodzą metodą POST
  • RESTful/Resource Oriented - operacja do wykonania jest wyrażona za pomocą metody HTTP a "byt", na którym wykonujemy działanie jest określony w URI
  • Hybryda - różne połączenia cech typów przedstawionych powyżej

Następnie przedstawione zostały 4 główne koncepcje, na których opiera się REST:

  • RESOURCE - czyli czym jest zasób: byt, który możemy wyróżnić w systemie, nazwać go, na którym możemy wykonać operację i dla którego istnieje reprezentacja) oraz rodzaje zasobów
  • URI - jako adres oraz jednocześnie nazwa zasobu, URI powinny być: naturalne, przewidywalne, unikalne, zgodne z przyjętą strukturą (hackable!). Dany zasób może mieć więcej niż 1 URI, ale trzeba pamiętać o canonical variant
  • REPRESENTATIONS - czyli reprezentacja zasobu: dane/bajty, które możemy zobaczyć jak spytamy się o dany zasób. Reprezentacja może też być związana z danymi przesyłanymi od klienta do serwera, np. przy operacji PUT lub POST. Reprezentacje pojedynczego zasobu mogą się różnić ze względu na format i język, a negocjacja odbywa się przy pomocy nagłówków przesyłanych przez klienta: Accept oraz Accept-language. W odpowiedzi serwer może określić czym różnią się poszczególne reprezentacje za pomocą nagłówka: Vary. Ma to bardzo duże znaczenia dla cachowania odpowiedzi, a odpowiedź z nagłówkiem: Vary: * nie jest cachowana. Z praktycznego puktu widzenia negocjacja zawartości (Content Negotiation) może oprócz przekazywania nagłówków: Accept oraz Accept-language opierac się na przekazaniu pewnych wartości w parametrze żądania HTTP, np. ?hl=nl (@google) lub bezpośrednio w URI, np. /path/resource-name.nl.html. Prowadzi to do relacji 1URI na 1 reprezentacje co nie jest stricte zgodne z REST, ale ma być bardzo praktyczne.
  • LINKS - czyli prezentacja URI w reprezenatacji, linki w swojej naturze są miękkimi referencjami (soft-references)

oraz 4 właściwości na których opiera sie REST Oriented Architecture:

  • ADDRESSABILITY - czyli użycie URI do dostępu do dowolnych zasobów. Jako przykład braku tej właściwości został przedstawiony "tradycyjny" sposób uzyskania dostępu do określonego elementu na liście wyszukiwania: wejdź na stronę domową, uzupełnij kryteria wyszukiwania w taki i taki sposób, element szukany to 3 od góry na 2 stronie. Do zalet związanych z ADDRESSABILITY zaliczamy: łatwość dystrybucji adresów zasobów - URI mogą być publikowane wszędzie (email, strona HTML, a nawet przekaz ustny); "link love"; wykorzystanie URI w usługach związanych z bookmarking i tagging; zasoby (lub w szczególności ich reprezentacje) mogą być cachowalne; mogą być składowymi systemów typu Mash-up, byc "obrabiane" przez usługi tłumaczeniowe i walidacyjne (Translation & Validation Services). Dodatkowo pojawiły się takie hasła jak : Chaining, Aggregation, Scraping, które nie do końca rozumiem w tym kontekście.
  • STATELESSNESS - bezstanowość, która mówi, że każde żądanie musi zawierać w sobie wystarczającą ilość danych aby mogło byc w pełni obsłużone bez korzystania z kontekstu przechowywanego na serwerze(HTTP request isolation). Podejście takie ma wiele zalet, a oprócz tych wymienionych przy Addressability dochodzi jeszcze większa skalowalność oraz ułatwiony load-balancing. W jaki sposób radzić sobie ze stanem w aplikacjach opartych o REST jest przedstawione poniżej.
  • CONNECTEDNESS - cecha związana z koncepcją LINKS, która określa, że reprezentacja może zawierać link/linki (soft-references). Linki te doprowadzą nas do zasobów, których reprezentacje mogą zawierać linki do kolejnych zasobów itd. Idea jest prosta: publikujemy pojedynczy URL a klient nawiguję do kolejnych linków zawartych w reprezentacjach poszczególnych zasobów i w ten sposób "uczy"się naszej aplikacji.
  • UNIFORM INTERFACE - określa sposób interakcji klienta z serwerem. Objemuję nie tylko metody HTTP żądania, ale także nagłówki żądania/odpowiedzi oraz kody tych ostatnich. Jeśli chodzi o odpowiedzi serwera powinno się wykorzystywać nagłówki oraz kody odpowiedzi zdefiniowane w HTTP, ale jednocześnie unikać własnych nagłówków X-Headers i Cookies aby wszystko było w myśl zasady: "Principle of least surprise"

Poszczególne metody HTTP wraz z ich semantyką to:

  • GET - pobranie reprezentacji zasobu. GET powinno być operacją bezpieczną oraz idempotentą, czyli jej wykonanie nie powinno zmieniac stanu na serwerze i do tego niezaleznie od tego ile razy wykonano tą metodę. Dopuszcza się pewne "delikatne skutki uboczne" wynikające z wykonania metody: podbicie liczników, odłożenie się logów. Bardzo podobała mi się anegdota o narzędziu (nie pamiętam nazwy), które w tle pobierało strony, do których istniały linki na aktualnie oglądanej stronce. Sama idea wygląda pięknie: w momencie kiedy czytam zawartość danej strony, w tle automatycznie ściągają się stronki, do których aktualnie czytana strona posiada linki. Problem polegał na tym, że bezwiedne podążanie za zawartymi na stronie odnośnikami potrafi nieświadomie zmienić stan na serwerze. Wyobraźmy sobie co się stanie jesli na czytanej przeze mnie stronie znajduję się link a jego wywołanie: GET /entry?id=12&method=delete usuwa zasób z serwera ...
  • HEAD - to samo co GET, ale bez contentu
  • OPTIONS - określa jakie metody są dostępne dla danego zasobu, powinna być bezpieczna i idempotenta
  • DELETE - usuwa zasób, idempotentna. Pomimo, że odpowiedz serwera na ponowne wykonanie operacji DELETE na uprzednio usuniętym zasobie może róznić się od odpowiedzi na pierwsze(faktyczne) usunięcie, z punktu widzenia serwera stan się nie zmienił.
  • PUT - uaktualnia, choć bardziej poprawne/dokładne jest stwierdzenie, że PUT nadpisuje stan istniejącego zasobu ignorując jego stan aktualny. PUT może być używany do tworzenia nowych zasobów w sytuacji kiedy to klient decyduję pod jakim URI dany zasób będzie "rezydował". Metoda idempotentna.
  • POST - pomimo tego, że wiele dokumentów opisuję POST jako metodę służącą stricte do tworzenia zasobów nie jest to do końca prawda, ponieważ w tym przypadku "Sky is the Limit". Metoda nie jest (nie musi być) ani bezpieczna ani idempotentna

Następnie przedstawione zostały podstawowe kroki przy analizie oraz projektowaniu systemów opartych na REST. Obawiałem się, że zostanie przedstawiony przykład w stylu biblioteki, ale Marc Portier oraz Steven Noels pokazali kilka nieznanych mi chwytów:

  • Tworzenie zasobów może zostać zrealizowane za pomocą PUT w przypadku gdy klient decyduję pod jakim URI ma dany zasób istnieć - serwer odpowiada kodem 200, lub za pomocą POST - serwer odpowiada kodem 201 oraz nagłówkiem location header, którego wartością jest adres nowo utworzonego zasobu
  • POST moze być wykorzystywany do updatowania stanu zasobu. Takie podejście pozwala uniknąć przepisywania aktualnego stanu zasobu oraz umożliwia inkrementalne uaktualnianie stanu zasobu (PUT nadpisuję stan)
  • W pewnych przypadkach POST może zostać użyte do wywołania metod o innej semantyce, np. POST uri?method=put/delete , ale podejście z 1 endpointem obsługującym wszystkie żądania jest w świecie REST niepoprawne

Jedną z właściwości REST jest bezstanowość, która to często jest "nie do przełknięcia" dla wielu osób. Znaczna ilość jeśli nie większość aktualnie działających systemów korzysta z mechanizmu stanowości, przy czym stan ten zazwyczaj jest przechowywany po stronie serwera.
Statelessness wcale nie oznacza, że stanowości nie ma, a jedynie to, że klient jest odpowiedzialny za utrzymywanie stanu a nie serwer. Dodatkowo wyróżniono 2 typy stanów "Conversational State" jako ten "bad" oraz "Application state" jako ten "good", ale jak na razie nie potrafię dobrze złapac różnicy czym jeden różni się od drugiego. W końcu pokazano w jaki sposób pragmatycznie radzić sobie z przechowywaniem stanu po stronie serwera aby pozostać w świecie REST. Tym rozwiązaniem są tymczasowe zasoby (Temporary Resources). Scenariusz wygląda zazwyczaj w taki sposób:

  • Klient wysyła żądanie utworzenia tymczasowego zasobu (z jego punktu widzenia nie jest to zasób tymczasowy), np.

    POST /temp/upload HTTP/1.1

    Serwer odpowiada adresem pod którym znajduję się nowo utworzony zasób, np.

    HTTP 1.1 201 Created
    Location: /temp/upload/666
  • Teraz juz możemy ładować do utworzonego przed chwilą kontenera kolejne zasoby, np.
    PUT /temp/upload/666/cv-jb.pdf HTTP/1.1
    Host: example.com

    <DATA>

    i 2 wywołanie:

    PUT /temp/upload/666/cl-jb.pdf HTTP/1.1
    Host: example.com

    <DATA>
  • Jednak to klient musi trzymać "referencje" do swoich zasobów:

    PUT /application/20090120/jb HTTP/1.1
    Host: example.com

    {name:”James Bond”,
    mugshot: “/temp/upload/666/cl-jb.pdf”,
    resume: “/temp/upload/666/cv-jb.pdf” }

W bardzo podobny sposób można zrealizować (zasymulować) atomowość pomiędzy wywołaniami. W swoich założeniach HTTP nijak ma się do XA. Rozwiązaniem jest potraktowanie transakcji jako zasobu, a całe przetwarzanie może wyglądać w następujący sposób:

  • Najpierw tworzymy sobie transakcje jako zasób:

    POST /tx/transfer HTTP/1.1
    Host: example.com

    Serwer odpowiada adresem pod którym znajduję się nowo utworzony zasób/transakcja:

    201 Created
    Location: /tx/transfer/933

  • Następnie ładujemy poszczególne operację:

    PUT /tx/transfer/933/account/912-883-32 HTTP/1.1
    Host: example.com

    balance=300

    oraz

    PUT /tx/transfer/933/account/555-283-12 HTTP/1.1
    Host: example.com

    balance=300
  • W końcu finalizujemy transakcję:

    PUT /tx/transfer/933 HTTP/1.1
    Host: example.com

    committed=true

Wykorzystanie mechanizmu zasobów tymczasowych wiąże się z też z pewnymi konsekwencjami: musimy utworzyć kontener (zasób tymczasowy), przechowywać w jakiś sposób jego składowe (na dysku/w pamięci/w BD ?) oraz zaimplementować mechanizm usuwania kontenerów i ich zawartości. Jak to zazwyczaj bywa: No free launch.

Ostatnią sprawą jaką poruszono jest kolejkowanie (Queueing) żądań.
Tradycyjne cachowanie sprawdza się jeśli żądanie jest typu read. W innym przypadku można posłużyć się "Write Accept" Proxy, które na dane żądanie odpowiada jedynie kodem HTTP 202 Accepted. Żądanie klienta nie jest obsługiwane w "real-time", ale jest kolejkowane do dalszego przetwarzania. Dodatkowo może zostać zwrócony nagłówek Location, zawierający adres do zasobu w celu późniejszego sprawdzenia statusu przetwarzania żądania. Podejście takie sprawdza się szczególnie w przypadku systemów "głosuj teraz" ("vote now") i pozwala sobie poradzić z krótkotrwałą falą żądań.

Później został przedstawiony framework Kauri, a prezentacja nabrała bardziej marketingowego charakteru.

Brak komentarzy:

Prześlij komentarz