Přejít k navigační liště

Zdroják » Různé » Java na webovém serveru: Vlastní JSP značky a servlety

Java na webovém serveru: Vlastní JSP značky a servlety

Články Různé

Po minulém teoretičtějším dílu seriálu budeme dnes zase trochu programovat. Naučíme se, jak vytvářet vlastní JSP značky a funkce, které nám ušetří psaní a pomáhají vytvářet znovupoužitelný kód. A ukážeme si, jak vytvořit jednoduchý servlet, který klientům zpřístupní fotky z externího adresáře.

Dosud jsme v naší aplikaci používali jen standardní JSP značky, např. <fmt:message/> pro vložení lokalizovaného textu, <c:choose/> pro větvení nebo <jsp:include/> pro vkládání stránek.

Java nám ale nabízí možnost definovat si vlastní značky a funkce – vytvořit si tak v podstatě vlastní jazyk na míru a ušetřit si práci díky znovupoužitelnému kódu.

Jelikož se opět budeme věnovat vývoji naší aplikace Nekuřák.net, stáhneme si aktuální zdrojové kódy z Mercurialu:

$ hg pull
$ hg up "9. díl"

Případně si je můžete stáhnout jako bzip2 archiv přes web.

Píšeme vlastní JSP značky

Vlastní značky si můžeme definovat pomocí tzv. Tag File, což je v podstatě obyčejný soubor s příponou .tag, který uvnitř obsahuje nám už dobře známou JSP XML syntaxi – pouze je potřeba uvnitř něj definovat, jaké atributy naše značka bude mít. K tomu slouží tato direktiva:

<jsp:directive.attribute name="" type="" required=""/>

Název atributu uvádíme jako name, do type zadáme datový typ – třídu – např. java.lang.String nebo třeba nějakou naši vlastní třídu, pomocí required nastavíme povinnost nebo nepovinnost daného atributu.

Ukažme si vlastní JSP značky raději hned na příkladu. V naší aplikaci budeme zobrazovat fotky podniků (hospod). Každý podnik může mít více fotek a budeme je zobrazovat pomocí javascriptové prohlížečky (založené na knihovně bxSlider) vybavené šipkami na přepínání fotek. Jelikož stejnou prohlížečku budeme mít jak na výpisu podniků, tak na stránce s detailem jednoho podniku, bylo by dost hloupé tento kód (HTML + JavaScript) kopírovat na více míst. Proto si vytvoříme zvláštní JSP značku díky níž tento kód zapoudříme do „komponenty“, kterou následně můžeme vkládat na libovolnou stránku.

Definice naší JSP značky vypadá následovně (nachází se v souboru fotkyPodniku.tag):

<?xml version="1.0" encoding="UTF-8"?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
      xmlns:c="http://java.sun.com/jsp/jstl/core"
      xmlns:fmt="http://java.sun.com/jsp/jstl/fmt"
      xmlns:fn="http://java.sun.com/jsp/jstl/functions"
      xmlns:nkfn="/WEB-INF/nekurakFunkce"
      version="2.0">

    <jsp:directive.attribute name="podnik" type="cz.frantovo.nekurak.dto.Podnik" required="true"/>

    <div id="fotkyPodniku${podnik.id}">
    <c:forEach var="fotka" items="${podnik.fotky}">
        <p>
        <a href="${nkfn:fotka(fotka.id, false)}">
            <img src="${nkfn:fotka(fotka.id, true)}" alt="fotka" title="${fn:escapeXml(fotka.popis)}"/>
        </a>
        </p>
    </c:forEach>
    <p><img src="grafika/fotkaPodnikuZadne.png" alt="žádné další fotografie"/></p>
    </div>

    <c:if test="${nkfn:maFotky(podnik)}">
    <script type="text/javascript">
    fotkyPodniku.aktivuj(${podnik.id});
    </script>
    </c:if>
</jsp:root>

Značka se jmenuje fotkyPodniku (podle názvu souboru) a má jediný parametr – vyžaduje objekt třídy Podnik (i v JSP hrají roli datové typy a provádí se kontrola).

Do stránky s výpisem podniků vložíme prohlížečku fotek pomocí následujícího kódu (viz uvod.jsp):

<nk:fotkyPodniku podnik="${p}"/>

Java JSP

Jak vidíte, fotkyPodniku se nachází ve jmenném prostoru nk, ten tu není sám od sebe a musíme si ho nejprve „importovat“ (viz níže).

Ve svých JSP značkách se nemusíme omezovat jen na atributy, můžeme jim předávat parametry i pomocí těla elementu – příklad:

<x:mojeZnacka>
    <div>libovolné XML</div>
    text a další <br/> elementy
