Stripes a Spring ve webových aplikacích

Z FI WIKI
Verze z 28. 4. 2013, 14:27; 374351@muni.cz (diskuse | příspěvky)

(rozdíl) ← Starší verze | zobrazit aktuální verzi (rozdíl) | Novější verze → (rozdíl)
Přejít na: navigace, hledání


Ukážeme si kompletní webovou aplikaci se strukturou reálných webových aplikací.

Aplikace ukazuje základní práci s daty v relační databázi. Základní datovou entitou je kniha, reprezentovaná třídou Book. Aplikace umožňuje zadání nové knihy, změnu údajů o knize, smazání knihy, výpis všech knih a vyhledávání knih.

Aplikace využívá z platformy Spring IoC container a knihovnu pro práci s JDBC. Dále používá logovací rozhraní SLF4J (Simple Logging Facade for Java) a logovací framework Log4J. Pro sestavení je použit Maven, jehož projekty jdou v NetBeans přímo otevřít. Pro webové rozhraní je použit framework Stripes.


Prostředí

  • NetBeans 7.2
  • Oracle JDK 7
  • Tomcat 7
  • Maven 3
module add netbeans-7.2 jdk-1.7.0_03 maven-3.0.4

Pokud nemáte v NetBeans nakonfigurovaný TomCat, přidejte instanci z adresáře /packages/share/netbeans-7.2/apache-tomcat-7.0.27 a zvolte si privátní adresář pro TomCat někde ve svém adresáři.

Zkontrolujte si, že používáte JDK 7. Klikněte pravým tlačítkem na projekt, vyberte Properties - Build - Compile a vyberte JDK 1.7. Pokud není v nabídce, klikněte na Manage Java Platforms a přidejte adresář /packages/run.64/jdk-1.7.0_03.

Zkontrolujte si, že i TomCat používá JDK 7. Klikněte na záložku Services, vyberte Servers - Apache Tomcat, klikněte pravým tlačítkem, vyberte Properties, pak záložku Platform a nastavení Java Platform.


Základ aplikace

Začneme základní kostrou aplikace. Stáhněte si soubor spring-stripes-zaklad.zip a rozbalte ho na disk. Obsahuje tyto soubory:

pom.xml                                                 ... popis pro Maven, zejména seznam použitých knihoven
src/main/webapp/WEB-INF/web.xml                         ... deployment descriptor webové aplikace
src/main/webapp/WEB-INF/spring-context.xml              ... konfigurační soubor pro Spring
src/main/java/cz/muni/makub/model/Book.java             ... JavaBean představující datovou položku, knihu
src/main/java/cz/muni/makub/model/BookManager.java      ... rozhraní pro práci s knihami
src/main/java/cz/muni/makub/model/BookManagerImpl.java  ... implementace práce s knihami pomocí Spring JDBC
src/main/webapp/rozvrh.jsp                              ... šablona společného vzhledu webových stránek
src/main/webapp/style.css                               ... CSS style sheet
src/main/resources/log4j.xml                            ... konfigurace logování 
src/main/resources/StripesResources.properties          ... i18n texty pro Stripes
src/main/webapp/META-INF/context.xml                    ... konfigurační soubor pro Tomcat s definicí spojení na databázi
src/main/webapp/index.jsp                               ... úvodní webová stránka


V této podobě aplikace obsahuje základní konfiguraci pro logování, Spring, Stripes, Tomcat a samotnou webovou aplikaci. K dispozici jsou tři třídy, jedna představující datovou položku, druhá je rozhraní pro práci s daty a třetí je implementace tohoto rozhraní.

Otevřete adresář jako projekt v NetBeans, tj. vyberte v menu File - Open project a najděte adresář s rozbaleným zipem. Prohlédněte si všechny soubory !

Maven

V souboru pom.xml jsou zejména vyjmenovány knihovny, které jsou pro sestavení aplikace potřeba. Dále je v něm uvedena použitá verze jazyka Java, tj. 7.

Logování

Každá pořádná aplikace potřebuje logovat. Zde je použit framework Log4J, který je pomocí souboru log4j.xml nastaven tak, aby logy šly na standardní výstup. Aplikace ale nezávisí přímo na Log4J, ale používá wrapper SLF4J, který teprve vybírá logovací framework. SLF4J nahrazuje dříve používaný wrapper Jakarta Commons Logging, viz PV168/Logování.

Model

