Java RESTfull Web Services

Mariusz Szulist
January 27, 2016

Wraz z rozwojem Internetu, nastaniem ery WEB 2.0, otaczających nas z każdej strony aplikacji mobilnych, kroczącego wielkimi susami HTML 5 i wszędobylskiego JavaScript oraz niespełnionych oczekiwań w stosunku do stosu SOAP, coraz więcej słyszy się o usługach opartych na REST.

Co to jest REST

REST to skrót od Representational State Transfer, technika ta bazuje na protokole http i założeniu że unikalny URL reprezentuje jakiś obiekt. Można wchodzić w interakcję z tym obiektem za pomocą metod HTTP:

  • POST (tworzenie)
  • PUT (aktualizacja)
  • GET (pobieranie)
  • DELETE (usuwanie)

Ciekawy i jasny opis znajdziecie tutaj.

REST jest architekturą oprogramowania opartą o HTTP a tym samym jest bezstanowy, gdzie każdy request odbywa się w całkowitej izolacji. Dzięki temu jest bardzo skalowalny, nie trzeba trzymać sesji i obsługiwać związanych z nią obsługą wyjątków. Jeśli sesja jest nam potrzebna klient sam musi zapewnić jej przechowywanie

WADL

Usługi bazujące na SOAP wykorzystywały WSDL do opisu kontraktu. Dla opisania możliwych interakcji w architekturze REST wykorzystywany jest WADL. Niestety nie jest on standardem i nie jest szeroko stosowany. Jego przykładowy kształt podaję go jako ciekawostkę:

JAVA API dla REST

Aby pisanie kodu nie opierało się na mozolnym wysyłaniu i odbieraniu ramek HTTP, człowiek wyszedł z jaskini i opracował JAX-RS, który od JAVA EE 7 jest dostępny w wersji 2.0 (jsr 339). Dla programistów ważne jest że w wersji 2.0 JAX-RS został zintegrowany z Bean Validation (javax.validation), co sprawia że kodowanie w nowym trendzie staje się jeszcze przyjemniejsze.

Annotacje używane poniżej są z pakietów javax.ws.rs.*, zakładamy że nasz serwer jest skonfigurowany pod domeną www.sklepik.speednet.pl

@Path("/napoj")
public class NapojRestService {
    @GET
    @Produces("text/plain")
    public String getDrink() {
        return "Coca cola";
    }
}

Aplikacja wdrożona z następującą klasą opisującą usługę REST składa się z kilku elementów:

  1. @Path mówi nam, pod jakim Uri będzie wystawiony zasób, w tym przypadku http://www.sklepik.speednet.pl/napoj
  2. Metoda została oznaczona @GET przez co zostanie ona wywołana na odpowienie żądanie HTTP
  3. Określono typ MIME dla odpowiedzi jako „text/plain” za pomocą @Produces zamiast tekstowego opisu sugerowane jest by korzystać ze stałych zdefiniowanych w dokumentacji, co w naszym przypadku zmieniłoby kod na @Produces(MediaType.TEXT_PLAIN)

I już ☺ mamy najprostszą usługę. Klasę można wdrożyć jako @Stateless i korzystać z dobrodziejstw CDI.

Rozszerzamy wiedzę

Ścieżki i parametry

A co, jeśli chcielibyśmy by usługi danego typu np. „drink”, podzielić logicznie na typy napojów? Tak by
http://www.sklepik.speednet.pl/drink/carbonated zwróciło mi tylko napoje gazowane itd

Realizacja tej koncepcji poniżej:

@Path("/drink")
public class DrinkRestService {
    @GET
    public Drink getDrink() {
        // URI : /drink
    }

    @GET
    @Path("/carbonated")
    public Carbonated getCarbonated() {
        // URI : /drink/carbonated
    }

    @GET
    @Path("/still")
    public Still getStill() {
        // URI : /drink/still
    }
}

Mamy już piękne ścieżki, do pełni szczęścia potrzebna jest nam wiedza jak przekazywać parametry w URI, a przede wszystkim w jaki sposób zmapować je tak, by były łatwo dostępne z kodu aplikacji.

