piątek, 30 września 2011

Prosta aplikacja RESTful w Struts2 (Convention i REST plugin), Guice iMongoDB

Ponieważ od jakiegoś czasu zdobywam wiedzę na temat nowych frameworków i technologii postanowiłem stworzyć właśnie projekt który integruje je wszystki. Wykorzystam w nim frameworki Guice oraz Struts2, a za przechowywanie danych odpowiadać będzie MongoDB wspierana przez bibliotekę Morphia. Dodatkowo z racji, że ostatnio co chwile mówi się o aplikacjach REST-owych postanowiłem wykorzystać ten wzorzec w mojej aplikacji - "liście zakupowej".


Skąd pobrać projekt


Projekt dostępny jest w formie repozytorium GIT pod adresem https://github.com/darek/struts2-rest-shopping-list

Struts2 oraz Guice


Jeśli ktoś nie zna frameworka Struts2 to wspomnę tylko, że jest to jeden z bardziej popularnych bibliotek do tworzenia aplikacji JEE. Cechuje się elastycznością oraz rozszerzalnością (poprzez pluginy).
Guice jest dość świeży na polu frameworków, służy do wstrzykiwania zależności, a jego twórcami są inżynierowie Google.

MongoDB


Jak wspomniałem na wstępie MongoDB to baza danych oparta o dokumenty, oznacza to tyle, że dane w niej zawarte nie reprezentowane są w formie tabel posiadającej sztywny schemat a raczej w formie łatwo rozszerzalnego dokumentu (wykorzystywany jest tu format BSON). Największą zaletą (prócz łatwej skalowalności i wydajności) jest brak schematu, możemy wrzucić do niej co tylko chcemy bez potrzeby aktualizacji struktur tabel, każdy dokument nawet jeśli jest w tej samej kolekcji może się różnić od innych. Dokumenty te jednak nie posiadają powiązań więc trudniej jest zaimplementować schemat typu jeden do wielu, wiele do wielu. Pomimo tego baza ta idealnie nadaje się do mojej aplikacji. W końcu każda lista zakupowa posiada swój własny zestaw produktów do kupienia, nie potrzebujemy tu oddzielnej tabeli do składowania produktów.

REST


Od dłuższego czasu coraz większą popularność zyskuje wzorzec architektury zwany REST (Representational State Transfer) zaproponowany przez Roya T. Fieldinga w publikacji Principled Design of the Modern Web Architecture. W wielkim skrócie chodzi o takie przygotowanie aplikacji by zasoby którymi operuje były dostępne pod unikalnymi linkami (URI), czyli żeby wyświetlić szczegóły użytkownika naszej aplikacji wykorzystamy adres http://example.com/user/23 (gdzie 23 to jego unikalny identyfikator). Dzięki takiemu rozwiązaniu zapewniony jest warunek bezstanowości aplikacji (nie ma ukrytego wysyłania danych aby zobaczyć dany zasób, np. http://example.com/user + cookie.id=23 do wyświetlenia zasobu użytkownika). Drugą chyba najbardziej widoczną cechą jest użycie większej ilości operacji protokołu HTTP. Zwykła aplikacja zazwyczaj używa tylko dwóch metod: POST i GET podczas gdy aplikacje napisane z wykorzystaniem REST-u mogą dodatkowo wykorzystać metody CREATE, PUT, DELETE  dzięki czemu jeden adres URI może służyć do wielu celów, gdy wywołamy adres http://example.com/user/23 metodą DELETE aplikacja powinna usunąć użytkownika o identyfikatorze 23, wywołując ten sam adres metodą PUT zaktualizujemy dane użytkownika itd. Więcej o architekturze REST możecie poczytać w sieci.

Aplikacja


Założenia aplikacji


  • Aplikacja ma być bardzo uproszczona, powinna posiadać 2 główne widoki, pierwszy do tworzenia list zakupowych i wybierania ich poprzez wpisanie specjalnego identyfikatora, drugi widok powinien wyświetlać szczegóły listy wraz z produktami, oraz powinien pozwalać na dodawanie i usuwanie produktów.
  • Tworzenie listy ma następować po wpisaniu w formularzu nazwy listy i wysłaniu go metodą POST do odpowiedniego kontrolera
  • Otwieranie listy następować będzie poprzez wpisanie jej identyfikatora w odpowiednim formularzu i wysłanie go metodą GET do kontrolera list
  • Dodawanie produktów odbywać się będzie poprzez wysłanie danych z formularza (nazwa i ilość) metodą POST do kontrolera produktów
  • Kasowanie produktów odbywać się będzie poprzez wysłanie identyfikatora produktu metodą DELETE do kontrolera produktów

