Úvod
Servletový kontajner Jetty má oproti ostatným riešeniam výhodu v ľahkom embeddovaní, čiže použití ako súčasti inej aplikácie. To zároveň znamená, že ho možno veľmi jednoducho nakonfigurovať v Java kóde a spúšťať priamo z našich aplikácií. Veď koniec koncov, jeden zo sloganov je:
Nenasadzujte aplikácie do Jetty — nasaďte Jetty do aplikácie!
To sa mi napríklad osvedčilo pri demonštrovaní a školení rôznych webových frameworkov, kde nie je nutné predstavovať a vysvetľovať nasadzovanie webových aplikácií hneď na začiatku práce s nimi.
Poznámka k verzii
Predošlá verzia článku sa zameriavala na Jetty 6. Medzičasom prešiel projekt mnohými zmenami: predovšetkým bol zmigrovaný pod krídla nadácie Eclipse. Pre vývojárov je však dôležitejšie, že API prešlo dramatickým refactoringom (premenovania tried a metód, vyhadzovanie starých a napr. kompletné prekopanie dynamického deploymentu), čo zneplatňuje množstvo informácií z existujúcej dokumentácie: pozor na to.
Ako stiahnuť Jetty
Dnes (apríl 2013) je k dispozícii Jetty vo verzii 9, dostupná zadarmo pod kuratelou projektu Eclipse. ZIP celej distribúcie má cca 8,5 MB.
Ako použiť Jetty v našej aplikácii
V jednoduchých prípadoch stačí z celej inštalácie použiť tri archívy:
jetty-http-9.0.2.v20130417.jar
jetty-io-9.0.2.v20130417.jar
jetty-server-9.0.2.v20130417.jar
jetty-util-9.0.2.v20130417.jar
a štandardné servletové API:
servlet-api-3.0.jar
Jetty umožňuje implementovať triedy obsluhujúce HTTP požiadavky klienta viacerými spôsobmi. Od jednoduchého handlera až po plne funkčnú webovú aplikáciu konfigurovanú podľa špecifikácie.
Implementácia tried pomocou handlera
Najprimitívnejším spôsobom ako rýchlo postaviť obslužnú triedu je použitie handlera. Handler je konceptuálne veľmi podobný servletom. Poskytuje jedinú univerzálnu metódu handle()
a takmer všetko ponecháva na vás. Vôbec nerozlišuje HTTP metódy (GET, POST, …), ani mapovanie URL adries (handler vybavuje ľubovoľnú adresu). Výhoda oproti iným postupom spočíva v tom, že server je možné nakonfigurovať menším počtom riadkov.
public class HelloWorldHandler extends AbstractHandler {
public void handle(String target, Request baseRequest,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
PrintWriter out = response.getWriter();
out.println("Hello from Jetty Handler!");
baseRequest.setHandled(true);
}
}
Je to naozaj jednoduché: jediný zádrheľ spočíva v nutnosti označiť požiadavku Request
za vybavenú: v opačnom prípade bude putovať do prípadných ďalších handlerov.
V parametri target
dostaneme príponu URL adresy za názvom servera – čiže ak navštívime adresu http://localhost:8080/service/data
, v target
e bude /service/data
.
Celý server následne naštartujeme troma riadkami. Špecifikujeme port, nastavíme serveru implicitný handler a spustíme ho.
public static void main(String[] args) throws Exception {
Server server = new Server(8080);
server.setHandler(new HelloWorldHandler());
server.start();
}
Jetty sa naštartuje a do logu/konzoly vypíše:
2013-04-29 14:32:56.118:INFO:oejs.Server:main: jetty-9.0.2.v20130417
2013-04-29 14:32:56.160:INFO:oejs.ServerConnector:main: Started ServerConnector@6ad5934d{HTTP/1.1}{0.0.0.0:8080}
Existuje možnosť používať viacero handlerov naraz, pričom pridať do servera ich môžeme použitím metódy addHandler()
. Požiadavka na server potom bude putovať handlermi dovtedy, pokiaľ ju niekto neoznačí za obslúženú cez setHandled()
.
Implementácia tried pomocou jedného servletu
Ďalšou možnosťou je použitie plnoprávneho servletu. V tomto prípade však musíme dodať do projektu ďalší JAR:
jetty-servlet-9.0.2.v20130417.jar
Vytvorme si jednoduchý servlet:
public class DateServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter writer = resp.getWriter();
// vypíšeme aktuálny dátum a čas
writer.println(new Date());
}
}
Nasadenie servletov pre jednoduché prípady je možné urobiť pomocou preddefinovaného handlera podporujúceho servlety, čiže triedy ServletHandler
.
Server server = new Server(8080);
ServletHandler handler = new ServletHandler();
handler.addServletWithMapping(DateServlet.class, "/");
server.setHandler(handler);
server.start();
V metóde addServletWithMapping
špecifikujeme triedu servletu (o vytvorenie inštancie sa postará servlet) a navyše sme povinní uviesť aj koncovku URL adresy, ktorú bude obsluhovať tento servlet.
Toto primitívne použitie servletového handlera však nedáva k dispozícii ServletContext
u ani sessiony. (Pokus o vytvorenie session zlyhá.)
Mapovanie URL adries na servlety
Mapovanie je realizované podľa špecifikácie servletov a implementované v triede PathMap
. Pravidlá pre vyhodnocovanie sú nasledovné:
- presná zhoda. Sufix
/books
má zhodu s adresouhttp://localhost:8080/books
, ale už nie shttp://localhost:8080/books/orders
. - hľadanie najdlhšieho sufixu. Špecifikácia sa musí končiť hviezdičkou. Sufix
/books/*
má zhodu shttp://localhost:8080/books/
aj shttp://localhost:8080/books/orders
- hľadanie najdlhšieho prefixu. Špecifikácia musí začínať hviezdičkou.
Sufix
*.do
má zhodu shttp://localhost:8080/books.do
aj shttp://localhost:8080/books/orders.do
- štandardné správanie. V tomto prípade má
/
zhodu s ľubovoľnou adresou.
Implementácia pomocou ServletHolder
a
V predošlom prípade sme nemali možnosť nijak konfigurovať servlet v takom rozmedzí, ako to poskytuje web.xml
. ServletHandler
umožňuje len pridať servlet a namapovať ho na URL. Pokročilú konfiguráciu možno riešiť cez ServletHolder
, ktorým obalíme inštanciu servletu, a pridáme ho do Server
a.
ServletHandler handler = new ServletHandler();
ServletHolder servletHolder = new ServletHolder(DateServlet.class);
// ekvivalentné nastaveniu <init-param> vo web.xml
servletHolder.setInitParameter("locale", "sk");
// Ekvivalent init-on startup. Číslo špecifikuje poradie.
servletHolder.setInitOrder(0);
handler.addServletWithMapping(servletHolder, "/date");
server.setHandler(handler);
server.start();
Podobne ako v predošlých prípadoch nemáme k dispozícii ServletContext
ani sessiony.
Servlet môže potom preberať inicializačné parametre tradičným spôsobom:
public class DateServlet extends HttpServlet {
private static final String LOCALE_INIT_PARAM = "locale";
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter writer = resp.getWriter();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, getLocale());
writer.println(dateFormat.format(new Date()));
}
public Locale getLocale() {
// názov vytiahneme z <init-param>
String localeString = getInitParameter(LOCALE_INIT_PARAM);
if(localeString == null || localeString.isEmpty()) {
return Locale.US;
}
return new Locale(localeString);
}
}
Implementácia pomocou Context
u
Ak v našej aplikácii potrebujeme podporu sessionov, môžeme použiť triedu Context
ako náhradu ServletHandler
a. Popri tom umožňuje nastaviť tzv. context path, teda prefix pre URL adresy, od ktorého sa budú odvíjať URL cesty namapované pre servlety. Context
navyše dáva servletom k dispozícii inštanciu ServletContextu
, čo v predošlých prípadoch nebolo možné.
V tomto prípade tiež potrebujeme dodať do projektu ďalší JAR:
jetty-security-9.0.2.v20130417.jar
Kód bude vyzerať nasledovne:
Server server = new Server(8080);
// vytvoríme kontext a zapneme podporu pre sessions
ServletContextHandler context = new ServletContextHandler(server, "/date", ServletContextHandler.SESSIONS);
ServletHolder servletHolder = new ServletHolder(DateServlet.class);
servletHolder.setInitParameter("locale", "sk");
servletHolder.setInitOrder(0);
context.addServlet(servletHolder, "/date.txt");
server.start();
Server sa rozbehne s nepatrne odlišnou hláškou:
2013-04-29 17:30:56.974:INFO:oejs.Server:main: jetty-9.0.2.v20130417
2013-04-29 17:30:57.019:INFO:oejsh.ContextHandler:main: started o.e.j.s.ServletContextHandler@674482f7{/date,null,AVAILABLE}
2013-04-29 17:30:57.038:INFO:oejs.ServerConnector:main: Started ServerConnector@196a1a66{HTTP/1.1}{0.0.0.0:8080}
V tejto konfigurácii bude DateServlet
obsluhovať adresy s prefixom http://.../date
a so sufixom /date.txt
.
Nastavenie adresára so statickými stránkami
Bežná webová aplikácia spĺňajúca štandardy musí dodržiavať predpísanú adresárovú štruktúru. Základom je koreňový adresár webovej aplikácie, v ktorom sú statické stránky (ich URL je tvorená kontextovou cestou a názvom súboru) a podadresár WEB-INF
(obsahujúci triedy a knižnice).
Niektoré servlety vyžadujú korektné fungovanie tejto vlastnosti – príkladom je servlet v Spring MVC, ktorý používa koreňový adresár na vyhľadávanie JSP stránok.
Na tejto vlastnosti tiež závisí správne fungovanie metódy getResource()
zo špecifikácie servletov.
Povoliť toto správanie v rámci Contextu
je ľahké: na kontexte nastavíme túto cestu pomocou setResourceBase()
. Použiť možno buď absolútnu cestu alebo relatívnu cestu vzhľadom k aktuálnemu adresáru.
context.setResourceBase("web");
Ak máme resource base nastavenú na D:/books/web
a context path nastavená v kontexte je /books
, potom vyžiadanie getResource("/index.html")
vráti obsah stránky v adresári D:/books/web/index.html
.
Štandardný DefaultServlet
pre výpis adresárov a statické súbory.
Veľmi často chceme, aby Jetty dokázala vypisovať obsahy adresárov a obsluhovať požiadavky na statické súbory v koreňovom adresári aplikácie. Na tento účel je k dispozícii DefaultServlet
, ktorý toto všetko umožňuje.
Implementácia s použitím WebAppContext
u
Ďalším „levelom“ je použitie WebAppContextu
, ktorý rozširuje klasický Context
o možnosť konfigurovať webaplikáciu zo štandardného súboru web.xml
(v adresári WEB-INF
). Samozrejmosťou je zapnutie sessionov, prístup ku ServletContext
u a navyše podpora autentifikácie.
Klasický príklad, v ktorom definujeme jeden servlet a namapujeme ho na koreňovú URL kontextu:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>DateServlet</servlet-name>
<servlet-class>sk.upjs.ics.novotnyr.jetty.DateServlet</servlet-class>
<init-param>
<param-name>locale</param-name>
<param-value>sk</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>DateServlet</servlet-name>
<url-pattern>/date.txt</url-pattern>
</servlet-mapping>
</web-app>
Ak ho uložíme do súboru C:/Projects/jetty-test/web/WEB-INF/web.xml
, potom konfigurácia v kóde je nasledovná:
Server server = new Server(8080);
// prvý parameter udáva cestu k adresáru s WEB-INF
// druhý parameter udáva context path
WebAppContext context = new WebAppContext(server, "./web", "/date");
server.start();
Chýbajúcu triedu WebAppContext
nájdeme v JARe, ktorý pridáme do projektu. Okrem toho potrebujeme dodať ešte podporu pre XML parser. Oba JARy teda sú:
jetty-webapp-9.0.2.v20130417.jar
jetty-xml-9.0.2.v20130417.jar
Nezľaknime sa, že premenná context
sa nikde nepoužíva: vzťah medzi kontextom a serverom sa ustanoví vo vnútri konštruktora WebAppContext
u. Ten potrebuje tri parametre:
- objekt servera
- adresár, ktorý obsahuje podadresár
WEB-INF
. Alternatívne môže cesta obsahovať cestu k WAR súboru obsahujúcemu celú aplikáciu. - cestu s prefixom URL adresy.
V tejto ukážme sme teda nakonfigurovali dátumový servlet pomocou XML presne tak, ako v predošlej ukážke.
Bokom ešte poznamenajme, že v prípade takéhoto kontextu sa automaticky nainštaluje aj DefaultServlet
: navštívte napr. http://localhost:8080/date
a uvidíte výpis súborov.
Automatická aktualizácia kontextov
Z Tomcatu je známa možnosť automaticky znovunačítať kontext v prípade, že sa zmení niektorá z tried webaplikácie. V prastarých servletových kontajneroch bolo totiž nutné po každej zmene (kompilácii tried, zmenách nastavení) reštartnúť celý server, čo bolo pomerne nepohodlné.
Jetty umožňuje znovunačítavanie svojským spôsobom, ktorý je síce menej pohodlný ako v prípade Tomcatu, ale stále je to lepšie ako nič. Filozofia je jednoduchá: v Jetty sa sleduje súbor popisovača nasadenia a v prípade, že sa zmení jeho dátum a čas, kontext sa zahodí a nasadí nanovo.
Popisovač nasadenia v Jetty v podstate kopíruje Java syntax, ale zapisuje ju pomocou XML súboru.
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE Configure
PUBLIC "-//Mort Bay Consulting//DTD Configure//EN"
"http://jetty.mortbay.org/configure.dtd">
<Configure class="org.mortbay.jetty.handler.ContextHandler">
<Set name="contextPath">/books</Set>
<Set name="resourceBase">d:/projects/books/web</Set>
</Configure>
Všimnite si, že nastavujeme context path aj resource base podobným spôsobom, ako sme to robili v kóde. Rovnako si všimnime, že tento XML súbor nakonfiguruje nový ContextHandler
.
Súbor môžeme uložiť do adresára C:/Projects/jetty-test/etc
. Samotné znovunačítavanie sa deje pomocou triedy ContextDeployer
. Jeho API je však pomerne ťažkopádne.
Najprv dodáme do projektu JAR s pomocnými triedami pre deployment:
jetty-deploy-9.0.2.v20130417.jar
Najprv vytvoríme inštanciu WebAppProvider
a, a nastavíme mu adresár, v ktorom sa majú vyhľadávať zmeny v popisovačoch nastavení. (U nás ide o adresár etc
.). Prirodzene, adresár môže obsahovať viac popisovačov pre viacero kontextov. Ďalej nastavíme interval kontroly zmien popisovačov. Tri sekundy znamenajú, že pokiaľ sa zistí zmena (napr. úprava) popisovača, prebehne znovunasadenie celého kontextu.
Následne potrebujeme vytvoriť zoznam kontextových handlerov (ContextHandlerCollection
). Tie budú obsahovať jednotlivé kontexty nasadené na základe popisovačov v XML súboroch. U nás máme len jeden deskriptor, čo znamená, že do tohto zoznamu sa o chvíľu nasadí jediný kontextový handler.
Celú správu nasadenia rieši DeploymentManager
. Ten potrebuje poznať:
- inštanciu
WebAppProvider
a — teda poskytovateľ údajov o kontextoch - inštanciu
ContextHandlerColllection
u, do ktorej sa nasadia jednotlivé kontexty.
Objekt deployment managera potom pridáme do servera a sme hotoví.
Server server = new Server(8080);
WebAppProvider webAppProvider = new WebAppProvider();
webAppProvider.setMonitoredDirName("./etc");
webAppProvider.setScanInterval(3);
ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
DeploymentManager deploymentManager = new DeploymentManager();
deploymentManager.addAppProvider(webAppProvider);
deploymentManager.setContexts(contextHandlers);
server.addBean(deploymentManager);
server.start();
Porovnanie prístupov
- Handler
- najjednoduchší prístup
- mimo špecifikácie servletov
- len primitívne veci
- žiadne mapovanie na URL adresy
Servlet Handler
- podporuje servlety
- žiadne sessiony
- žiaden
ServletContext
- žiadna konfigurácia parametrov
Servlet Holder
- obalením servletu a pridaním do Servlet Handlera umožňuje konfigurovať servlet
- žiadne sessiony
- žiaden
ServletContext
ServletContextHandler
- základná verzia kontextu pre webovú aplikáciu
- umožňuje podrobnejšie mapovanie URL na servlety
WebAppContext
- ako
Context
, možnosť konfigurovať z XML
- ako
Odkazy
- Embedding Jetty — stránka v Jetty Wiki s ďalšími príkladmi
- Jetty Deployment Architecture