Dzięki JAX-RS mamy możliwość wyciągnięcia następujących parametrów @PathParam, @QueryParam, @FormParam, @MatrixParam, @CookieParam, @HeaderParam.

Parametr jest reprezentowany zmienną w nawiasach lub poprzez wyrażenie regularne. Załóżmy że chcielibyśmy spytać się o napój HopCola, czy znajduje się w napojach gazowanych. Poniżej przykładowe rozwiązanie:

@Path("/drink")
@Produces(MediaType.APPLICATION_JSON)

public class DrinkRestService {
	@Path("carbonated/{text}")
	public Carbonated searchCarbonated(@PathParam("text") String textToSearch) {
			// URI : /drink/carbonated/HopCola
	}
}

Schemat ścieżki zapisanej w @Path jasno opisuje, że za /drink/carbonated/ znajduje się nasz parametr text o wartości HopCola.

Jeśli chcielibyśmy zapytać się o napoje gazowane o cenie mniejszej niż zadana przykładowy kod wyglądałby jak ten poniżej:

@Path("/drink")
@Produces(MediaType.APPLICATION_JSON)
public class DrinkRestService {	
    @GET
    @Path("carbonated/{price : \d+}")
    public Carbonated getCarbonatedByPrice(@PathParam("price") Long price) {
        // URI : drink/carbonated/7
    }
}

Zastosowaliśmy tutaj opis, że parametr price musi spełniać warunki wyrażenia regularnego \d+.

Bardziej znanym i akceptowalnym sposobem na zapytania jest następujący format URI drink/carbonated?price=7 jeśli chcemy osiągnąć taki rezultat powyższy kod zmieniamy w następujący sposób:

@Path("/drink")
@Produces(MediaType.APPLICATION_JSON)
public class DrinkRestService {	
    @GET
    // @Path(carbonated/"{price : \d+}") comment this
    public Carbonated getCarbonatedByPrice(@QueryParam("price") Long price) {
        // URI : drink/carbonated?price=7
    }
}

Format zwracanych danych

W powyższych przykładach wszystkie dane zwracaliśmy w formacie JSON:
@Produces(MediaType.APPLICATION_JSON)
Przykładowo jeśli z jakichś powodów ta sama odpowiedź ma być dostępna w formacie JSON lub XML, możemy zastosować następującą sztuczkę:

@Path("/drink")
@Produces(MediaType.TEXT_PLAIN)
public class DrinkRestService {	
    @GET
    @Path("/carbonated")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Carbonated getCarbonatedByPrice(@QueryParam("price") Long price) {
        // URI : drink/carbonated?price=7
    }

    @GET
    @Path("/carbonated")
    public Gazowane getCarbonatedByPricePlainText(@QueryParam("price") Long price) {
        // URI : drink/carbonated?price=7
    }
}

Powyższa metoda wygeneruje format najbardziej odpowiadający żądaniu klienta, które definiuje się za pomocą nagłówka Accept. Po więcej informacji odsyłam do RFC 2616.

W przypadku gdy Request będzie zawierał nagłówek o następującej treści:
Accept: text/plain oraz będzie wysłany na odpowiednie uri (np. drink/carbonated?price=7)
to uruchomiona zostanie metoda getCarbonatedByPricePlainText.

TYPY ZWRACANE

Póki co zajęliśmy się formatem zwracania danych za pomocą annotacji @Produces, serwis REST może zwracać to co każdy standardowy typ Java lub dowolny obiekt. Zaleca się jednak by zwracać w metodach javax.ws.rs.core.Response otrzymywany z ResponseBuilder. Przykładowa metoda wyglądałaby następująco:

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getAsJson() {
    return Response.ok(new Drink("Cola"),MediaType.APPLICATION_JSON).build();
}

Dzięki temu, że typem zwracanym jest Response mamy wpływ np. na zwracany kod żądania jeśli byśmy chcieli wyznaczyć własne.

To, czy lepiej zwracać Response czy POJO, zostawiam do dyskusji.
Bardziej dociekliwych zapraszam do zapoznania się z informacjami, które znajdziecie pod linkami zamieszczonymi poniżej.

Linki:

Chcesz poznać nas lepiej? Dowiedz się, co nas wyróżnia.