Aplikace využívá Model-View-Controller architekturu, a model je tvořen třídou Book představující datovou entitu - knihu, a rozhraním BookManager obsahujícím metody pro operace s knihami. Toto rozhraní má pak jednu implementaci BookManagerImpl, která přes Spring JDBC pracuje s relační databází.

Spring

Spring je aktivován ve web.xml zavedením jeho ContextListeneru. V souboru spring-context.xml je nastaveno jednak vytvoření DataSource pro připojení na databázi, a pak že se Spring má řídit anotacemi na třídách. Díky tomu Spring najde ve třídě BookManagerImpl anotaci @Repository, která mu řekne, že má vytvořit instanci této třídy, a anotaci @Autowired, která zajistí injekci DataSource do této vytvořené instance. Tím máme vyřešenu inicializaci modelu aplikace a jeho přístup k databázi.

Dalo by se Spring nahradit třeba vytvořením vlastního ContextListeneru, který by vytvořil instance BookManagerImpl a zajistil jeho spojení na databázi, ale Spring je využit i ve třídě BookManagerImpl pro práce s JDBC, tak je výhodné jej využít i pro inicializaci aplikace. Dále je pak výhodné v ActionBeans frameworku Stripes využít anotaci @SpringBean, která nám zajistí injekci odkazu na model.

CSS a jednotný vzhled stránek

V souboru style.css je CSS (Cascading Style Sheet), tj. definice grafického vzhledu některých HTML elementů, např. navigačního menu nebo tabulek. V souboru rozvrh.jsp je pak šablona základního rozvržení stránky udělaná pomocí JSP taglib ze Stripes. Obsahuje navigační menu a odkaz na style.css.

Stripes

Framework Stripes je aktivován v souboru web.xml nastavením příslušných filtrů a servletu. Konfigurace nastavuje i použité jazyky a kódování utf-8. V souboru StripesResources.properties jsou předpřipravená anglická chybová hlášení.

Databáze

V konfiguraci pro Tomcat v souboru context.xml je uvedeno spojení na databázi typu JavaDB jménem books.

Vytvořte proto databázi (Services - Databases - JavaDB - Create database...) jménem books, zvolte username a password jako 'pokus'. Připojte se k databázi.

Vytvořte tabulku (klikněte pravým tlačítkem myši na spojení k databázi, zvolte Execute command, zkopírujte příkaz níže a proveďte ho).

CREATE TABLE books (
 id INT NOT NULL GENERATED ALWAYS AS IDENTITY CONSTRAINT books_pk PRIMARY KEY,
 name VARCHAR(30),
 author VARCHAR(30),
 paperback VARCHAR(5), 
 color VARCHAR(8), 
 price int
);