Konfigurowanie projektu


Na początek skonfigurujemy wszystkie zależności wymagane w projekcie przy użyciu maven-owskiego pliku pom.xml.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>shopping-list</groupId>
<artifactId>shopping-list</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<!-- <packagingExcludes>**/web.xml</packagingExcludes> -->
<webXml>web/WEB-INF/web.xml</webXml>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>morphia</id>
<url>http://morphia.googlecode.com/svn/mavenrepo/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-servlet</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>com.google.code.morphia</groupId>
<artifactId>morphia</artifactId>
<version>0.99</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-rest-plugin</artifactId>
<version>2.2.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.2.3.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-convention-plugin</artifactId>
<version>2.2.3.1</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-struts2</artifactId>
<version>3.0</version>
</dependency>
</dependencies>
<modules>
</modules>
</project>

Poza podstawowymi frameworkami (Guice i Struts) użyjemy również dwóch pluginów Strutsa2 które ułatwią pisanie aplikacji, oraz pozwolą zaimplementować architekturę REST, zostaną one opisane później. Aby dodać biblioteki Morphia potrzebne było skonfigurowanie dodatkowego repozytorium (jako repozytorium służy strona google code).

Naszym głównym frameworkiem będzie Struts2 który będzie odpowiadać za przekierowywanie naszych żądań do odpowiednich klas i generowanie odpowiedzi, Guice będzie pełnił rolę narzędzia do wstrzykiwania zależności (zob. Dependency Injection) dlatego też potrzebujemy plugin Guice-Struts2 który to pozwoli ładnie połączyć oba frameworki.

Jak w każdej aplikacji webowej tak i w tej potrzebujemy skonfigurowany plik web.xml. Ponieważ nasza aplikacja nie jest szczególnie skomplikowana tak i tutaj nie będzie niczego nadzwyczajnego. No może poza jedną rzeczą.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>shopping-list</display-name>
<listener>
<listener-class>com.darekzon.shoppinglist.listener.GuiceStruts2PluginListener</listener-class>
</listener>

<filter>
<filter-name>struts</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>struts</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>


Poza skonfigurowaniem Struts-owego flitra i przekierowaniem do niego wszystkich zapytań musieliśmy utworzyć Listener który odpowiadać będzie za wstrzykiwanie zależności. Listener stworzył Bob Lee jednak nie pamiętam skąd go pobrałem (zapewne jakaś strona w Google Code lub z forum), poza skopiowaniem Listenera zainstalowałem w nim mój moduł który będzie wstrzykiwał zależności (entity manager oraz repozytorium).

public class GuiceStruts2PluginListener extends GuiceServletContextListener {

public Injector getInjector() {
return Guice.createInjector(
new Struts2GuicePluginModule(),
new ServletModule() {
@Override
protected void configureServlets() {
// Struts 2 setup
bind(StrutsPrepareAndExecuteFilter.class).in(Singleton.class);
filter("/*").through(StrutsPrepareAndExecuteFilter.class);

install(new ShoppingListModule());
}
});
}

}


Pora na konfigurację głównego frameworka, ponieważ ostatnio zniechęciłem się do XML-a użyję plików *.properties które umieszczam w głównym folderze z źródłami.
Plik struts.properties odpowiedzialny jest za konfigurację Strutsa i wygląda następująco:

struts.objectFactory = guice

# Fix exception "Cannot cast HttpHeaders to String"
struts.rest.namespace =

struts.mapper.class = rest
struts.convention.action.suffix = Controller
struts.convention.action.mapAllMatches = true
struts.convention.action.alwaysMapExecute = true

struts.convention.relative.result.types    = dispatcher
struts.convention.default.parent.package = rest-default
struts.convention.package.locators  = actions

struts.devMode = true


Na samym początku konfigurujemy fabrykę obiektów (czyli wskazujemy Strutsowi aby używał pluginu Struts2Guice o którym wspomniałem wcześniej).
Opcja struts.rest.namespace to przestrzeń nazw dla serwisów REST-owych (tak jakby kontekst pod którym będą dostępne). Ustawienie tej wartości na "nic" pozwoli uniknąć bardzo uciążliwego problemu przez którego nie można było używać HttpHeaders jako rezultatu metod (zob. błąd 3616 ), warto dodać, że wartość tą należy zostawić pustą, wstawienie "" spowoduje wyrzucanie wcześniej wspomnianych błędów.

