Moje odkazy
Obsah článku:
vydáno: 16. 3. 2010 17:18, aktualizováno: 21. 9. 2020 16:08
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.
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
.
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 znakčy. 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ů.
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ě.
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ánkámi,
dnes se konečně podíváme, jak implementovat jednoduchý servlet přímo (bez JSP).
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í nástroje pro vývojáře, které jsou ve většině prohlížečů dostupné pod klávesovou zkratkou F12,
a unixový příkaz touch, kterým nastavíme datum souboru (např. touch 1.jpg
).
Můžete si tak ověřit, ž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).
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í.
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 fotmá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).
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.
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.
Tento článek zatím nikdo nekomentoval