</x:mojeZnacka>

V definici takové značky pak zpracujeme toto tělo elementu pomocí <jsp:doBody/>. Pomocí atributů tedy předáváme JSP značkám objekty a pomocí těla předáváme libovolná XML nebo textová data.

Soubory Tag File jsou jednodušším a podle mého příjemnějším způsobem definice vlastních značek. Další možností je napsat definici značky v jazyce Java jako třídu implementující rozhraní javax.serlvet.jsp.tagext.JspTag.

Píšeme vlastní funkce

Pamatujete si ještě na funkci escapeXml() z druhého dílu našeho seriálu?

<abbr title="${fn:escapeXml(param.parametr1)}">„escapovaný“</abbr>

Tak podobné funkce si můžeme definovat vlastní a pak je používat ve svých JSP stránkách. A vůbec to není těžké. Funkci implementujeme v libovolné třídě jako veřejnou statickou metodu ( public static). Případně ji ani nemusíme implementovat a pouze si vybereme nějakou již existující. Abychom mohli funkci ve svých JSP stránkách používat, musíme si ji přidat do tzv. Tag Library Descriptor souboru (TLD).

Opět bude lepší ukázat si vše na příkladu. V předchozí kapitole jsme vytvořili prohlížečku fotek pomocí JSP značky. Aby tato prohlížečka měla co zobrazovat, musí znát URL daných fotek. V datovém modelu naší aplikace máme fotku identifikovanou pouze pomocí číselného ID a předpokládáme, že název souboru s fotkou bude tvořen tímto číslem a příponou a že náhledy k fotkám budou ve zvláštním adresáři. Napíšeme si tedy funkci, která vrací relativní URL dané fotky na základě jejího číselného ID a toho, zda chceme náhled nebo plné rozlišení.

Tuto funkci jsme definovali ve třídě cz.frantovo.nekurak.web.FunkceEL jako statickou metodu fotka:

public static String fotka(int id, boolean nahled) {
    String prostredek = nahled ? Fotky.PODADRESAR_NAHLED : Fotky.PODADRESAR_ORIGINAL;
    return SERVLET + "/" + prostredek + "/" + id + "." + Fotky.PRIPONA;
}

A v souboru nekurakFunkce.tld (Tag Library Descriptor) se na ni odkážeme:

<function>
    <name>fotka</name>
    <description>Sestaví URL na fotku s daným ID.</description>
    <function-class>cz.frantovo.nekurak.web.FunkceEL</function-class>
    <function-signature>java.lang.String fotka(int, boolean)</function-signature>
</function>

Java JSP

V .tld  souboru je důležité definovat jeho URI. V našem případě <uri>/WEB-INF/nekurakFunkce</uri>, což je identifikátor, na který se budeme odkazovat v JSP stránkách při importování jmenných prostorů.

Import XML jmenných prostorů v JSP

Abychom mohli v JSP stránkách používat své vlastní značky nebo funkce, musíme si „importovat“ příslušné XML jmenné prostory.

To se dělá v kořenovém elementu JSP stránky ( <jsp:root/>), podobně jako když jsme si importovali jmenné prostory ze standardní knihovny.

<?xml version="1.0" encoding="UTF-8"?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
      xmlns:c="http://java.sun.com/jsp/jstl/core"
      xmlns:fmt="http://java.sun.com/jsp/jstl/fmt"
      xmlns:fn="http://java.sun.com/jsp/jstl/functions"
      xmlns:nk="urn:jsptagdir:/WEB-INF/tags/nekurak"
      xmlns:nkfn="/WEB-INF/nekurakFunkce"
      version="2.0">

V případě funkcí a značek definovaných v TLD souboru se odkazujeme na URI uvedené v tomto souboru ( xmlns:nkfn="/WEB-INF/nekurakFunkce"). A v případě značek definovaných pomocí Tag File souborů se odkazujeme na adresář s těmito soubory ( xmlns:nk="urn:jsptagdir:/WEB-INF/tags/nekurak"). Tento adresář se musí nacházet v /WEB-INF/tags/. Takto definované značky mají jméno podle souboru, ve kterém se nachází.

Předponu jmenného prostoru ( nk, nkfn) si můžeme zvolit libovolnou. Vhodné ale je volit krátké názvy a vždy si stejné jmenné prostory označovat stejně.

Vytváříme servlety

Už dříve jsme si říkali, že servlet je javovská třída, potomek javax.servlet.http.HttpServlet, která se stará o vyřizování HTTP požadavků. Dosud jsme si vystačili s JSP stránkami, dnes se konečně podíváme, jak implementovat jednoduchý servlet přímo (bez JSP).