struts.mapper.class pozwala ustalić jak żądania URL będą mapowane (co będzie wywoływane po wpisaniu adresu url), rozwinięcie tej nazwy znajdziecie w plikach konfiguracyjnych konkretnego pluginu (w tym przypadku wystarczy wejść do biblioteki struts2-rest-plugin i otworzyć plik struts-plugin.xml).Kolejne wartości ustalają jak ma działać plugin Convention, aby dowiedzieć się co oznaczają możecie przejść do odpowiedniej sekcji na stronie pluginu.

Konfiguracja bazy danych jest bardzo prosta, wystarczy podać dane dostępowe takie jak host, port, login oraz haslo, a także tzw. datastore (czyli nazwę bazy). To wszystko odbywa się w pliku database.properties którego nie trzeba przytaczać.

Jak działają pluginy Strutsa


W aplikacji mamy doczynienia z dwoma pluginami Struts2, pierwszy z nich Convention będzie mapować nasze adresy URL na pakiety i klasy co to znaczy?
Załóżmy, że mamy dwie klasy com.darekzon.application.UserController oraz com.darekzon.application.user.DetailsController. Niech nasza aplikacja będzie dostępna pod adresem http://example.com/. W takim przypadku adres http://example.com/user wywoła nam klasę UserController, natomiast adres http://example.com/user/details wywoła klasę DetailsController znajdującą się w pakiecie user.
Rest-plugin działa trochę inaczej, on do mapowania używa metod HTTP. Mając nasz kontroler UserController możemy go wywołać na 4 sposoby za każdym razem zostanie wywołana inna metoda.

GET         - http://example.com/user  - zostanie wywołana metoda INDEX
POST       - http://example.com/user  - zostanie wywołana metoda CREATE
GET         -  http://example.com/user/ID  - zostanie wywołana metoda SHOW
PUT         - http://example.com/user/ID - zostanie wywołana metoda UPDATE
DELETE - http://example.com/user/ID - zostanie wywołana metoda DELETE


Metoda PUT oraz Delete muszę mieć dodatkowy parametr ID (identyfikator obiektu) ponieważ są wykonywane na obiekcie (więc logiczne, że trzeba wskazać obiekt). Gdy do metody GET dodamy identyfikator zostanie wywołana metoda SHOW ponieważ aplikacja wie że w tym wypadku, nie chcemy listy a konkretny obiekt.

Drugi kontroler


Nie będę pokazywał jak powstał pierwszy kontroler, bo jest on na tyle prosty, że nie wymaga opisu, zabawę zaczniemy z drugim, najważniejszym kontrolerem który  odpowiadać będzie za obsługę całej listy, a zwie się ShoppingListController.
Ponieważ ShoppingListController będzie operować na danych, musimy wskazać mu klasę która będzie do tego przeznaczona, w tym przypadku jest to ShoppingListRepository a dodajemy ją do kontrolera poprzez wstrzyknięcie

ShoppingListRepository shoppingListRepository;

@Inject
publicvoid setShoppingListRepository(ShoppingListRepository pr){
this.shoppingListRepository = pr;
}

Szczegółowa implementacja tej klasy dostępna jest w źródłach.
Ważny jest również fakt, że klasa ta implementuje interfejs ModelDriven<T>, dzięki temu gdy poprosimy o dane w formacie XML nasza aplikacja będzie wiedziała jaki obiekt przetworzyć do żądanej postaci. Gdyby nie było tego interfejsu, przy prośbie o format XML dostalibyśmy przetkonwerterowaną całą klasę ShoppingListController czego chcemy uniknąć. Interfejs ten wymaga od nas zaimplementowania jednej metody "public T getModel()" zwracającej obiekt który będziemy konwertować (w razie konieczności).

Strona początkowa:


Strona początkowa aplikacji posiada dwa formularze, pierwszy otwiera listę o zadanym przez nas identyfikatorze, druga po podaniu nazwy tworzy taką listę. Widok prezentuje się następująco:



