To jest jeden z artykułów w ramach darmowego kursu programowania aplikacji webowych w Javie. Proszę zapoznaj się z pozostałymi częściami, mogą one być pomocne w zrozumieniu materiału z tego artykułu.

W kursie tym zakładam, że znasz język Java. Możesz go poznać w osobnym kursie programowania w języku Java. Do budowania wszystkich projetków używam Gradle, przeczytasz o nim więcej w osobnym artykule.

Słownik pojęć

Na początku postaram się wytłumaczyć pojęcia, które będą używane w dalszej części artykułu.

Czym jest web service

Wyobraź sobie obiekt. Obiekt ma zestaw metod. W normalnych warunkach metody na tym obiekcie możemy wywoływać wyłącznie w tym samym programie, w ramach tej samej wirtualnej maszyny Java. W uproszczeniu web service1 to mechanizm umożliwiający wywołanie jakiejś funkcjonalności za pośrednictwem internetu. Istnieje kilka podejść do tworzenia web service’ów.

Jednym z nich jest tak zwany REST.

Czym jest REST

REST to rozwinięcie Representational State Transfer. REST to zbiór praktyk, które określają w jaki sposób powinniśmy implementować web service’y. REST kręci się wokół tak zwanych encji (ang. resource). Encja to obiekt, który reprezentuje byt w aplikacji. Może to być na przykład obiekt reprezentujący rezerwację stolika, użytkownika aplikacji czy kredyt w banku.

Operacje na encjach wykonuje się za pomocą zapytań HTTP. Do encji dobieramy się używając odpowiednich typów zapytań. Typy zapytań określone są przez czasowniki HTTP: GET, POST, PUT i DELETE. Czasowniki te wraz z adresem URL definiują dokładnie jaką operację chcemy wykonać na danej encji.

Czasownik Przykład Znaczenie
GET GET /rezerwacja/123 Pobranie rezerwacji o identyfikatorze 123
PUT PUT /rezerwacja/123 Edycja rezerwacji o identyfikatorze 123
POST POST /rezerwacja Utworzenie nowej rezerwacji
DELETE DELETE /rezerwacja/123 Usunięcie rezerwacji o identyfikatorze 123

Zanim powstał REST, web service’y w języku Java tworzono w oparciu o SOAP (ang. Simple Object Access Protocol). SOAP w porównaniu do REST jest dużo bardziej złożony. SOAP oparty jest o XML’a i jest dość rozwlekłym protokołem. Moim zdaniem REST zdobył dużą przewagę właśnie swoją prostotą w porównaniu do SOAP. REST jest de facto standardem jeśli chodzi o tworzenie web service’ów w większości aplikacji webowych.

Wysłane zapytanie informuje także web service o preferowanej formie odpowiedzi. Dzieje się to zazwyczaj przy pomocy nagłówków HTTP. Na przykład nagłówek zapytania Accept: application/json informuje web service, że klient oczekuje odpowiedzi w formacie JSON.

Jest to tylko krótkie wprowadzenie, jeśli chcesz dowiedzieć się więcej na temat REST odsyłam Cię do materiałów dodatkowych.

PUT czy POST?

W tabeli wyżej wspomniałem o tym, że to zapytania typu POST powinny tworzyć nową instancję a zapytania typu PUT powinny ją edytować. Dla pełni informacji muszę Ci powiedzieć, że z tego co wiem, nie jest to nigdzie ustandaryzowane.

Spotkasz się zarówno z takim podejściem jak w tabeli wyżej jak i odwrotnym, w którym to zapytania typu POST służą do edycji encji.

Czym jest Java EE

W jednym zdaniu. Java EE to platforma, która oparta jest na zbiorze specyfikacji. Technologie opisane w tych specyfikacjach są używane głównie do tworzenia aplikacji webowych.

Logo Java EE

Teraz należy Ci się rozwinięcie. Język Java już znasz. Java wraz z zestawem biblioteki standardowej to Java SE (ang. Standard Edition). Istnieje również taki twór jak Java EE (ang. Enterprise Edition). Jak napisałem wyżej Java EE to nic innego jak zbiór różnych specyfikacji. Można powiedzieć, że Java EE rozbudowuje możliwości Java SE. Oczywiście podstawą tutaj jest język programowania Java. Java EE w wersji 8 została opublikowana 31 sierpnia 2017 roku. Java EE w wersji 8 to 41 osobnych specyfikacji! Jedną ze specyfikacji jest Java Servlets. Opisywałem ją w poprzednich artykułach w ramach kursu.

