Ako nakonfigurovať Jetty v Java kóde

Ú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 targete 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 ServletContextu 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 adresou http://localhost:8080/books, ale už nie s http://localhost:8080/books/orders.
  • hľadanie najdlhšieho sufixu. Špecifikácia sa musí končiť hviezdičkou. Sufix /books/* má zhodu s http://localhost:8080/books/ aj s http://localhost:8080/books/orders
  • hľadanie najdlhšieho prefixu. Špecifikácia musí začínať hviezdičkou. Sufix *.do má zhodu s http://localhost:8080/books.do aj s http://localhost:8080/books/orders.do
  • štandardné správanie. V tomto prípade má / zhodu s ľubovoľnou adresou.

Implementácia pomocou ServletHoldera

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 Servera.

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 Contextu

Ak v našej aplikácii potrebujeme podporu sessionov, môžeme použiť triedu Context ako náhradu ServletHandlera. 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 WebAppContextu

Ď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 ServletContextu 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 WebAppContextu. 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 WebAppProvidera, 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 WebAppProvidera — teda poskytovateľ údajov o kontextoch
  • inštanciu ContextHandlerColllectionu, 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

Odkazy

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *