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

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.
Seriál: Java na webovém serveru (16 dílů)
- Java na serveru: úvod 8. 1. 2010
- Java na webovém serveru: první web 15. 1. 2010
- Java na webovém serveru: práce s databází 29. 1. 2010
- Java na webovém serveru: práce s databází II 12. 2. 2010
- Java na webovém serveru: lokalizace a formátování 19. 2. 2010
- Java na webovém serveru: autorizace a autentizace 26. 2. 2010
- Java na webovém serveru: autorizace a autentizace II 5. 3. 2010
- Java na webovém serveru: porovnání Javy a PHP 10. 3. 2010
- Java na webovém serveru: Vlastní JSP značky a servlety 17. 3. 2010
- Java na webovém serveru: posílání e-mailů a CAPTCHA 24. 3. 2010
- Java na webovém serveru: píšeme REST API 7. 4. 2010
- Java na webovém serveru: SOAP webové služby 14. 4. 2010
- Java na webovém serveru: hlasování a grafy v SVG 28. 4. 2010
- Java na webovém serveru: Komentáře a integrace s Texy 9. 6. 2010
- Java na webovém serveru: AJAX formuláře 23. 6. 2010
- Java na webovém serveru: implementujeme Jabber 30. 6. 2010
Nálepky:
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}"/>
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>
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
- Developing JSP Custom Tags – starší způsob psaní JSP značek v Javě.
- Designing JSP Custom Tag Libraries – totéž téma, od O’Reilly.
- JSPTags.com – katalog různých volně dostupných knihoven JSP značek.
- Annotation Type WebServlet – JavaDoc anotace @WebServlet.
- Status Code Definitions – HTTP stavové kódy.
- Header Field Definitions – hlavičky používané v HTTP protokolu.
- bxSlider – pěkný plugin do jQuery.
Jen drobnost,
load-on-startup
neznamená počet instancí servletu (ta je vždycky jedna), ale pořadí, v jakém se servlety inicializují.Díky za opravu, je to tak.
BTW: někdy může existovat více instancí jednoho serveru – záleží na implementaci AS a použití rozhraní SingleThreadedModel.
Třeba u Google App Engine, že?
sorry :-)
serveru → servletu
(to je tak, když člověk už myslí na druhou část věty)
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).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.
Doporučuji příslušnou specifikaci dostudovat. Sice se tam píše, že:
„For a servlet not hosted in a distributed environment (the default), the servlet container must use only one instance per servlet declaration.“
ale zároveň:
„However, for a servlet implementing the SingleThreadModel interface, the servlet container may instantiate multiple instances to handle a heavy request load“
Nicméně asi nemá cenu se o tom přít – užitečnost vícenásobných instancí téhož servletu je sporná (asi taky proto je SingleThreadModel zavržený) a navíc mi přijde celkem přirozené psát servlety tak, aby byly vláknově bezpečné – co jde, tak bezstavově a pokud jsou potřeba nějaké proměnné (třeba kolekce) na úrovni třídy, tak počítat s tím, že k nim může přistupovat víc vláken současně.
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.