Java Servlets jest specyfikacją, którą można używać w kontenerach serwletów. Niestety nie wszystkie ze wspomnianych 41 specyfikacji można używać w kontenerze serwletów. Java API for RESTful Web Services (w skrócie JAX-RS) jest tu dobrym przykładem. Aby móc używać tej technologii potrzebujemy kontenera aplikacji.

Kontener aplikacji a kontener serwletów

Apache Tomcat, którego używałem do tej pory w ramach kursu jest kontenerem serwletów. Umożliwia on uruchamianie aplikacji webowych, które używają podzbioru specyfikacji Java EE (na przykład specyfikacji serwletów).

Pisanie web service’ów w oparciu o JAX-RS wymaga kontenera aplikacji. Jednym z kontenerów aplikacji jest Apache TomEE.

Instalacja kontenera aplikacji

Jest wiele kontenerów aplikacji. W tym kursie będę używał Apache TomEE. Jest to jeden z darmowych kontenerów. Aby go zainstalować pobierz TomEE PluME i rozpakuj plik ZIP do dowolnego folderu. To tyle, instalację kontenera aplikacji masz już za sobą.

Ważne jest, żebyś pobrał wersję TomEE PluME, lub TomEE+. Te wersje wspierają specyfikację JAX-RS.

Pobieranie TomEE Plume

Web service z JAX-RS

build.gradle

Nadszedł czas na to, żeby utworzyć swój pierwszy web service. Do budowania aplikacji użyłem Gradle. Plik build.gradle wygląda następująco:

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'maven'
apply plugin: 'war'
apply from: 'https://raw.github.com/akhikhl/gretty/master/pluginScripts/gretty.plugin'

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
group = 'pl.samouczekprogramisty.kursaplikacjewebowe'
version = '1.0-SNAPSHOT'

dependencies {
    providedCompile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
    providedCompile group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1'

    testCompile group: 'junit', name: 'junit', version: '4.12'
    testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
    testCompile group: 'org.mockito', name: 'mockito-all', version: '1.10.19'
}

task explodedWar(type: Copy) {
    into "$buildDir/explodedWar"
    with war
}

war.dependsOn explodedWar

Najbardziej istotnym fragmentem jest providedCompile group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1'2. Jest to zależność, która zawiera klasy określone przez specyfikację JAX-RS. providedCompile mówi Gradle o tym, że zależność jest wymagana przez aplikację wyłącznie w trakcie kompilacji. Nie zostanie umieszczona w wynikowymi pliku war. Nie jest ona tam potrzebna ponieważ jest dostępna na classpath kontenera aplikacji.

Pierwszy web service

Utwórz klasę, która będzie odpowiedzialna za encję Reservation. Może to być na przykład ReservationWebservice:

package pl.samouczekprogramisty.kursaplikacjewebowe.rest;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

@Path("/reservation")
public class ReservationWebservice {
    @GET
    public Response listReservations() {
        return Response.ok("Oto wszystkie rezerwacje :)").build();
    }
}

Jest to standardowa klasa Javy, tak zwane POJO (ang. Plain Old Java Object). Zawiera ona jedną metodą, listReservations. Dodatkowo znajdują się w niej dwie adnotacje: @Path i @GET. Metoda ta zwraca instancję obiektu Response.

Ta kombinacja to Twój pierwszy web service. Adnotacja @Path informuje JAX-RS o tym pod jakim URL dana klasa/metoda powinna odpowiadać. Adnotacja @GET mówi o tym jaki czasownik HTTP jest obsługiwany przez daną metodę.

Następnie zbuduj aplikację używając polecenia:

gradle war

Po jego wykonaniu w katalogu build/libs projektu powinien znaleźć się plik war. Przekopiuj go do katalogu webapps w miejscu gdzie zainstalowałeś TomEE. W moim przypadku wygląda to następująco:

cp /home/mapi/KursAplikacjeWebowe/06_rest_endpoint/build/libs/06_rest_endpoint-1.0-SNAPSHOT.war /home/mapi/opt/apache-tomee/webapps/rest.war

Zauważ, że w trakcie kopiowania zmieniłem nazwę pliku war z 06_rest_endpoint-1.0-SNAPSHOT.war na rest.war. Zrobiłem tak, ponieważ TomEE, podobnie jak Tomcat używa nazwy pliku war jako fragmentu adresu URI aplikacji. Szybciej napiszę rest niż 06_rest_endpoint-1.0-SNAPSHOT.

Po przekopiowaniu pliku war uruchom serwer TomEE. Możesz to zrobić przy pomocy pliku catalina.sh3:

/home/mapi/opt/apache-tomee/bin/catalina.sh run

Jeśli zrobiłeś wszystko zgodnie z powyższą instrukcją masz swój pierwszy działający web service. Gratulacje ;). Aby móc zobaczyć go w działaniu odwiedź stronę: http://localhost:8080/rest/reservation. Powinieneś zobaczyć napis Oto Wszystkie rezerwacje :).

Jak działa web service z użyciem JAX-RS

Skoro napisałeś już swój pierwszy web service warto zrozumieć co dzieje się pod spodem. Nie ma tam żadnej magii. Jedynie trochę pracy po stronie kontenera aplikacji.

Otóż w naszym przypadku specyfikacja JAX-RS wymaga od kontenera aplikacji utworzenia specjalnego serwletu. Serlwet ten ma za zadanie obsługiwać wszystkie żądania, które wysyłane są do naszej aplikacji.

Kontener aplikacji musi przeskanować wszystkie klasy w naszej aplikacji pod kątem adnotacji JAX-RS. Jeśli znajdzie te adnotacje w klasach zapamiętuje je. Następnie używa tych klas do obsługi żądań wysyłanych przez klienty4.

Więc w tym przypadku, wysłanie żądania na adres http://localhost:8080/rest/reservation spowoduje wywołanie tego serwletu. Następnie serwlet stworzy instancję klasy ReservationWebservice. Na tej instancji wywoła metodę listReservations. Wartość zwrócona przez tę metodę zostanie użyta do przygotowania odpowiedzi dla klienta.

REST bez użycia JAX-RS

Jak widzisz z powyższego opisu JAX-RS jest jedynie nakładką na mechanizm serwletów. Jeśli czytałeś poprzednie artykuły w ramach kursu to wiesz, że serwlety są sercem ogromnej większości aplikacji webowych napisanych w Javie.

Skoro jest to nakładka, to można tę samą funkcjonalność uzyskać bez niej. Innymi słowy można pisać REST’owe web service’y bez użycia JAX-RS, używając standardowego kontenera serwletów. Jednak JAX-RS sporo upraszcza, pozwala programiście używać wyższego poziomu abstrakcji. Nie musisz pamiętać o doGet czy innych metodach z API serwletów.

Pozostałe metody web service’u

Metody te to jedynie szablony, które mają pokazać Ci przykład użycia adnotacji udostępnionych przez JAX-RS.

Pobieranie encji. Metoda GET

Poprzednia metoda listReservations odpowiadała na żądanie do ścieżki /reservation i zwracała (teoretycznie) wszystkie rezerwacje. Tym razem chcemy zwrócić pojedynczą rezerwację. Proszę spójrz na przykład poniżej:

@GET
@Path("{id}")
public Response getReservation(@PathParam("id") Integer id) {
    return Response.ok("Oto rezerwacja o identyfikatorze " + id + " :)").build();
}

Jak widzisz w tym przypadku metoda getReservation dekorowana jest adnotacją @Path. Metoda ta zostanie wywołana do obsłużenia zapytania wysłanego pod adres /reservation/{id}. Składnia {id} użyta jest do przechwytywania części adresu URL. Dzięki tej składni i użyciu adnotacji @PathParam część tej ścieżki zostanie przekazana w trakcie wywołania metody getReservation. Na przykład zapytanie GET /reservation/123 spowoduje wywołanie tej metody z argumentem id o wartości 123.

Edycja encji. Metoda PUT

@PUT
@Path("{id}")
public Response updateReservation(@PathParam("id") Integer id) {
    return Response.ok("Zmodyfikowaliśmy rezerwację o numerze " + id + " :)").build();
}

W tym przypadku nowa jest dla Ciebie jedynie adnotacja @PUT. Informuje ona kontener aplikacji o tym, że metoda updateReservation powinna być wywołana jeśli klient wyśle zapytanie PUT /reservation/{id}.

Usuwanie encji. Metoda ‘DELETE`

@DELETE
@Path("{id}")
public Response deleteReservation(@PathParam("id") Integer id) {
    return Response.ok("Usunęliśmy rezerwację o numerze " + id + " :)").build();
}

Dodawanie encji. Metoda ‘POST`

@POST
public Response createReservation() {
    return Response.ok("Rezerwacja została utworzona!").build();
}

Testowanie zapytań typu PUT, DELETE

Do przetestowania zapytań typu PUT czy DELETE w przeglądarce potrzebujesz odrobiny HTML’a i kodu w języku JavaScript. Przygotowałem dla Ciebie prostą stronę, która może Ci w tym pomóc.

Jeśli Twoja instancja TomEE będzie uruchomiona i aplikacja będzie zainstalowana, to po odpowiednim wypełnieniu pól zostanie wysłane żądanie.

Słów kilka o CORS