Servlet pro zpřístupnění fotek

Vždycky je vhodné oddělit aplikaci a data – oceníme to nejen při instalaci nových verzí aplikace, ale třeba i při zálohování nebo při obnovování dat po výpadku. Např. v unixových operačních systémech se aplikace nacházejí v adresáři /usr/bin/  – zatímco jejich data jsou typicky v adresáři /var/. Naše aplikace si ukládá data do relační databáze (záznamy o podnicích, uživatelích atd.), do téže databáze bychom mohli ukládat i fotografie podniků, nicméně raději jsem zvolil konservativnější přístup – fotografie budou uloženy jako normální soubory na disku. Jenže kam s nimi? Obrázky tvořící design stránky, jako jsou různá pozadí nebo vlaječky států, můžeme umístit přímo do aplikace (nakonec budou ve .war archivu společně s JSP stránkami, HTML, javascriptem, styly atd.). Jenže kdybychom stejně ukládali fotky podniků, museli bychom při přidání každé fotky znovu kompilovat aplikaci a nasazovat ji na server. To by bylo dost nešikovné a navíc by fotky nemohli přidávat uživatelé.

Proto uložíme fotky podniků do zvláštního adresáře vně naší aplikace. V tomto případě je to adresář /var/www/nekurak.net/fotky/. Fotky v něm umístěné teď potřebujeme zpřístupnit přes HTTP klientům  – a k tomu použijeme právě servlet.

Náš servlet je tvořen třídou cz.frantovo.nekurak.servlet.Fotky a jeho implementace je následující:

public class Fotky extends HttpServlet {

    /** Název inicializačního parametru */
    private static final String INIT_ADRESAR = "adresar";
    /** Název podadresáře obsahujícího fotku v plném rozlišení */
    public static final String PODADRESAR_ORIGINAL = "original";
    /** Název podadresáře obsahujícího výchozí náhled fotky */
    public static final String PODADRESAR_NAHLED = "nahled";
    public static final String PRIPONA = "jpg";
    private static final String LOMITKO = File.separator;
    /** Regulární výraz */
    private static final String VZOR_CESTY = "^" + LOMITKO + "(" + PODADRESAR_ORIGINAL + "|" + PODADRESAR_NAHLED + ")" + LOMITKO + "\d+\." + PRIPONA + "$";
    private static final String MIME_TYP = "image/jpeg";
    private File adresar;
    private static final Logger log = Logger.getLogger(Fotky.class.getSimpleName());

    @Override
    public void init() throws ServletException {
    super.init();
    String initAdresar = getServletConfig().getInitParameter(INIT_ADRESAR);
    adresar = new File(initAdresar);
    if (adresar.isDirectory()) {
        log.log(Level.INFO, "Servlet „Fotka“ byl úspěšně inicializován.");
        log.log(Level.INFO, "Adresář s fotkami: " + initAdresar);
        log.log(Level.INFO, "RegExp cesty: " + VZOR_CESTY);
    } else {
        throw new ServletException("Servlet „Fotka“ se nepodařilo inicializovat. Cesta: " + initAdresar);
    }
    }

    /**
     * @param pozadavek pouze GET (není důvod podporovat POST)
     * @param odpoved odešleme fotku s MIME typem podle konstanty, délkou a datem podle souboru.
     * @throws ServletException pokud je požadovaná cesta chybná (nevyhovuje vzoru)
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest pozadavek, HttpServletResponse odpoved) throws ServletException, IOException {

    String cesta = zkontrolujParametr(pozadavek.getPathInfo());
    File soubor = new File(adresar, cesta);

    if (soubor.isFile() && soubor.canRead()) {

        if (soubor.lastModified() > pozadavek.getDateHeader("If-Modified-Since")) {
        /** Soubor se změnil nebo ho klient ještě nemá načtený. */
        odpoved.setContentType(MIME_TYP);
        odpoved.setContentLength((int) soubor.length());
        odpoved.setDateHeader("Last-Modified", soubor.lastModified());

        ServletOutputStream vystup = odpoved.getOutputStream();
        InputStream vstup = new FileInputStream(soubor);