(sloupec paperback měl být typu BOOLEAN, ale JavaDB ho nepodporuje :-( Takže budeme ukládat jako řetězce true/false.)

Naplňte ji daty např. příkazem

INSERT INTO books (name,author,paperback,color,price) VALUES ('Babička','Božena Němcová','true','GREEN',100);

Společný vzhled stránek

V aplikaci zatím existuje jen jedna webová stránka src/main/webapp/index.jsp s následujícím obsahem:

 
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="s" uri="http://stripes.sourceforge.net/stripes.tld" %>
 
<s:layout-render name="/rozvrh.jsp" nadpis="Hlavní stránka">
    <s:layout-component name="telo">
        Tady zatím nic není. Ale je to úvodní stránka aplikace.
    </s:layout-component>
</s:layout-render>

Soubor index.jsp je zobrazen, pokud URL vede na adresář. Jeho obsah využívá knihovnu značek JSP ze Stripes pro snadnou definici jednotného vzhledu všech stránek. Značka s:layout-render pomocí atributu name říká, že se má vložit obsah značky s:layout-definition ze souboru rozvrh.jsp a na příslušné místo vložit obsah značky s:layout-component.

Než spustíte aplikaci, nastavte v properties projektu v kategorii Run položku server na Tomcat 7.0.

Spusťte aplikaci a prohlédněte si vygenerovanou stránku.


samostatné cvičení

Vytvořte několik dalších JSP stránek využívajících stejný vzhled a přidejte je do navigačního menu.

Úloha pro pokročilé: změňte index.jsp tak, aby se přidal další obsah hlavičky stránky, ne do těla. Například další definici CSS nebo JavaScript.

Zobrazení záznamů z databáze

Začneme zobrazením všech záznamů z databáze. Vytvořte soubor src/main/java/cz/muni/makub/web/BooksActionBean.java (tj. vytvořte nový java package cz.muni.makub.web - pozor, to web na konci je důležité ! - a v něm třídu BooksActionBean.java) s obsahem:

 
package cz.muni.makub.web;
 
import cz.muni.makub.model.Book;
import cz.muni.makub.model.BookManager;
import net.sourceforge.stripes.action.*;
import net.sourceforge.stripes.controller.LifecycleStage;
import net.sourceforge.stripes.integration.spring.SpringBean;
import net.sourceforge.stripes.validation.Validate;
import net.sourceforge.stripes.validation.ValidateNestedProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.util.List;
 
@UrlBinding("/books/all")
public class BooksActionBean implements ActionBean {
 
    final static Logger log = LoggerFactory.getLogger(BooksActionBean.class);
 
    private ActionBeanContext context;
 
    @SpringBean
    protected BookManager bookManager;
 
    @DefaultHandler
    public Resolution all() {
        log.debug("all()");
        return new ForwardResolution("/show.jsp");
    }
 
    public List<Book> getBooks() {
        return bookManager.getAllBooks();
    }
 
    @Override
    public void setContext(ActionBeanContext context) {
        this.context = context;
    }
 
    @Override
    public ActionBeanContext getContext() {
        return context;
    }
}

a soubor src/main/webapp/show.jsp (tj. v projektu NetBeans klikněte na Web Pages a vytvořte JSP) s obsahem

 
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="s" uri="http://stripes.sourceforge.net/stripes.tld" %>
 
<s:layout-render name="/rozvrh.jsp" nadpis="Knihy">
    <s:layout-component name="telo">
        <s:useActionBean beanclass="cz.muni.makub.web.BooksActionBean" var="actionBean"/>
        Všechny knihy:
        <table class="zakladni">
            <tr>
                <th>id</th>
                <th>autor</th>
                <th>název</th>
                <th>paperback</th>
                <th>barva</th>
                <th>cena</th>
                <th></th>
                <th></th>
            </tr>
            <c:forEach items="${actionBean.books}" var="book">
                <tr>
                    <td>${book.id}</td>
                    <td><c:out value="${book.author}"/></td>
                    <td><c:out value="${book.name}"/></td>
                    <td><c:out value="${book.paperback}"/></td>
                    <td><f:message key="Book.Color.${book.color}"/></td>
                    <td><c:out value="${book.price}"/></td>
                    <td></td>
                    <td></td>
                </tr>
            </c:forEach>
        </table>
    </s:layout-component>
</s:layout-render>

Tento ActionBean je pomocí anotace @UrlBinding namapován na URL /books/all, pomocí anotace @SpringBean dostane injekci odkazu na instanci BookManagerImpl a jeho metoda all() je označena anotací @DefaultHandler takže bude zavolána při vyvolání URL. Metoda all() jen předá řízení na JSP stránku show.jsp, která ale zavolá pomocí ${actionBean.books} metodu getBooks() která načte údaje z databáze.

Do souboru index.jsp přidejte odkaz:

 
<br><s:link href="/books/all">odkaz na knihy</s:link>

Vytváření nových záznamů

Abychom mohli do databáze i přidávat, potřebujeme přidat formulář na stránku a do ActionBeanu obsluhu nového eventu, který nazveme add.

Nejprve tedy vytvořte nový soubor src/main/webapp/form.jsp s obsahem:

 
<%@ taglib prefix="s" uri="http://stripes.sourceforge.net/stripes.tld" %>
<s:errors/>
<table>
    <tr>
        <th><s:label for="b1" name="book.author"/></th>
        <td><s:text id="b1" name="book.author"/></td>
    </tr>
    <tr>
        <th><s:label for="b2" name="book.name"/></th>
        <td><s:text id="b2" name="book.name"/></td>
    </tr>
    <tr>
        <th><s:label for="b3" name="book.paperback"/></th>
        <td><s:checkbox id="b3" name="book.paperback"/></td>
    </tr>
    <tr>
        <th><s:label for="b4" name="book.color"/></th>
        <td><s:select id="b4" name="book.color">
            <s:options-enumeration enum="cz.muni.makub.model.Book.Color"/>
        </s:select></td>
    </tr>
    <tr>
        <th><s:label for="b5" name="book.price"/></th>
        <td><s:text id="b5" name="book.price" size="4"/></td>
    </tr>
</table>

a do show.jsp vložte následující text:

 
        <s:form beanclass="cz.muni.makub.web.BooksActionBean">
            <fieldset><legend>Nová kniha</legend>
                <%@include file="form.jsp"%>
            <s:submit name="add">Vytvořit novou knihu</s:submit>
            </fieldset>
        </s:form>

Speciální soubor form.jsp vyrábíme proto, abychom ho mohli později použít znovu pro editování.

Ve tříde BooksActionBean změnte @URLBinding na

 
@UrlBinding("/books/{$event}")

protože od teď bude třída obsluhovat více eventů, jednak all pro zobrazení všech knih a pak add pro přidání nové knihy.

Dále do třídy přidejte:

 
    @ValidateNestedProperties(value = {
            @Validate(on = {"add", "save"}, field = "author", required = true),
            @Validate(on = {"add", "save"}, field = "name", required = true),
            @Validate(on = {"add", "save"}, field = "price", required = true, minvalue = 1)
    })
    private Book book;
 
    public Resolution add() {
        log.debug("add() book={}", book);
        bookManager.createBook(book);
        return new RedirectResolution(this.getClass(), "all");
    }
 
    public Book getBook() {
        return book;
    }
 
    public void setBook(Book book) {
        this.book = book;
    }

Metody getBook() a setBook() umožňují Stripes mapovat položky formuláře začínající na book. na položky ve třídě Book. Anotace @ValidateNestedProperties a @Validate definují požadavky na validaci dat zadaných uživatelem. V tomto případě položky author a name musí být vyplněny a položka price musí mít hodnotu alespoň 1. To, že price musí být číslo, poznají Stripes samy podle toho, že je typu int.

Lokalizace do češtiny

Protože uživatel může při zadávání dat udělat různé chyby (např. do ceny zadat něco jiného než číslo), Stripes mají standarní sady chybových hlášení. Přeložíme ta, která se mohou objevit, do češtiny. Chybová hlášení potřebují znát lokalizovné názvy jednotlivých položek formuláře, přidáme je tedy taky. A nakonec přidáme i počeštěné názvy položek výčtového typu Color.

Takže vytvořte soubor src/main/resources/StripesResources_cs.properties (klikněte v projektu na Other Sources - resources - StripesResources.properties pravým tlačítkem a vyberte z menu Add - Locale a zadejte Language code cs) s obsahem:

 #chybova hlaseni
 stripes.errors.header=<div style="color:#b72222; font-weight: bold">Prosím opravte následující chyby:</div><ol>
 validation.required.valueNotPresent=pole {0} musí být vyplněno
 converter.number.invalidNumber=Hodnota "{1}" zadaná do pole {0} musí být číslo
 validation.minvalue.valueBelowMinimum=Minimální hodnota pro {0} musí být {2}
 
 #nazvy poli
 book.author=autor
 book.name=název
 book.paperback=paperback
 book.color=barva
 book.price=cena

 #nazvy polozek enumu
 Book.Color.BLUE=modrá
 Book.Color.GREEN=zelená
 Book.Color.RED=červená
 Book.Color.ORANGE=oranžová
 Book.Color.WHITE=bílá
 Book.Color.OTHER=jiná

Spusťte si upravenou aplikaci.

Mazání záznamů

Teď přidáme možnost mazat záznamy. K tomu potřebujeme obsluhu dalšího eventu, nazveme ho delete. Do ActionBeanu přidejte

 
    public Resolution delete() {
        log.debug("delete({})", book.getId());
        bookManager.deleteBook(book);
        return new RedirectResolution(this.getClass(), "all");
    }

a do show.jsp změňte předposlední sloupec generovaných řádků tabulky na:

 
<td><s:link beanclass="cz.muni.makub.web.BooksActionBean" event="delete"><s:param name="book.id" value="${book.id}"/>smazat</s:link> </td>

Pokud chcete, aby parametr s id knihy byl v URL hezčí, můžete změnit @UrlBinding na

 
@UrlBinding("/books/{$event}/{book.id}")

Tato vlastnost Stripes se nazývá clean URLs.

Všimněte si, že metoda delete() vrací RedirectResolution, tj. po vyvolání smazání dostane prohlížeč redirect na stránku se zobrazením. Této technice se říká Redirect-After-Post a zabraňuje nechtěným duplicitním operacím v databázi.

Editace záznamů

Poslední operací s daty je editace. Její implementace je o trochu komplikovanější, musíme totiž udělat dva eventy, první natáhne původní data z databáze a zobrazí editovací formulář, druhý pak přijme nová data od uživatele a uloží je do databáze.

Do ActionBeanu tedy přidejte:

 
    @Before(stages = LifecycleStage.BindingAndValidation, on = {"edit", "save"})
    public void loadBookFromDatabase() {
        String ids = context.getRequest().getParameter("book.id");
        if (ids == null) return;
        book = bookManager.getBookById(Integer.parseInt(ids));
    }
 
    public Resolution edit() {
        log.debug("edit() book={}", book);
        return new ForwardResolution("/edit.jsp");
    }
 
    public Resolution save() {
        log.debug("save() book={}", book);
        bookManager.updateBook(book);
        return new RedirectResolution(this.getClass(), "all");
    }

Metoda s anotací @Before je nový rys, anotace zajišťuje, že metoda je vyvolána ve specifikované fázi zpracování requestu. Potřebujeme, aby ještě před nastavováním dat z formuláře do instance třídy Book byla instance naplněna původními daty z databáze. V tomto případě sice nastavujeme všechny položky knihy, takže to není nezbytně nutné, ale pokud by formulář umožňoval editovat jen některé položky, bylo by nutné nejdřív natáhnout původní data.

Metoda loadBookFromDatabase() tak podle HTTP parametru book.id načte z databáze původní data a naplní proměnnou book. Nastavování hodnot z formuláře (binding) před vyvoláním metody save() tak proběhne na naplněné instanci třídy Book.

Abychom mohli editaci vyvolat, v show.jsp je třeba změnit poslední sloupec tabulky na:

 
<td><s:link beanclass="cz.muni.makub.web.BooksActionBean" event="edit"><s:param name="book.id" value="${book.id}"/>edit</s:link> </td>

a vytvořit novou stránku pro editaci, která využije soubor form.jsp. Vytvořte tedy nový soubor src/main/webapp/edit.jsp s obsahem:

 
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="s" uri="http://stripes.sourceforge.net/stripes.tld" %>
<s:layout-render name="/rozvrh.jsp" nadpis="Editace knihy">
    <s:layout-component name="telo">
       <s:useActionBean beanclass="cz.muni.makub.web.BooksActionBean" var="actionBean"/>
 
       <s:form beanclass="cz.muni.makub.web.BooksActionBean">
           <s:hidden name="book.id"/>
            <fieldset><legend>Změna údajů</legend>
                <%@include file="form.jsp"%>
            <s:submit name="save">Uložit</s:submit>
            </fieldset>
        </s:form>
 
    </s:layout-component>
</s:layout-render>

Spusťte si znovu aplikaci.

Obsluha dalších událostí

Obsluhu dalších událostí můžeme do action beanu snadno dodat přidáním dalších metod, které mají následující tvar:

 
 
public Resolution nazevUdalosti() {
    //obsluha udalosti
    ...
    
        //vraceni ForwardResolution pro zobrazení JSPO stránky
        return new ForwardResolution("/stranka.jsp");
    } else {
        //vraceni RedirectResolution pro redirect prohlizece na prislusne URL
        return new RedirectResoution(NejakyActionBean.class,"nazevNejakeUdalosti");
    }
}

událost pak vyvoláme odkazem v JSP s případnými parametry. Odkaz lze specifikovat pomocí URL nebo pomocí třídy s ActionBeanem:

 
  <s:link beanclass="TridaActionBeanu" event="nazevUdalosti"> 
     <s:param name="p1" value="${p1}" /> 
     <s:param name="p2" value="${p2}" /> 
     text odkazu 
  </s:link>

případně submitem ve formuláři se stejným jménem:

 
  <s:submit name="nazevUdalosti"> text tlacitka </s:submit>

Parametry a obsah polí ve formuláři se Stripes pokusí nastavit do stejnojmených properties na action beanu, tj. parametr "p1" se pokusí nastavit např. do:

 
 int p1;
 public int getP1() { return p1; } 
 public void setP1(int i) { p1 = i; }

samostatné cvičení (přidání eventu)

Přidejte do stránky editace tlačítko "Storno", kterým se uživatel vrátí na seznam knih bez provedení změn.

samostatné cvičení (lokalizace)

Proveďte lokalizaci do angličtiny a důslednou internacionalizaci. Statické texty nahraďte JSP tagy

 
<%@ taglib prefix="f" uri="http://java.sun.com/jsp/jstl/fmt" %>
...
 <f:message key="klic.textu" />