Pierwsza metoda jaką wywołujemy (czyli index wchodząc pod adres http://example.com/shopping-list/index.xhtml) wygląda następująco:

public HttpHeaders index(){
if(this.id != null && this.list==null){
returnnew DefaultHttpHeaders("notfound").withStatus(404);
}
if(this.id !=null && this.list!=null){
// BUG - even with directly setted 302 status application is returning 201 (created) status.
return new DefaultHttpHeaders("show").setLocationId(this.id).withStatus(302);
}
return new DefaultHttpHeaders("index");
}

Tak naprawdę mogłaby być ona pusta z racji, że jej jedynym zadaniem jest wyświetlenie dwóch formularzy, ale wysyłanie formularza otwierającego listę powoduje wywołanie w/w metody (a nie metody show jak mogłoby się zdawać) dzieje się tak ponieważ wysłanie formularza przenosi nas pod adres http://example.com/shopping-list/index.xhtml?id=IDENTYFIKATOR  - a tego plugin rest nie potrafi zmapować do metody SHOW. W metodziej tej sprawdzamy oczywiście, czy lista istnieje, jeśli nie, wyświetlamy komunikat (cały widok) z informacją o braku listy oraz co ważne wysyłamy status HTTP 404 (NOT FOUND).
Jeśli lista została znaleziona wysyłamy jej identyfikator oraz status 302 (FOUND) który powinien wymusić na przeglądarce automatyczne przekierowanie pod prawidłowy adres (tj. http://example.com/shopping-list/ID.xhtml)

Tworzenie listy:


Lista tworzona jest po wysłaniu formularza metodą post pod adres http://example.com/shopping-list/index.xhtml, wtedy wywoływana jest metoda create która wygląda następująco:
@Validations(requiredStrings = {@RequiredStringValidator(type = ValidatorType.FIELD, fieldName = "list.name", message = "You must set shopping list name a value for string.")})
public HttpHeaders create(){
String id = this.shoppingListRepository.create(this.list);
return new DefaultHttpHeaders("create").withStatus(201).setLocationId(id.toString());
}
Nad wyraz prosta (jak cała aplikacja) metoda składa się z 3 części:
  1. ustawienie walidatora tak by wymagał podania nazwy listy
  2. przesłanie listy (z wypełnionym polem nazwy) do repozytorium w celu zapisania w bazie
  3. zwrócenie nagłówków ze statusem 201 (CREATED) oraz identyfikatorem listy w celu przejścia do strony z listą

Metodę tą możemy wywołać na 3 sposoby, poprzez wypełnienie formularza na stronie oraz wciśnięcie przycisku, poprzez wysłanie zapytania w formie XML-a (metodą CREATE) pod adres http://example.com/shopping-list.xml lub wysłanie zapytania w formie JSON (również CREATE) pod ten sam adres lecz z rozszerzeniem json.

Po wywołaniu tej metody zostajemy automatycznie przekierowani pod adres http://example.com/shopping-list/ID.xhtml który wywołuje metodę show.
Strona z listą zakupów prezentuje się jak poniżej:



public HttpHeaders show(){
if(this.list == null){
returnnew DefaultHttpHeaders("notfound").withStatus(404);
}
returnnew DefaultHttpHeaders("show").withStatus(200);
}


W metodzie tej sprawdzamy jedynie czy lista istnieje, jeśli nie, generujemy informację o braku listy (jak w metodzie index) oraz zwracamy status 404 (NOT FOUND), w innym przypadku wyświetlamy listę oraz zwracamy status 200 (OK).

Dodawanie produktów


Dodawanie produktów następuje poprzez wysłanie formularza z nazwą produktu oraz ilością jaką chcemy kupić metodą POST do klasy ProductController co ma insynuować tworzenie nowego Produktu gdy tak naprawdę będziemy jedynie aktualizować naszą listę. Poniżej znajduje się wynik wysłania formularza.



Po dodaniu produktu wysyłamy status 204 (NO CONTENT) ponieważ nie chcemy wysyłać niczego, zamiast tego odświeżamy naszą stronę dzięki czemu dostajemy świeże dane. Gdybyśmy zamiast tego chcieli wysłać naszą listę i odświeżyć ją poprzez javascript musimy wysłać status 200 (OK)

Kasowanie produktów


Aby wykasować produkt z listy należy kliknąć w ikonę koszyka która pojawia się po najechaniu na nasz produkt. Po kliknięciu javascript automatycznie wyśle żądanie typu DELETE w odpowiedzi dostając status taki sam jak przy dodawaniu.

Inne formaty


Aby zobaczyć naszą listę zakupową w innym formacie wystarczy otworzyć stronę z rozszerzeniem XML:



bądź JSON:



Dzięki temu możemy zintegrować naszą aplikację z aplikacjami zewnętrznymi bez zbędnego nakładu pracy.

Podobne tematy


  1. Podobny tutorial: http://www.zulutown.com/blog/2009/01/28/rest-web-application-with-struts21-rest-and-convention-plugins/
  2. Hypertext Transfer Protocol: http://www.w3.org/Protocols/rfc2616/rfc2616.html
  3. Nieco krytycznie o REST: http://michalorman.pl/blog/2011/02/bullshit-bingo-rest-i-restful/
  4. Prezentacja na temat REST: https://www.soa.edu.pl/c/document_library/get_file?uuid=46b0faf6-6743-4184-ab16-dbddfd413685&groupId=10122

Brak komentarzy:

Prześlij komentarz