        try {
            byte[] zasobnik = new byte[1024];
            int bajtuNacteno;
            while ((bajtuNacteno = vstup.read(zasobnik)) != -1) {
            vystup.write(zasobnik, 0, bajtuNacteno);
            }
        } catch (Exception e) {
            throw new ServletException("Chyba při odesílání obrázku klientovi.", e);
        } finally {
            vstup.close();
            vystup.close();
        }
        } else {
        /** Soubor se od posledního načtení klientem nezměnil → není potřeba ho posílat znova. */
        odpoved.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
        }

    } else {
        /** Neexistující nebo nečitelný soubor → HTTP 404 chyba */
        odpoved.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
    }

    /**
     * @param cesta cesta požadovaná klientem: <code>request.getPathInfo()</code>
     * @throws ServletException pokud cesta nevyhovuje vzoru
     */
    private static String zkontrolujParametr(String cesta) throws ServletException {
    if (Pattern.matches(VZOR_CESTY, cesta)) {
        /** cesta je v pořádku → pokračujeme */
        return cesta;
    } else {
        /** Chybná cesta → HTTP 500 chyba */
        throw new ServletException("Chybná cesta k obrázku: " + cesta);
    }
    }
}

V metodě init(), která se provádí při vytvoření servletu, si načteme inicializační parametr adresar, který říká, v jakém adresáři se nacházejí fotky (to proto, aby cesta /var/www/nekurak.net/fotky/ nebyla zadaná natvrdo ve zdrojovém kódu a bylo ji možné změnit i bez kompilace). A otestujeme, zda tento adresář existuje – pokud by neexistoval, dojde k chybě už při nasazení aplikace a ne až při jejím běhu.

Nejdůležitější metodou každého servletu je doGet() (případně doPost() a další), která obsluhuje HTTP požadavky klientů. Tyto metody mají dva parametry – objekty, které představují HTTP požadavek klienta a odpověď, kterou mu pošleme.

Náš servlet Fotky si z požadavku načte požadovanou cestu a zkontroluje, zda je platná (soubory, které neodpovídají přesnému vzoru nebudeme poskytovat, přestože by se nacházely v daném adresáři). Pokud soubor s obrázkem existuje a je čitelný, pošleme ho klientovi.

Využijeme vlastností HTTP protokolu a budeme se chovat úsporně: pokud uživatel požaduje určitý obrázek podruhé, jeho prohlížeč nám posílá hlavičku If-Modified-Since. Na serveru se podíváme, zda soubor nebyl od té doby upraven a pokud nebyl (což bude nejčastější případ), pošleme klientovi strohou odpověď HTTP 304 Not modified a vlastní data si vezme prohlížeč ze své mezipaměti (nemusí se zbytečně přenášet podruhé po síti).

K vyzkoušení této funkcionality se nejlépe hodí doplněk pro Firefox Firebug a unixový příkaz touch, kterým nastavíme datum souboru (např. touch 1.jpg). Můžete si tak vyzkoušet, že prohlížeč stahuje soubory jen pokud se na serveru změnily.

V tomto servletu jsme mohli použít GET parametry pro ID fotky – a URL mohlo vypadat např. takto: /fotky?id=1&nahled=true. Přesto jsem raději zvolil URL, které odpovídá adresářové struktuře. Má to jednu důležitou výhodu  – později můžeme statický obsah (fotky) servírovat klientům pomocí jiného programu (např. apache nebo nginx) a tato data nemusí vůbec proudit přes náš javovský aplikační server. Taková konfigurace má smysl hlavně u hodně zatížených aplikací, které obsluhují velké množství požadavků (naší aplikace se to asi nikdy týkat nebude).

Nastavení ve web.xml

Servlet bychom měli naprogramovaný, ovšem zatím se jedná o pouhou třídu, která sama od sebe nic nedělá. Servlety, které mají být v aplikaci činné, musíme uvést v konfiguračním souboru aplikace ( web.xml) a následně je namapovat na určité URL.

Následujícím zápisem definujeme servlet. Nejdůležitější je jeho název ( servlet-name) a implementační třída ( servlet-class). Dále zde uvádíme inicializační parametry, se kterými pak můžeme pracovat v kódu servletu, a počet instancí, které se mají vytvořit při startu.

<servlet>
        <description>
        Servlet zpřístupňující fotky umístěné ve zvláštním adresáři
        (data oddělená od aplikace).
    </description>
        <servlet-name>fotky</servlet-name>
        <servlet-class>cz.frantovo.nekurak.servlet.Fotky</servlet-class>
        <init-param>
            <description>
        Adresář na disku, který obsahuje fotky podniků.
        Musí existovat při startu aplikace.
        </description>
            <param-name>adresar</param-name>
            <param-value>/var/www/nekurak.net/fotky</param-value>
        </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

Poté servletu přiřadíme cestu, na které bude odpovídat požadavkům klientů. Tato cesta je relativní vůči kontextu naší aplikace (např. /nekurak.net-web/), nikoli ke kořeni celého webového serveru.