CORS (ang. Cross-Origin Resource Sharing) jest mechanizmem używanym przez przeglądarki. Polega on na dodawaniu odpowiednich nagłówków do odpowiedzi serwera. Zawartość tych nagłówków informuje przeglądarkę czy może używać wyników tego zapytania.

Mechanizm ten użyty jest do podniesienia bezpieczeństwa używania stron internetowych. Na chwilę obecną musisz wiedzieć, że aby móc używać strony do testowania Twój web service musi dodawać odpowiednie nagłówki do odpowiedzi.

Innymi słowy zamiast wysyłać prostą odpowiedź:

Response.
    ok("Usunęliśmy rezerwację o numerze " + id + " :)").
    build();

Musiałbyś dodać zestaw nagłówków:

Response.
    ok("Usunęliśmy rezerwację o numerze " + id + " :)").
    header("Access-Control-Allow-Origin", "*")
    header("Access-Control-Allow-Headers", "Content-Type")
    header("Access-Control-Allow-Methods", "DELETE")
    build();

Jest to wymagane dla każdego zapytania, które będzie uruchamiane ze strony do testowania. Jest to spowodowane tym, że adres URL źródła zapytania jest inny niż adres URL web service’u5. W związku z tym, że trzeba to umieścić w wielu miejscach JAX-RS przychodzi z pomocą.

Adnotacja @Provider

Więcej o tej adnotacji przeczytasz w drugiej części artykułu. Dzisiaj opiszę jedynie część jej możliwości.

JAX-RS udostępnia adnotację @Provider. Służy ona do oznaczenia komponentów, które powinny być automatycznie odkryte przez kontener aplikacji. Tą adnotacją oznacza się klasy, które są odpowiedzialne za przekrojowe zadania (ang. cross-cutting) związane z aplikacją.

Przykładem takiego przekrojowego zadania może być automatyczne dodawanie nagłówków do każdej odpowiedzi. Ta funkcjonalność pozwala na łatwą implementację wymagań narzucanych przez CORS:

@Provider
public class CORSFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        MultivaluedMap<String, Object> headers = responseContext.getHeaders();
        if (!headers.containsKey("Access-Control-Allow-Origin")) {
            headers.add("Access-Control-Allow-Origin", "*");
        }
        if (!headers.containsKey("Access-Control-Allow-Headers")) {
            headers.add("Access-Control-Allow-Headers", "Content-Type");
        }
        if (!headers.containsKey("Access-Control-Allow-Methods")) {
            headers.add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");
        }
    }
}

Klasa CORSFilter implementuje interfejs ContainerResponseFilter. Interfejs ten zawiera metodę filter, która pozwala między innymi na dodanie nagłówków do każdej odpowiedzi.

Dodatkowe materiały do nauki

Jest tego sporo, głównie w języku angielskim:

Podsumowanie

Dzisiaj udało Ci się zdobyć sporo wiedzy. Dowiedziałeś się czym jest REST. Co składa się na platformę Java EE. Poznałeś część funkcjonalności jednej ze specyfikacji znajdującej się pod parasolem Java EE. Zainstalowałeś kontener aplikacji, no i przede wszystkim napisałeś swój pierwszy web service przy pomocy JAX-RS.

W kolejnym artykule z tej serii zajmę się implementacją bardziej użytecznego web service’u. Jeśli nie chcesz pominąć kolejnych artykułów na blogu dopisz się do samouczkowego newslettera i polub Samouczka na Facebooku. Do następnego razu!

  1. Zawsze staram się tłumaczyć angielskie terminy. Jednak w tym przypadku poddałem się. Jak przetłumaczyć web service? Usługa sieciowa? Serwis internetowy? Takie tłumaczenie wprowadzałoby więcej zamieszania niż pożytku. Zostanie więc web service, jeśli masz jakiś pomysł jak to przetłumaczyć daj znać ;). 

  2. W rzeczywistości mimo, że importuję tu API w wersji 2.1 serwer TomEE, którego używam (7.0.4) wspiera wersję 2.0. 

  3. Jeśli pracujesz w systemie Windows wówczas plik ten nazywa się catalina.bat

  4. Oczywiście jest to uproszczenie. W praktyce proces jest trochę bardziej rozbudowany. Proces ten opisany jest w sekcji 3.7 specyfikacji “Matching Requests to Resource Methods”. Domyślnie nowa instancja klasy obsługującej zapytanie tworzona jest dla każdego zapytania. W większości przypadków nie jest to zachowanie, które chcesz zostawić na produkcyjnym środowisku. 

  5. W tym przypadku porównywany jest protokół (np. http), domena (np. www.samouczekprogramisty.pl) i port (np. 8080). 

Zostaw komentarz