<servlet-mapping>
        <servlet-name>fotky</servlet-name>
        <url-pattern>/fotky/*</url-pattern>
</servlet-mapping>

V mapování se odkazujeme na název servletu (definovaný výše ve web.xml), nikoli na název třídy. Vzorů cest ( url-pattern) přiřazených servletu zde můžeme uvést libovolné množství.

Zaujaly vás možnosti Javy a chcete se dozvědět o tomto jazyce víc? Akademie Root nabízí školení Základy programovacího jazyka Java a Pokročilejší kurz jazyka Java, na nichž se naučíte, jak tento multiplatformní objektově orientovaný jazyk používat.

Mapování JSP stránky jako servletu

Jak už jsme si dříve říkali, i JSP stránka se nakonec přeloží na servlet a zkompiluje. Proto i JSP stránky můžeme mapovat jako servlety a přiřadit jim další URL.

Této vlastnosti jsme využili k namapování stránky poskytující data ve formátu Atom na URL http://nekurak.net/atom (resp. /atom).

<servlet>
        <servlet-name>atom</servlet-name>
        <jsp-file>/WEB-INF/atom/atom.jsp</jsp-file>
</servlet>
<servlet-mapping>
        <servlet-name>atom</servlet-name>
        <url-pattern>/atom/*</url-pattern>
</servlet-mapping>

Poznámka: všimněte si, že nepotřebujeme ani žádnou zvláštní knihovnu pro formát Atom (nebo RSS). Využijeme výhod toho, že JSP stránka publikuje XML a Atom/RSS jsou na XML založené. Stačí si vytvořit jednoduchou šablonu v JSP (a jako aplikační a datovou vrstvu použijeme ty, které už máme – přidáváme jen další způsob prezentace dat).

Mapování servletů pomocí anotací

Od verze 3.0 specifikace servletů je možné k jejich mapování a parametrizaci používat i anotace. Díky tomu není potřeba je konfigurovat ve web.xml.

@WebServlet(name = "fotky",
urlPatterns = {"/fotky/*"},
initParams = {
    @WebInitParam(name = "adresar", value = "/var/www/nekurak.net/fotky")
})
public class Fotky extends HttpServlet { … }

Pomocí anotací můžeme zadat i inicializační parametry servletu. Možná vás napadne, že v takovém případě bychom je mohli psát rovnou do dané třídy jako konstanty. Výhodou ale je, že parametry uvedené v anotaci můžeme „přebít“ parametry ve web.xml. Tudíž do anotací můžeme zadat výchozí hodnoty, které se použijí, pokud ve web.xml žádné specifikované nejsou.

Závěr

V dnešním díle jsme se naučili vytvářet vlastní JSP značky pomocí Tag File souborů a vlastní funkce, které můžeme používat uvnitř JSP stránek. Napsali jsme servlet pro zpřístupnění fotek ze zvláštního adresáře. A jako malý bonus jsme si ukázali jak v JSP generovat Atom, který je použitelný pro agregaci obsahu. Neměli bychom zapomínat, že tyto prostředky jsou skvelým pomocníkem a šetří nám práci, ale zároveň patří do prezentační vrstvy a podle toho se mají používat – obchodní logika by měla být v nižších vrstvách aplikace.

Odkazy

Komentáře

Subscribe
Upozornit na
guest
8 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Ladislav Thon

Jen drobnost, load-on-startup neznamená počet instancí servletu (ta je vždycky jedna), ale pořadí, v jakém se servlety inicializují.

v6ak

Třeba u Google App Engine, že?

Ladislav Thon

To je pravda, ale SingleThreadModel je deprecated a uživatel by v každém případě měl předpokládat, že servlet je jen jeden = musí být thread-safe (ideálně bezestavový – ač je to možná s podivem, obvykle to vůbec není problém –, jiná řešení velmi rychle vedou k výkonnostním problémům).

Guido

To je samozřejmě nesmysl – instance servletu je vždy jen jedna a pro každý request vytváří konejner nový thred. Viz specifikace.

Jedinou výjimkou, kdy existuje více instancí servletu, je, pokud je aplikace nasazená v distribuovaném prostředí – na více JVM. Ale i tak, v každém JVM je pouze jedna instance každého servletu. Opět viz specifikace.

BTW, kdybyste dělal SCWCD, tak byste to věděl. Rovněž mohu doporučit skvělou knížku z edice Head First, kde je toto několikrát, s odkazem na specifikaci, zdůrazňováno.

Guido

Specifikace jest dostudována. Neříká se v ní, jakým způsobem se má SingleThreadModel implementovat – je to vendor specific. Navíc je STM od verze 2.4 deprecated a ani předtím nebylo doporučeno ho používat.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.