Aplikacje WEB


Aplikacje WEB stanowią istotny element współczesnych systemów informatycznych. Umożliwiają one dostęp i interakcję do rozlicznych serwisów i systemów informatycznych w środowisku rozproszonym, sieciowym, przy wykorzystaniu prostych i uniwersalnych protokołów. Temat jest niezwykle rozległy, mógłby stanowić właściwie treść odrębnej, dużej monografii. Tutaj przedstawimy wprowadzenie do tej tematyki, starając sie przy tym podać podstawy -  niezbędne i przydatne do dalszego studiowania technologii programowania aplikacji WEB.

1. Technologie programowania aplikacji WEB

Korzystając z Internetu na co dzień stykamy się z aplikacjami WEB.

Aplikacje WEB stanowią zestaw komponentów programistycznych, działających po stronie serwera i dynamicznie reagujących na zlecenia ze strony programów  klienckich, zgłaszane za pośrednictwem protokołu sieciowego (w szczególności HTTP).


Aplikacje WEB mają szerokie zastosowania, głównie w Internecie i intranecie, obejmujące m.in.:
Wspomniane programy klienckie są zazwyczaj przeglądarkami stron WWW (ze względu na uniwersalny, powszechnie dostępny charakter tego interfejsu), ale mogą również obejmować takie komponenty jak aplety, czy nawet stanowić wyspecjalizowane "wolnostojące" programy. Takie programy i takie aplety bedą wtedy stanowić "kliencką" częsć aplikacji WEB. W każdym przypadku jednak jądrem aplikacji są komponenty działające po stronie serwera.

Środowisko Javy doskonale nadaje się do tworzenia aplikacji WEB, bowiem:
Przykładowy schemat działania aplikacji WEB pokazuje rysunek.

Rys. Działanie aplikacji WEB

r

1 - zlecenie klienta
2 - odebrane przez kompoenn WEB
3 - odowłanie do klas realizujących logikę
4,5 - sięgnięcie po dane i ich odbiór
6 -  komponent WEB odbira wyniki pretwarzania,
7,8 - przekazanie wyników klientowi

Podstawową technologią, służącą w Javie do tworzenia aplikacji WEB jest technologia serwletów (Java Servlet Technology).

Serwlet jest klasą Javy, rozszerzającą funkcjonalność serwerów w przetwarzaniu zleceń programów klienckich.
Przy programowaniu serwletów wykorzystujemy klasy z pakietu  javax.servlet - stanowiącego interfejs programistyczny Servlet API. Klasy te i ich metody dostarczają różnorodnych środków odbierania i reagowania na zlecenia klientów oraz przesyłania im wyników (odpowiedzi).


Serwlet może (a nawet powinien) pelnić rolę kontrolera w aplikacjach WEB budowanych w oparciu o paradygmat "Model-View-Controller" i zajmować się odbieraniem zleceń, zarządzaniem nimi, przekazywaniem ich innym komponentom programowym, odbieraniem od nich wyników i przekazywaniem modułom prezentacyjnym. Ważne jest przy tym, by w strukturze aplikacji WEB występowała wyrażna separacja pomiędzy danymi, a ich prezentacją.
Technologia serwletów - jako najwcześniejsza technologia programowania komponentów Web w Javie - nie wymusza jednak spełnienia tego wymagania. Nader często serwlety używane były (i są) do generowania dynamicznych stron WWW w taki sposób, że kod odpowiedzialny za prezentację (tworzenie wyglądu strony) zmieszany jest z kodem, odpowiedzialnyn za logikę przetwarzania danych. Niejednokrotnie takiego pomieszania - w przypadku "czystych" serwletów trudno jest uniknąć.

Technologia Java Server Pages (JSP) po części odpowiada na to wyzwanie, jednocześnie nieco upraszczając tworzenie cześci aplikacji webowych, szczególnie tych odpowiedzialnych za wygląd.  Traktowana najpierw jako głównie swoisty język skryptowy (strona JSP zawiera statyczną treść np. w języku HTML oraz treść dynamiczną, generowaną przez elementy JSP), obecnie akcentuje deklaratywne znaczniki, które - w trakcie interpretacji strony - "wywołują" określone procedury.  Wprowadzeniu standardowej biblioteki znaczników (Java Server Pages Standard Tag Library) dało twórcom aplikacji WEB uniwersalne i stosunkowo łatwe (przede wszystkim dla tych co nie znają Javy)  sposoby uzyskiwania różnorodnej funckjonalności.
W pewnym sensie JSP jest "tylko" nakładką na serwlety: istotnie strony JSP są - przy pierwszym do nich odwołaniu - tłumaczone na serwlety, a te ostatnie sa kompilowane "w locie" i wykonywane przez serwer aplikacji.

I wreszcie, swoistym uzupełnieniem do "czystych" serwletów i JSP jest technologia Java Server Faces . przeznaczona do elastycznego generowania GUI aplikacji WEB po stronie serwera, ale przede wszystkim umożliwiająca lepszą separację modeli i widoków, której - mimo wszystko - JSP do końca nie zapewnia.

Relacje pomiędzy podstawowymi technologiami Javy, służącymi tworzeniu aplikacji WEB przedstawia rysunek.

r

Nowe trendy w tworzeniu aplikacji WEB dotykają przede wszystkim ułatwionych sposobów programowania (tutaj wymienić można środowisko Grails = Groovy on Rails), a także zmian po stronie programowania klientów, związanych z zastosowaniem rozbudowanych bibliotek JavaScript i technologii (czy raczej podejścia) AJAX. AJAX (Asynchrounous Java Script and XML) polega na  asynchronicznym (nie wymagającym przeładowania stron) odbierania przez klienta danych od serwera, co umozliwia tworzenie prawdziwie interaktywnych stron WEB.

W niniejszym rozdziale omówimy zagadnienia związane z konstrukcją, rozwijaniem i wdrażeniem aplikacji WEB, skupiając uwagę na serwletach. Niestety, ograniczona objętość nie pozwala na pełniejsze przedstawienie technologii JSP i Java Server Faces. Ta pierwsza jest zresztą technologią raczej "nieprogramistyczną". 
Co więcej, w przypadku obu tych technologii mamy wyraźne odniesienia do serwletów, które stanowią dla nich bazę. Dlatego opanowanie programowania aplikacji WEB głównie pod kątem serwletów nie jest stratą czasu: będzie sprzyjać łatwiejszemu i pełniejszemu rozumieniu rozszerzających mechanizmów JSP i JSF. O nich powiemy tylko kilka słów w końcowym podpunkcie.
Również AJAX dobrze wpisuje się w zastosowania serwletów jako podstawoego "budulaca"  modułów dzialających po stronie serwera.
 

2. Wdrażanie i uruchamianie aplikacji WEB

Aplikacje WEB wykonywane są w środowisku serwerów aplikacji. Serwery zarządzają aplikacjami i ich wykonaniem (np. w szczególności ładowaniem i wywoływaniem serwletów, przekazywaniem zleceń, polityką bezpieczeństwa itp.). Aby to robić, serwery muszą mieć dostęp do informacji o aplikacji, jej umiejscowieniu i pewnych właściwościach.   Nie wystarczy zatem samo oprogramowanie i skompilowanie programów (klas), trzeba jeszcze aplikację skonfigurować, distarczyć odpowiednich informacji o niej oraz odpowiednio umiejscowić. Nazywa się to wdrożeniem (deployment) aplikacji.

Istnieje wiele różnych serwerów aplikacji (np. IBM WebSphere, JBoss, BEA WebLogic),  które wraz z obsługą aplikacji WEB świadczą usługi typu "middleware" w oparciu o platformę Enterprise Java Beans (w ramach tych serwerów aplikacji można wyróżnić serwer EJB i serwer WEB).  Istnieją też samodzielne serwery WEB, które zajmują się przede wszystkim obsługą aplikacji WEB (np. Tomcat), ew. dodatkowo umiejąc łączyć sie z serwerami EJB.
Proces wdrażania aplikacji WEB na wszystkich tych serwerach jest ideowo podobny i (ogólnie) polega na:
Zacznijmy od końca. Każda aplikacja WEB ma swój kontekst  swoiste środowisko izolowane od innych aplikacji działających na danym serwerze. Kontekst umożliwia m.in wymianę i dzielenia informacji pomiędzy komponentami tej samej aplikacji (np. róznymi serwletami, które stanowia jej komponenty), jak również komunikowanie się aplikacji z serwerem. Serwer uruchamia aplikację w jej kontekście (tworzy dla niej kontekst) i identyfikuje ją poprzez nazwę kontekstu (ścieżkę kontekstu). Aplikacja jest reprezentowana przez jej kontekst. Ścieżka kontekstu służy także do wywoływania aplikacji lub jej części z poziomu programów klienckich. Pomiędzy ścieżką kontekstu a realnym umiejscowieniem  struktury katalogowej aplikacji na fizycznej maszynie musi być ustanowiona  odpowiedniość (inaczej serwer nie odnalazłby komponentów aplikacji).

Struktura katalogowa aplikacji jest ściśle określona. Komponenty i deskryptory aplikacji muszą być ulokowane w następujący sposób:

r

Główny katalog aplikacji - MojaAp (1) zawiera katalog WEB-INF oraz może, ale nie musi zawierać strony JSP, a także dowolne komponenty typu dokumenty HTML, XML. pliki graficzne itp. (dostępne dla użytkownika).
Kluczową rolę spełnia podkatalog WEB-INF (2). Zawiera on:
Ta struktura katalogowa odwzorowywana jest na ścieżkę kontekstu przy wdrożeniu.

W środowisku serwera Tomcat (a będziemy posługiwać się i omawiać konkretne przykłady właśnie dla Tomcata)  istnieją trzy sposoby wdrożenia gotowej aplikacji, przygotowanej w pokazanej wyżej strukturze katalogowej:
Na starcie Tomcat  tworzy i uruchamia konteksty aplikacji,  zarejestrowanych w jeden z trzech w/w sposobów.

Jak już wspomniano, kluczową rolę dla aplikacji WEB odgrywa deskryptor wdrożenia - plik web.xml.
Jest to plik XML, którego elementy określają wdrożeniowe właściwości aplikacji.
Należą do nich:

Właściwości są definiowane za pomocą znaczników XML, okalających wartość właściwości. Znaczniki (otwierający i zamykający) wraz z umieszczonym pomiędzy nimi ciałem nazywane są elementami. Elementy mogą być zagnieżdżone. Np. następujący element XML:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

określa konfigurację sesji i zawiera element session-timeout, określający ile czasu nieaktywne połączenie ma zachowywać sesję (tu 30 minut). 

Jak każdy plik XML - deskryptor wdrożenia rozpoczynamy znacznikiem:

<?xml version="1.0" encoding=charset?>

gdzie jako charset podajemy konkretną stronę kodowa.

Następnie otwieramy główny element znacznikiem <web-app .... >,
Dla wersji 2.4 Servlet API w otwierającym znaczniku <web-app ... >   podajemy lokalizację definicji formatu i znaczenia elementów deskryptora - jak poniżej.
W głównym elemencie umieszczone są wszystkie inne elementy, opisujące własciwości aplikacji.
Wygląda to w następujący sposób:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-app_2_4.xsd">
   ...
   ...  inne elementy 
   ...
</web-app>

Uwaga: obecnie dostępna jest specyfikacja Servlet API 2.5, ale nie wnosi ona zbyt wielu zmian i interesujących dodatków, możemy więc spokojnie pzostać przy 2.4.

Warto zauważyć, że w niektórych przypadkach  np. prostych stron JSP deskryptor wdrożenia może zawierać tylko główny element (web-app), bez elementów zagnieżdżonych.
Dobrym zwyczajem jest jednak dostarczenie wtedy przynajmniej  krótkiej opisowej nazwy aplikacji (znacznik <display_name> ). Będzie ona widoczna w narzędziach zarządzania aplikacjami na serwerze, dzięki temu będziemy się mogli łatwo przekonać czy aplikacja działa, a także  kończyć i wznawiać jej działanie.
Mamy np. do dyspozycji managera aplikacji Tomcata, który wygląda tak jak na rys.

r

Podane ścieżki kontekstu jednoznacznie identyfikują aplikację, <display_name> daje dodatkowa informację.

No dobrze, stworzyliśmy deskryptor, mamy wymaganą strukturę katalogową aplikacji, stosując jeden z trzech omawianych wcześniej sposobów wdrożyliśmy ją w środowisku serwera. Jak ją teraz wywołać?

Aplikacja WEB jest wywoływana za pomocą jednego ze zleceń (requests) HTTP np. GET lub POST. Url, który poprzedza parametry lub strumień danych zlecenia ma formę:

Wywołanie aplikacji WEB
http://url_serwera/kontekst [ / {  strona_jsp |  mapowanie_nazwy_serwletu } ]

 [...] oznaczają opcjonalność,  { x | y } oznacza "jedno z"
Natomiast:


Jeżeli w odwołaniu podano tylko http://url_serwera/kontekst, to aplikacja zostanie uruchomiona w następujących przypadkach:
   <welcome-file-list>
     <welcome-file>plik1.jsp</welcome-file>
     <welcome-file>plik2.jsp</welcome-file>
     ...
   </welcome-file-list>

a w głównym katalogu aplikacji znajdują się któreś z tych plików, to zostanie wywołany pierwszy z listy, który jest w katalogu (jest to również sposób na dostarczenie użytkownikowi zwykłego pliku html, z którego albo mogą prowadzić linki do róznych części aplikacji, albo można dostarczyć form logowania, albo może on informować użytkownika, że zapomniał dodać do adresu zlecenia dodatkowej specyfikacji, np. zmapowanej nazwy serwletu i przez to nie może uruchomić aplikacji),
Zobaczmy przykłady wywołania aplikacji WEB w  środowisku Tomcat. Domyślna instalacja udostępnia serwer poprzez port 8080 lokalnego hosta: http://localhost:8080.
Wyobraźmy sobie teraz następujące przypadki:

A. Aplikacja, powiedzmy - "Sklep ogrodowy", napisana jako strona JSP (plik run.jsp), odwołująca się do klasy GardenShop.class (JavaBean) i mająca w pliku web.xml jedynie element <display-name> została wdrożona poprzez umieszczenie jej w podkatologu gs katalogu webapps:

<katalog_intalacyjny_Tomcata>
      webapps
          gs
            WEB-INF
                  classes
                      GardenShop.class
                  web.xml
             run.jsp

Możemy ją wtedy wywołać poprzez podanie w przeglądarce URL-a:

http://localhost:8080/gs/run.jsp


B. Aplikacja napisana jako strona JSP (w pliku index.jsp), odwołująca się do klasy GardenShop.class (JavaBean) i mająca w pliku web.xml jedynie element <display-name> została wdrożona poprzez umieszczenie jej w podkatalugu gs katalogu webapps:

<katalog_intalacyjny_Tomcata>
      webapps
          gs
            WEB-INF
                  classes
                      GardenShop.class
                  web.xml
             index.jsp

Możemy ją wtedy wywołać poprzez podanie w przeglądarce URL-a:

http://localhost:8080/gs


C. Aplikacja napisana jako strona JSP (w pliku run.jsp), odwołująca się do klasy GardenShop.class (JavaBean), mająca plik web.xml w następujacej postaci:
<?xml version="1.0"?>
<web-app version="2.4"
     xmlns="http://java.sun.com/xml/ns/j2ee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

   <display-name>Sklep ogrodniczy</display-name>

   <welcome-file-list>
     <welcome-file>run.jsp</welcome-file>
   </welcome-file-list>

</web-app>
została wdrożona poprzez umieszczenie jej w podkatalugu gs katalogu webapps:

<katalog_intalacyjny_Tomcata>
      webapps
          gs
            WEB-INF
                  classes
                      GardenShop.class
                   web.xml
             run.jsp

Możemy ją wtedy wywołać poprzez podanie w przeglądarce URL-a:

http://localhost:8080/gs



D. Aplikacja "Sklep ogrodowy" została napisana jako serwlet GardenShopServlet.class. W pliku web.xml określono, że mapowanie nazwy tego serwletu -  to /run. Aplikacja została wdrożona w następujący sposób (uwaga nie ma tu już żadnych plików JSP):

<katalog_intalacyjny_Tomcata>
      webapps
          gs
            WEB-INF
                  classes
                      GardenShopServlet.class
                   web.xml


Możemy ją wtedy wywołać poprzez podanie w przeglądarce URL-a:

http://localhost:8080/gs
/run

E. Aplikacja "Sklep ogrodowy" została napisana jako serwlet GardenShopServlet.class. W pliku web.xml określono, że mapowanie nazwy tego serwletu  to /*. Aplikacja została wdrożona w następujący sposób:

<katalog_intalacyjny_Tomcata>
      webapps
          gs
            WEB-INF
                  classes
                      GardenShopServlet.class
                   web.xml


Możemy ją wtedy wywołać poprzez podanie w przeglądarce dowolnych URLI o postaci:

http://localhost:8080/gs
/*

czyli np.
http://localhost:8080/gs
http://localhost:8080/gs/run
http://localhost:8080/gs/start
http://localhost:8080/gs/prosze_kwiatki


Proces wdrażania aplikacji nie jest prosty.  Początkującym wiele problemów sprawia zorientowanie się w nowych pojęciach (np. co to jest ten "kontekst"?), gąszczu katalogów, wymagań co do struktur, nazw. Omawiamy wdrażanie na początku, bowiem bez tej wiedzy nie sposób naprawdę zrozumieć działania aplikacji WEB, ani też - tym bardziej ćwiczyć ich tworzenia. Nieco abstrakcyjny na razie, syntetyzujący opis powinien dawać ogólną, można by powiedzieć, strukturalną orientację (ważniejszą, myślę, dla zrozumienia niż bardzo konkretne instrukcje "zrób to, potem to, potem to" - na jakichś szczególnych przypadkach).

Ale oczywiście takie konkretne przykłady są także niezbędne dla zrozumienia tego materiału. Zajmiemy się wiec teraz budową i wdrożeniem dwóch konkretnych prostych serwletów. Ich rola polega głównie na pokazaniu w jaki sposób tworzy się (najprostsze) deskryptory wdrożenia (web.xml) dla serwletów, sposobów wdrażania aplikacji WEB i relacji pomiędzy ścieżką kontekstu a fizyczną lokalizacją aplikacji na dysku.

Przykładowa aplilkacja WEB będzie składać się z dwóch (niezwiązanych ze sobą) serwletów. Są one nie tylko proste, ale właściwie bezużyteczne. Ich głównym zadaniem jest szczegółowe pokazanie sposobów wdrożenia aplikacji, sposobu odwoływania się z generowanych stron do zasobów aplikacji (takich jak pliki graficzne czy HTML), oraz związków pomiedzy ścieżką kontekstu a fizycznym katalogiem aplikacji.

Pierwszy serwlet, po wywołaniu z przeglądarki klienta generuje stronę HTML, która zostaje zwrócona do przegladarki. Tło wygenerowanej strony będzie plikiem graficznym o nazwie os2.jpg (musimy go jakoś umieścić w strukturze katalogów aplikacji i umieć sie do niego odnieść z programu), na stronie będzie też odnośnik prowadzący do jakiegoś pliku HTML (też znajdującego się w ramach naszej aplikacji), o nazwie powiedzmy Bye.html.
Serwlet zapiszemy w pliku Msg.java. Po kompilacji programu, zdefiniowaniu deskryptora wdrożenia (web.xml) i przygotowaniu zasobów (grafiki, HTML) powinniśmy mieć na dysku następującą strukturę katalogową (główny katalog aplikacji nazwiemy serwlety1).

serwlety1
    WEB-INF
         classes
             Msg.class
         web.xml
    images               <---- pliki graficzne umieszczamy w podkatalogu
          os2.jpg
    Bye.html
 
No, ale najpierw musimy napisać serwlet.
Już za chwilę, w dalszej części wykładu, zajmiemy sie dokładnie budową i działaniem serwletów; teraz dla zrozumienia działania przykładu wystarczy wiedzieć, że:
Oto kod.
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class Msg extends HttpServlet {

  // Początek HTML i właściwości <body> - tło, kolor tekstu i linków
  private String prolog =
                 "<html><title>Przykład</title>" +
                 "<body background=\"images/os2.jpg\" text=\"antiquewhite\"" +
                 "link=\"white\" vlink=\"white\">";

  // Tagi zamykające
  private String epilog = "</body></html>";


  // Metoda obsługi zlecenia GET

  public void doGet(HttpServletRequest request,
                       HttpServletResponse response)
                 throws ServletException, IOException
  {
     // Możemy w ten sposób ustalić typ treści i stronę kodową
     // łatwiej niż przez generowanie metatagów HTML

     response.setContentType("text/html; charset=ISO-8859-2");

     // Strumień wyjściowy, tu generowana treść strony HTML
     // PrintWriter umożliwia użycie metod print i println

     PrintWriter out = response.getWriter();


     out.println(prolog);  // piszemy początek html i tag <body ... >

     // Piszemy treść
     out.println("<h1>Dokument HTML<br>wygenerowany przez serwlet</h1>");
     out.println("<br><br><a href=\"Bye.html\">Pożegnanie</a>");

     // Znaczniki zamykające
     out.println(epilog);
     out.close();
  }

}
Ważne jest w tym przykładzie, aby zauważyć sposób odwołania z generowanej strony do zasobów aplikacji (plików os2.jpg i  Bye.html). Klasa serwletu znajduje się w katalogu serwlety1/WEB-INF/classes. Odwołania do zasobów są jednak zrelacjonowane wobec  głównego katalogu aplikacji. Zatem odwołanie "images/os2.jpg" jest - gdy patrzymy na naszą strukturę katalogową -  odwołaniem "serwlety1/images/os2.jpg", a odwołanie "Bye.html" znaczy "serwlety1/Bye.html".

Przed wdrożeniem aplikacji musimy stworzyć deskryptor wdrożenia (web.xml). W przypadku serwletów podstawowe informacje, które trzeba podać to:
Mamy tu dwa odwzorowania: nazwy i klasy, nazwy i mapowania.
Do związania nazwy i klasy serwletu stosuje się podelemnt <servlet_name> elementu <servlet> np.
    <servlet>
       <servlet-name>HTMLMsg</servlet-name>
       <description>Prosty napis</description>
       <servlet-class>Msg</servlet-class>
    </servlet>
Widzimy tu, że:
Mapowanie uzyskujemy dzięki elementowi <servlet_mapping>
    <servlet-mapping>
        <servlet-name>HTMLMsg</servlet-name>
        <url-pattern>/msg</url-pattern>
    </servlet-mapping>
Przy tym:

Cały plik deskryptora wdrożenia (web.xml) dla naszej aplikacji (na razie zawierającej tylko jeden serwlet) wygląda tak:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>Serwlety 1</display-name>
    <description>Proste przykladowe serwlety</description>

    <servlet>
       <servlet-name>HTMLMsg</servlet-name>
       <description>Prosty serwlet</description>
       <servlet-class>Msg</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HTMLMsg</servlet-name>
        <url-pattern>/msg</url-pattern>
    </servlet-mapping>

</web-app>

Pozostaje nam teraz wdrożenie.

Załóżmy,  że główny katalog naszej aplikacji znajduje się w katalogu G:\Programy\serwlety1.

Jak już mówiliśmy, dla wdrożenia wystarczy go skopiować do katalogu <katalog_intalacyjny_Tomcata>\webapps uzyskując:

<katalog_intalacyjny_Tomcata>
     webapps
          .....
          serwlety1
          ....

Możemy też spakować go jarem do pliku o rozszerzeniu WAR i to archiwum przekopiować do katalogu webapps.


Uruchomienie naszego serwletu (zgodnie z ogólną regułą podaną wcześniej) uzyskamy przez:

r

Uruchomienie i dzialanie serwletu ilustrują poniższe rysunki.


Otwieramy przeglądarkę i poprzez wpisaniu na pasku adresu odwołania do serwletu - przesyłamy mu zlecenie GET, otrzymując w odpowiedzi wygenerowaną stronę:

r

Trzeci sposób wdrożenia - za pomocą deskryptora kontekstu - ma pewne zalety wobec omówionych:
Deskryptor kontekstu jest elementem XML o nazwie Context, w którym opisujemy kontekst aplikacji. Możemy wpisać ten deskryptor do pliku konfiguracyjnego serwera (conf/server.xml) pod elementem Host. Lepiej jednak (i bezpieczniej ze względu na możliwość przypadkowych modyfikacji pliku konfiguarcji serwera)  jest wpisac deskryptor kontekstu do pliku XML o dowolnej nazwie i umieścić ten plik w katalogu webapps. To wystarczy dla wdrożenia aplikacji (ale też wymaga restartu Tomcata).

Element Context może zawierać szereg podelementów, m.in. takie, które znajdują się również w pliku web.xml. Można też w nim definiować nazwy plików logów dla aplikacji (do których to plików będą zapisywane różne informacje  np. o błędach, ostrzeżeniach, ale również dowolna treść - np. za pomocą metody log(..) z klas serwletowych). Bardzo ważnym zastosowaniem deskryptora kontekstu jest ustanawianie powiązań z zasobami zewnętrznymi i podawanie parametrów potrzebnych, by do takich zasobów (np. baz danych) móc się odwoływać.

W tej chwili interesują nas jednak przede wszystkim te właściwości deskryptora kontekstu, które pozwalają wdrożyć aplikację bez umieszczania jej struktury katalogowej (spakowanej lub nie) pod webapps. W tym względzie deskryptor kontekstu wygląda niezwykle prosto:

Najprostsza forma deskryptora kontekstu
<Context
       path="ścieżka_kontekstu"
      docBase="ścieżka_do_katalogu_aplikacji"
</Context>

gdzie:


Zobaczmy to na przykładzie naszej aplikacji (zawierającej na razie jeden serwlet - Msg.java).
Jak pamiętamy, jej komponenty znajdowąły się w katalogu G:/Programy/serwlety1.
Załóżmy, że Tomcat jest zainstalowane w katalogu E:/Serwery/apache-tomcat-6.0.13. Przy wdrożeniu skopiowaliśmy katalog serwlety1 z G:/Programy do  E:/Serwery/apache-tomcat-6.0.13/webapps. Ścieżką kontekstu aplikacji była /serwlety1 i za jej pomocą uruchamialiśmy tę aplikację.

Teraz wdrożymy tę samą aplikację pod jeszcze dwoma różnymi kontekstami.
Skopiujemy najpierw jej komponenty do katalogu E:/WebAplikacja1 (oczywiście struktura katalogowa jest zachowana, z tym, że teraz głównym katalogiem aplikacji jest F:/WebAplikacja1).

Przygotujmy dwa deskryptory kontekstu. Pierwszy wprowadzi kontekst o nazwie Show związany z aplikacją umieszczoną w katalogu G:/Programy/serwlety1, drugi - kontekst o nazwie wa1 związany z aplikacją z katalogu E:/WebAplikacja1. Umieścimy je w plikach XML o dowolnych nazwach np.

KontekstG.xml
<Context  path="/Show"  docBase="G:/Programy/serwlety1/build">
</Context>

KontekstE.xml
<Context  path="/wa1"   docBase="../../WebAplikacja1">
</Context>
Tu zwrócmy uwagę na relatywną ścieżkę (zaczynamy z  katalogu aplikacji  E:/Serwery/apache-tomcat-6.0.13/webapps, .. /.. przenosi nas do katalogu E:/ i stąd wskazujemy katalog WebAplikacja1.

Po skopiowaniu tych plików XML  do katalogu webapps i uruchomieniu Tomcata uzyskamy dostęp do trzech aplikacji (które robią to samo, bo zostały skopiowane) pod kontekstami:
/serwlety1   (to jest nasze początkowe wdrożenie)
/Show         (to zapewnił plik KontekstG.xml i zwarty w nim deskryptor kontekstu)
/wa1           (to za sprawą deskryptora kontekstu z pliku KontekstE

Każde z wywołań
http://localhost:8080/wa1/msg,
http://localhost:8080/Show/msg
http://localhost:8080/serwlety1/msg

pokaże nam znany już obrazek pochodządzy z serwletu Msg.java.

Możemy się też przekonać łatwo, że rzeczywiście konteksty odnoszą się do konkretnych fizycznych katalogów.

W tym celu dodamy do naszej aplikacji serwlet Msg2.java, który będzie pokazywał różne informacje, m.in. o ścieżkach. Przy okazji zilustruje on niektóre metody klasy HTTPServlet, HTTPServletRequest oraz interfejsu ServletContext (który "opisuje" kontekst aplikacji w ramach której działa serwlet)..
Od obiektu reprezenetującego zlecenie (HTTPServletRequest) możemy dowiedzieć się m.in.
Od samego serwletu możemy pobrać jego kontekst (typ ServletContext) za pomocą metody getServletContext().
Z kolei od kontekstu możemy dowiedzieć się wielu ciekawych rzeczy, m.in.:

Zobaczmy kod nowego serwletu (warto zwrócić uwagę, że w zwracanej odpowiedzi nie stosujemy żadnych konstrukcji HTML - przeglądarka uzyska i pokaże czysty tekst).
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.net.*;

public class Msg2 extends HttpServlet {

  private ServletContext context;
  private PrintWriter out;

  public void doGet(HttpServletRequest req, HttpServletResponse resp)
                       throws ServletException, IOException
  {
    out = resp.getWriter();
    out.println("To jest strona wygenerowana przez serwlet " +
                 this.getClass().getName()+ ".class");
    out.println("-------------------------------------------------------");

    // Jak wygladal URL z ktorego przyszlo zlecenie
    String requestURL = req.getRequestURL().toString();
    out.println("RequestURL: " + requestURL);

    // Uzyskujemy kontekst
    context = this.getServletContext();

    // Możemy od niego pobrać informacje o serwerze
    out.println("\nServer info\n" + context.getServerInfo() );

    // Możemy dowiedzieć się jaka jest nazwa aplikacji
    // określona w <display-name>
    out.println("\nAplikacja ma nazwe: " + context.getServletContextName() );

    // Informacje o ścieżkach
    String contextPath = req.getContextPath();
    String servletPath = req.getServletPath();

    // Od kontekstu możemy dowiedzieć się też jakie są fizyczne ścieżki
    // prowadzace do "wirtualnych" URLI
    out.println("\nInformacja o sciezkach");
    msg("ContextPath", contextPath);
    msg("ServletPath", servletPath);

    // I nasze pliki "zasobowe" (HTML, JPG)
    msg("Plik Bye.html", "Bye.html");
    msg("Plik os2.jpg", "images/os2.jpg");

    // Lista zasobów aplikacji
    out.println("\nLista zasobow aplikacji");
    listResources("/");

    // Możemy na tych zasobach wykonywać op we-wy
    InputStream in = context.getResourceAsStream("/WEB-INF/web.xml");
    BufferedReader br = new BufferedReader( new InputStreamReader(in));
    out.println("\nPierwszy wiersz pliku web.xml");
    out.println(br.readLine());
    br.close();

    // W jakim katalogu działa serwlet?
    File dir = new File(".");
    out.println("\nA serwlet dzialal w katalogu: " + dir.getAbsolutePath());


    out.close();
  }

   // Listuje zasoby aplikacji
  private void listResources(String path)  {
    if (path == null) return;
    Set res = context.getResourcePaths(path);
    for (Iterator iter = res.iterator(); iter.hasNext(); ) {
      String resItem = (String) iter.next();
      if (resItem.endsWith("/")) listResources(resItem);
      else out.println(resItem);
    }
  }

  private void msg(String info, String path) {
    out.println("------------------------------------------");
    out.println(info);
    String realPath = context.getRealPath(path);
    out.println("Virtual: " + path);
    out.println("Real   : " + realPath);
  }

}

Niewątpliwie ciekawa jest tu metoda getResources() i jej rekurencyjne wykorzystanie we własnej metodzie listResources(). Za chwilę - po obejrzeniu wyników dzialania serwletu - powiemy o niej więcej. Przedtem jednak musimy nowy serwlet umiejscowić w strukturze naszej aplikacji.
W tym celu - po skompilowaniu serwletu do katalogu classes - musimy uzupelnić deskryptor wdrożenia. Teraz aplikacja składa się z dwóch serwletów, zatem opis nowego serwletu musi znaleźć się w pliku web.xml. Ustalimy przy tym, że do serwletu Msg2.java będzie można się odwołać podając po ścieżce kontekstu mapowanie /paths.
Nowy plik web.xml wygląda tak:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>Serwlety 1</display-name>
    <description>Proste przykladowe serwlety</description>

    <servlet>
       <servlet-name>HTMLMsg</servlet-name>
       <description>Prosty serwlet</description>
       <servlet-class>Msg</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HTMLMsg</servlet-name>
        <url-pattern>/msg</url-pattern>
    </servlet-mapping>

    <servlet>
       <servlet-name>Msg2</servlet-name>
       <description>Info o sciezkach</description>
       <servlet-class>Msg2</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Msg2</servlet-name>
        <url-pattern>/paths</url-pattern>
    </servlet-mapping>

</web-app<
a struktura naszej aplikacji jest teraz taka:

<główny_katalog_aplikacji>
    WEB-INF
         classes
             Msg.class
             Msg2.class
         web.xml        <--- zawiera opis obu serwletów
    images               <---- pliki graficzne umieszczamy w podkatalogu
          os2.jpg         <--- to jest tło strony generowanej przez serwlet Msg
    Bye.html            <--- a to strona do której prowadzi link z HTML-u z Msg


Po wdrożeniu w kontekście /serwlety1 (np. podkatalog serwlety1 w katalogu webapps)  będziemy ten serwlet uruchamiać poprzez

http://localhost:8080/serwlety1/paths

i uzyskamy w przeglądarce nastepujące wyniki:


To jest strona wygenerowana przez serwlet Msg2.class
-------------------------------------------------------
RequestURL: http://localhost:8080/serwlety1/paths

Server info
Apache Tomcat/6.0.13

Aplikacja ma nazwe: Serwlety 1

Informacja o sciezkach
------------------------------------------
ContextPath
Virtual: /serwlety1
Real   : E:\Serwery\apache-tomcat-6.0.13\webapps\serwlety1\serwlety1
------------------------------------------
ServletPath
Virtual: /paths
Real   : E:\Serwery\apache-tomcat-6.0.13\webapps\serwlety1\paths
------------------------------------------
Plik Bye.html
Virtual: Bye.html
Real   : E:\Serwery\apache-tomcat-6.0.13\webapps\serwlety1\Bye.html
------------------------------------------
Plik os2.jpg
Virtual: images/os2.jpg
Real   : E:\Serwery\apache-tomcat-6.0.13\webapps\serwlety1\images\os2.jpg

Lista zasobow aplikacji
/Bye.html
/images/os2.jpg
/WEB-INF/classes/Msg.class
/WEB-INF/classes/Msg2.class
/WEB-INF/web.xml

Pierwszy wiersz pliku web.xml
<?xml version="1.0" encoding="UTF-8"?>

A serwlet dzialal w katalogu: E:\Serwery\apache-tomcat-6.0.13\bin\.

wróćmy uwagę na pokazane fizyczne ścieżki (pamiętamy, że webapps znajduje się w tym przykładzie w katalogu E:/Serwery/apache-tomcat-6.0.13).
No i na ciekawą metodę gerResources(), która umożliwia dostep do zasobów aplikacji z serwletu. Działa ona podobnie do listowania katalogu - zwraca zbiór ścieżek, które znajdują się "pod" podaną jako argument ścieżką. Podając jako argument "/" odwołujemy się do głównej ścieżki aplikacji (inaczej zwanej context root ) i uzyskujemy wszystko co pod nią sie znajduje. W naszym przypadku getResources("/") zwróci:  zbiór elementów "/images/", "/WEB-INF/", "/Bye.html". Elementy, które można dalej rozwijać (stanowiące podkatalogi) kończą się ukośnikiem. Wszystkie elementy mają na początku ukośnik (np. plik Bye.html - jest przedstawiony jako "/Bye.html").

Na zasobach aplikacji możemy wykonywać operacje wejścia-wyjścia  poprzez uzyskanie strumienia związanego z zasobem za pomocą metody getResourceAsStream(String sciezkaZasobu). Pokazuje to przykład czytania pliku web.xml, a podkreślmy że ten sposób działania jest wygodny, bowiem - jak widać - roboczym katalogiem serwletu nie jest katalog naszej aplikacji, ale katalog serwera.


3. Budowanie, rozwijanie i wdrażanie aplikacji za pomocą Ant-a.

Jak widać proces rozwijania aplikacji WEB jest dość pracochłonny, żeby nie powiedzieć uciążliwy. Samo ostateczne wdrożenie (wymagające rozlicznych zabiegów konfiguracyjnych) - jako jednorazowe, przynajmniej na jakiś czas - nie straszy tak bardzo nakładami pracy. Gorzej z kolejnymi fazami rozwijania i testowania aplikacji. Jeżeli za każdym razem, przy każdej zmianie w procesie rozwoju programu, nawet w najprostszym przypadku musimy wykonać wszystkie opisane wcześniej czynności "ręcznie", to staje się to bardzo uciążliwe, a co gorsza skupia uwage na arbitralnych, konwencjonalnych szczególach technicznych (a jak musi się nazywać katalog, a jaka musi być jego struktura, a jakie muszą być elementy w pliku web.xml itp.), nie mających nic wspólnego z logiką aplikacji i sferą rozwiązywanego przez nią problemu.

Oczywiście - ze względu na naturę aplikacji WEB jest to nie do uniknięcia. Można jednak uniknąć całkowicie "ręcznego" powtarzania niezbędnych rutynowych czynności.
Istnieją środowiska wizualnego programowania, które ułatwiają te wszystkie sprawy (np. NetBeans, Eclipse).

Tutaj powiemy parę słów o innej, uniwersalnej, możliwości automatyzacji rutynowych czynności związanych z rozwijaniem i wdrażaniem aplikacji WEB - narzędziu, które nazywa się Ant.

Ant pozwala zapisać sekwencję wspólzależnych działań (zadań), które następnie wykonuje, co prowadzi w efekcie do instalacji lub wdrożenia aplikacji. Obejmują one m.in.:
Sekwencje tych zadań zapisujemy w specjalnym pliku XML, interpretowanym przez Ant, który wykonuje opisane czynności. Przy tym możemy używać zmiennych i w ten sposób "parametryzować" działanie Anta (w łatwy sposób, w jednym miejscu, zmieniać nazwy aplikacji, umiejscowienie jej struktur katalogowej itp.).

Nie sposób w kilku słowach opisać Anta (na ten temat są ponad 500-stronicowe książki, np. świetna "Java Development with Ant" Ericka Hatchera i Steve'a Looghrana).

Przedstawimy raczej "techniczną" instrukcję, dotyczącą łatwego sposobu budowania i instalowania aplikacji WEB za pomocą Anta,

Po pierwsze, musimy stworzyć okresloną strukturę katalogową rozwijanej apliakcji.
Najwygodniej zrobić to w następującej formie:

 
Ant wykonuje swoje zadania tylko wtedy gdy istnieje taka potrzeba (np. kompilacja jest wykonywana tylko jeśli żródła mają nowszą datę od dotychczasowych wyników kompilacji, pliki są kopiowane tylko wtedy, gdy są nowsze od już obecnych), Rozpoczynając opis zadań anta (ich wykonanie umieszczamy w tagu target) poprosimy o ustalenie bieżącego czasu (tstamp):
<project name="nazwa projektu" default="build" basedir=".">
  <target name="init">
      <tstamp/>
  </target>

Uwagi:
Następnie musimy ustalić specyficzne dla aplikacji właściwości: katalog w którym mają być umieszczone  elementy aplikacja gotowe do zainstalowania lub wdrożeuia, nazwę kontekstu apliakcji.

Specyficzne właściwości naszej apliakcji zapiszemy w pliku build.properties, np:
app=serwlety1
app.path=E:/webaps/SERWLETY
i użyjemy ich w pliku ant-a build.xml jako wartości zmiennych (oznaczanych przez ${nazwa_zmiennej}:
  <property file="build.properties"/>
  <property name="build" value="${app.path}/${app}/build" />
  <property name="context.path" value="${app}" />
Powyżej, uzyskaliśmy wartości z pliku build.properties, po czym użyliśmy ich do ustalenia właściwości build i context.path. Od tego momentu, zmienna ${build} ma wartość
e:/webaps/SERWLETY/serwlety1/build (to jest ten katalog, w którym umieszczone zostaną gotowe do wdrożenia czy instalacji komponenty), a zmienna ${context.path}  oznaczająca kontekst aplikacji ma wartość serwlety1.

Zadanie przygotowawcze polega na stworzeniu odpowiednich katalogów (jeśli jest taka potrzeba, jesli ich jeszcze nie ma).
  <target name="prepare" depends="init"
   	description="Buduje katalogi build">
    <mkdir dir="${build}" />
    <mkdir dir="${build}/WEB-INF" />
    <mkdir dir="${build}/WEB-INF/classes" />
    <mkdir dir="${build}/WEB-INF/lib" />	
    <mkdir dir="${build}/WEB-INF/tags" />			
  </target>

Uwagi:
Podstawowe zadanie build polega na kompilacji klas i kopiowaniu komponentów do katalogu instalacyjnego (oznaczanego przez zmienną ${build}).
Wcześniej powinniśmy ustalić ścieżką classpath, na której znajdą się niezbędne w kompilacji pakiety (pliki JAR).
   <!-- Sciezka klas dla kompilacji -->

  <path id="classpath">
    <fileset dir="${catalina.home}/lib">
      <include name="*.jar"/>
    </fileset>
  </path>

Uwaga:

Do tej ścieżki (identyfikowanej przez classpath) odwołamy się w zadaniu kompilacji (javac), stanowiącego cześc zadania build. Inne fragmenty tego zadania polegają na kopiowaniu plikó.

  <target name="build" depends="prepare"
     description="Kompilacja i kopiowanie" >
    <javac srcdir="src" destdir="${build}/WEB-INF/classes" debug="on">
    	<include name="**/*.java" />
     	<classpath refid="classpath"/>
    </javac>
    <copy todir="${build}/WEB-INF">
      <fileset dir="web/WEB-INF"    >
        <include name="**/*.xml" />
        <include name="**/*.html" />
        <include name="**/*.tld" />
        <include name="**/*.properties" />
        <include name="**/*.txt" />
      </fileset>
    </copy>
    <copy todir="${build}">
      <fileset dir="web">
        <include name="**/*.html" />
        <include name="**/*.jsp" />
        <include name="**/*.jspf" />
        <include name="**/*.gif" />
      </fileset>
    </copy>
    <copy todir="${build}/WEB-INF/tags">
      <fileset dir="web">
        <include name="**/*.tag" />
      </fileset>
    </copy>
  </target>

Kompilowane i kopiowane są tylko te pliki, które zostały zmodyfikowane.
Zauważmy, że kopiowanie plików z rozszerzeniem *.tag, potrzebne jest po to by używać na stronach JSP  ew. własnych znaczników.

Po wykonaniu zadania build w katalogu oznaczanym przez zmienną ${build} znajduje sie gotowa do instalacji lub wdrożenia struktura katalogowa naszej aplikacji.

Bardzo mocną właściwością Anta jest możliwość dowolnego rozszerzania jego funkcjonalności przez definiowanie i implementację dodatkowych, specyficznych zadań (jako klas Javy, zresztą cały Ant napisany jest w Javie),

Takie specyficzne zadania związane z komunikacją z menedżerem aplikacji Tomcata zostały już dla nas przygotowane - musimy je tylko włączyć do pliku opisu zadań Anta (build.xml). Są to m.in. następujące zadania:
Musimy te zadania najpierw zdefiniować (bo wykorzystują zewnętrzne wobec Anta klasy):
  <!-- definiowane zadania Anta dla Tomcata -->
  <taskdef name="install" classname="org.apache.catalina.ant.InstallTask"/>
  <taskdef name="reload"  classname="org.apache.catalina.ant.ReloadTask"/>
  <taskdef name="remove"  classname="org.apache.catalina.ant.RemoveTask"/>	
  <taskdef name="deploy" classname="org.apache.catalina.ant.DeployTask"/>	
  <taskdef name="undeploy" classname="org.apache.catalina.ant.UndeployTask"/>	

Po czym możemy ich użyć np.:
  <target name="install" description="Instaluje aplikacje"
          depends="build">
    <install url="${url}" username="${username}" password="${password}"
          path="/${context.path}" war="file:${build}"/>
  </target>

  <target name="install-config"
      description="Instaluje w oparciu o context.xml" depends="build">
     <install url="${url}" path="niewazne"
      config="file:${app.path}/${app}/context.xml"
      username="${username}" password="${password}"/>
  </target>


  <target name="reload" description="Przeladowuje  aplikacje"
          depends="build">
    <reload  url="${url}" username="${username}" password="${password}"
          path="/${context.path}"/>
  </target>

  <target name="remove" description="Usuwa aplikacje">
    <remove url="${url}" username="${username}" password="${password}"
          path="/${context.path}"/>
  </target>

Uwaga:

Pokazane fragmenty pliku build.xml są na tyle ogólne, że dają się zastosować wlaściwie wobec każdej aplikacji zawierającej serwlety lub strony JSP.
Możemy zatem taki plik umieścić w jakimś ogólnie dostępnym katalogu (np. o nazwie common), nazwac np. defbuild.xml i wlączać go w każdym konkretnym pliku buld.xml dla konkretnych aplikacji.
We wspólnym katalogu (common) umieścimy też plik współnych właściwości wszystkich aplikacji (build.properties), w którym możemy zapisać np. nazwę użytkownika i hasło dostępu do menedżera aplikacji oraz ścieżkę do instalacji Tomcata, np:

username=admin
password=admin
catalina.home=E:/Serwery/apache-tomcat-6.0.13


Teraz w każdym konkretnym podkatalogu przeznaczonym dla konkretnej apliakcji umieszczamy tylko opisu właściwości tej aplikacji (nazwa aplikacji i ścieżka dostępu  - widzieliśmy już ten plik build.properties) oraz  plik buld.xml w następującej postaci:

<!--
    Projekt
-->

<!DOCTYPE project [
  <!ENTITY defbuild SYSTEM "../../common/defbuild.xml">
]>


<project name="moja aplikacja" default="build" basedir=".">
  <target name="init">
      <tstamp/>
  </target>

  &defbuild;

</project>

Zapis &defbuild włącza zawartości pliku defbuild.xml z katalogu common.
Dla porządku przytoczę jeszcze pełny zapis tego pliku:
  <!-- Wlasciwosci dla dostepu do Managera aplikacji -->
  <property name="url"      value="http://localhost:8080/manager"/>

       <!-- Konfiguracja wlasciwosci  --> 
  <property file="../../common/build.properties"/>
  <property file="build.properties"/>
  <property name="build" value="${app.path}/${app}/build" />
  <property name="context.path" value="${app}" />


  <!-- Sciezka klas dla kompilacji -->

  <path id="classpath">
    <fileset dir="${catalina.home}/lib">
      <include name="*.jar"/>
    </fileset>
  </path>

  <!-- definiowane zadania Anta dla Tomcata -->
  <taskdef name="install" classname="org.apache.catalina.ant.InstallTask"/>
  <taskdef name="reload"  classname="org.apache.catalina.ant.ReloadTask"/>
  <taskdef name="remove"  classname="org.apache.catalina.ant.RemoveTask"/>	
  <taskdef name="deploy" classname="org.apache.catalina.ant.DeployTask"/>	
  <taskdef name="undeploy" classname="org.apache.catalina.ant.UndeployTask"/>	


  <target name="prepare" depends="init"
   	description="Buduje katalogi build">
    <mkdir dir="${build}" />
    <mkdir dir="${build}/WEB-INF" />
    <mkdir dir="${build}/WEB-INF/classes" />
    <mkdir dir="${build}/WEB-INF/lib" />	
    <mkdir dir="${build}/WEB-INF/tags" />			
  </target>


  <!-- wykonanie zadan -->

  <target name="install" description="Instaluje aplikacje"
          depends="build">
    <install url="${url}" username="${username}" password="${password}"
          path="/${context.path}" war="file:${build}"/>
  </target>

  <target name="install-config"  
      description="Instaluje w oparciu o context.xml" depends="build">
     <install url="${url}" path="/${context.path}" war="file:${build}"
      config="file:${app.path}/${app}/context.xml"
      username="${username}" password="${password}"/>
  </target> 


  <target name="reload" description="Przeladowuje  aplikacje"
          depends="build">
    <reload  url="${url}" username="${username}" password="${password}"
          path="/${context.path}"/>
  </target>

  <target name="remove" description="Usuwa aplikacje">
    <remove url="${url}" username="${username}" password="${password}"
          path="/${context.path}"/>
  </target>


  <target name="build" depends="prepare" 
     description="Kompilacja i kopiowanie" >
    <javac srcdir="src" destdir="${build}/WEB-INF/classes" debug="on">
    	<include name="**/*.java" />
     	<classpath refid="classpath"/>
    </javac>
    <copy todir="${build}/WEB-INF">
      <fileset dir="web/WEB-INF"    >
        <include name="**/*.xml" />
        <include name="**/*.html" />
        <include name="**/*.tld" />
        <include name="**/*.properties" />
        <include name="**/*.txt" />
      </fileset>
    </copy>
    <copy todir="${build}">
      <fileset dir="web">
        <include name="**/*.html" />
        <include name="**/*.jsp" />
        <include name="**/*.jspf" />
        <include name="**/*.gif" />
      </fileset>
    </copy>
    <copy todir="${build}/WEB-INF/tags">
      <fileset dir="web">
        <include name="**/*.tag" />
      </fileset>
    </copy>
  </target>


oraz przykładowy plik build.properties (konkretnej aplikacji), np rozwijanej w katalogu G:/webaps/SERWLETY/jsp-test

app=jsp-test
app.path=G:/webaps/SERWLETY

Po wywołaniu ant install  w katalogu G:/webaps/SERWLETY/jsp-test zostanie utworzony katalog build, skopiowane do niego odpowiednie pliki i struktury katalogowe i aplikacja zostanie zainstalowana w kontekście jsp-test (ścieżką kontekstu aplikacji bedzie /jsp-test). Od tego momentu możemy odowołwyac się do tej aplikacji.

Jeśli chcemy zmienić plik web.xml (deskryptor wdrożenia), to po zmianie - aplikację musimy przeinstalować zatem:
Możemy również zainstalować aplikację używając pliku deskryptora kontekstu (powiedzmy, że nazywa się on context.xml, tak zresztą ustaliliśmy w pliku defbuild.xml dla Anta).  W tym przypadku wywołujemy anta do wykonania zadania install-config:
ant install-config (zob. jego definicję w pliku defbuild.xml)

Przy instalacjach jako umiejscowienie aplikacji możemy podać zarowno niespakaowany katalog aplikacji, jak i plik WAR (ze zpakowanym katalogiem aplikacji).

Zadanie wdrożenia (ant deploy) wymaga natomiast użycia wyłącznie pliku WAR, do którego wcześniej musimy spakować strukturę katalogową aplikacji. Do tego też możemy zastosowac Anta, przez zdefiniowanie i wykonanie następującego zadania:

  <target name="package"
      description="Pakuje WAR">
      <delete file="dist/${war.file}" />
      <jar jarfile="dist/${war.file}" >
        <fileset dir="${build}" />
      </jar>
  </target>

Katalog dist jest katalogiem "dystrybucji" i musi być oczywiście wcześniej utworzony.

Tutaj raczej pozastawiamy etap wdrożenia na boku (nie zdefiniowaliśmy zadań deploy i undeploy, zamaist nich używamy install i remove). Zależy nam bowiem przede wszystkim na narzędziu, które umożliwi szybkie przeinstalowywanie aplikacji przy jej rozbudowie i testowaniau. Takie narzędzie właśnie opisano, a niezbędne pliki znajdują sie w katalogu samples. Na koniec warto jeszcze tylko powiedzieć, że proponowane rozwiązanie jest - naturalnie - jednym z możliwych. Być może łatwiej jest używać środowisk w rodzaju NetBeans czy Ecliipse, skonfigurowanego jako Eclipse for Java EE Developer. Ale stosując własny skrypt Anta możemy bardzo dokładnie prześledzić co się dziej, gdzie i jak są umieszczane rózne komponenty aplikacji itp. Przejście na bardziej wizualne, "wyklikiwane" rozwiązania jest etapem, który raczej powinien następowac po zrzoumieniu mechanizmów dzialania. I dlatego na Ancie tu  poprzestaniemy.


4. Serwlety - model działania i obsługa zleceń

Zajmiemy się teraz bliżej serwletami HTTP, reprezentowanymi przez obiekty klasy HTTPServlet.
Najogólniejaza klasa serwletów GenericServlet implementuje interfejsy Servlet (określający najogólniejszą funkcjonalność serwletów) oraz ServletConfig (obiekt tego typu są przekazywane serwletom przy ich  inicjacji przez serwer i zawiera  kontekst aplikacji WEB - czyli znany nam już ServletContext).

GenericServlet określa serwlety bez ustalonego protokołu sieciowego, dziedzicząca ją klasa HTTPServlet służy do tworzenia serwletów obsługujących protokół HTTP.

r1


Aby stworzyć serwlet HTTP dziedziczymy klasę HTTPServlet i przedefiniowujemy wybrane jej metody


Serwlety są tworzone i inicjowane przy pierwszym wywołaniu (lub na starcie serwera - w zależnosci od opcji). Proces obejmuje kolejno:
Dopiero w fazie inicjacji tworzony jest obiekt typu ServletConfig i dopiero od tego momentu dostępny jest kontekst serwletu. Jest to ważna obserwacja, bowiem oznacza ona, że w konstruktorze  nie mamy dostępu do kontekstu aplikacji, a zatem  ścieżek i zasobów.
Przy inicjacji serwletu wywoływana jest metoda init(ServletConfig) z obiektem typu ServletConfig przekazanym jako argument, która z kolei wywołuje bezparametrowa metode init(). 
Dla wykonania jakichs jednorazowych prac inicjacyjnych (np. lączenia z bazą danych) przedefiniowujemy zwykle  bezparametrową metodę  init(),  bo jest to wygodniejsze (przedefiniowanie init(ServletConfig) wymagałoby - zgodnie z modelem działania serwletów - odwołania super.init()).
Po inicjacji serwlet jest gotowy do obsługi zleceń klientów.

Każde zlecenie przechodzi przez metodę service(...), która z kolei wywołuje odpowiednią dla danego rodzaju zlecenia (GET,POST,PUT, itd)

Ilustruje to wszystko poniższy przykładowy serwlet:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class Test extends HttpServlet {

  static String loadMsg = "Klasa zaladowana " + new Date();
  String createMsg = "\nSerwlet utworzony  " + new Date();
  String initMsg;
  String config1Msg = "\nW konstruktorze ServletConfig ",
         config2Msg = "\nW metodzie init ServletConfig ";

  public Test() {
    ServletConfig conf = getServletConfig();
    if (conf == null)
      config1Msg += "nie istnieje,\n" +
         "zatem nie ma dostepu do kontekstu i inicjalnych parametrów";
    else config1Msg +=  " istnieje !!??";
  }

  public void init() {
    initMsg  = "\nSerwlet zainicjowany " + new Date();
    ServletConfig conf = getServletConfig();
    if (conf == null) config2Msg +=  "nie istnieje !!??";
    else {
      config2Msg += "istnieje.\nMozemy odwolac sie do kontekstu" +
                     " i inicjalnych parametrów:\n";

    // Uzyskanie kontekstu i dostęp do niego - dwa równoważne sposoby
    ServletContext context1 = conf.getServletContext();
          // poniższe odwolanie oznacza getConfig().getServletContext()
    ServletContext context2 = getServletContext();
    String name = context2.getServletContextName();
    }
  }

  PrintWriter out;

  public void service(HttpServletRequest req, HttpServletResponse resp)
         throws ServletException, IOException
  {
     out = resp.getWriter();
     out.println(loadMsg);
     out.println(createMsg);
     out.println(config1Msg);
     out.println(config2Msg);
     out.println("obsluga zlecenia przez metode service " + new Date());

     // Jezeli przedefinujemy metodę servis
     // zazwyczaj będziemy wołać super.service(...)
     // by przekazać zlecenie do obsługi przez konkretne metody
     // np. doGet lub doPost

     super.service(req, resp);
     out.close();
  }


  public void doGet(HttpServletRequest req, HttpServletResponse resp)
                 throws ServletException, IOException
  {

    out.println("\nWywolana metoda doGet " + new Date());
    out.close();
  }


}
który wywołany z paska adresu przeglądarki (co jest równoważne zleceniu GET) utworzy i zwróci przeglądarce  stronę rekstową o następującej zawartości:
Klasa zaladowana Mon Sep 01 01:59:04 CEST 2008

Serwlet utworzony  Mon Sep 01 01:59:04 CEST 2008

W konstruktorze ServletConfig nie istnieje,
zatem nie ma dostepu do kontekstu i inicjalnych parametrów

W metodzie init ServletConfig istnieje.
Mozemy odwolac sie do kontekstu i inicjalnych parametrów:

obsluga zlecenia przez metode service Mon Sep 01 01:59:04 CEST 2008

Wywolana metoda doGet Mon Sep 01 01:59:04 CEST 2008

Na życzenie administratora serwera lub gdy aplikacja jest nieaktywna przez określony czas (ustalony przez odpowiednie opcje serwera) - serwlet jest usuwany. Wtedy wywoływana jest jego metoda destroy(). Możemy ją przedefiniować, szczególnie w tym celu, by uporządkować środowisko (np. zamknąć połączenia z bazami danych, usunąc jakieś niepotrzebne zasoby itp.).

Serwlety HTTP obslugują zlecenia HTTP.

Zlecenie HTTP składa się m.in. z:

Serwlety HTTP obsługują następujące rodzaje (metody) zleceń HTTP.

Zlecenie (metoda)
Znaczenie
Metoda klasy
HTTPServlet

GET uzyskanie zasobu identyfikowanego przez URL
doGet
HEAD Uzyskanie nagłówków
doHead
POST Wysłanie danych o nielimiotowanej długości
doPost
PUT Zapisanie zasobu
doPut
DELETE Usunięcie zasobu
doDelete
OPTIONS Zwrcaa metody HTTP podtrzymywane przez serwer
doOptions
TRACEZwraca nagłówki wysłane zleceniem TRACE (do celów testowania_
doTrace

Wymienione metody klasy HTTPServlet sa wywoływane, gdy przyjdzie odpowiednie zlecenie. Dlatego dla obsługi odpowiedniego rodzaju zlecenia przedefiniowujemy odpowiednie metody.
Wszystkie w/w metody mają takie same sygnatury oraz  (standardowo) mogą generować wyjątki klas ServletException i IOException


Sygnatury i wyjątki kontrolowane metod obsługi zleceń HTTP
z klasy HTTPServlet

protected void doNNN( HttpServletRequest req,  HttpServletResponse resp)
                                            throws ServletException,  java.io.IOException

gdzie NNN - zlecenie (GET,POST, HEAD, itd).


Zlecenia do obsługi przekazywane są jako obiekty klasy implementującej interfejs HTTPServletRequest (który z kolei rozszerza ogólniejszy interfejs ServletRequest)
Przygotowana standardowa implementacja tego interfejsu w klasie HTTPServletRequestWrapper służy do wygodnego budowania własnych klas zleceń (poprzez odziedziczenie gotowej implementacji, dzięki czemu unikamy konieczności definiowania metod interfejsu, a możemy je przedefiniować lub dodać jakieś nowe).
Wygląda to mniej więcej tak:

r

Obiekt-zlecenie możemy wykorzystać m.in. do:
Zwykle po interpretacji zlecenia i przeprowadzeniu odpowiednich działań, serwlet konstruuje odpowiedź, która ma być przesłana klientowi.
Odpowiedzi są obiektami klas implementujących interfejs HTTPServletResponse. Podobnie jak przy zleceniacj mamy tu odpowiedni "wrapper" ułatwiający tworzenie własnych klas iodpowiedzi.

r

Odpowiedzi składają się m.in. z :
Budując odpowiedź możemy wykorzystać metody ustalania i dodawania nagłówków:
ogólne - setHeader(...), addHeader(...) oraz specjalnie przygotowane na często używane przypadki np. setCharacterEncoding(...) czy setContentType(...).

Serwlet zapisuje treść odpowiedzi  do strumienia (znakowego lub bajtowego) związanego z obiektem-odpowiedzią. Strumienie te pobieramy od obiektu-odpowiedzi za pomocą metod getWriter() (strumień znakowy) lub getOutputStream() (strumień bajtowy, binarny).

W przypadku wystąpienia  błędu - zamiast ciała odpowiedzi  możemy posłać kod błędu z ew. dodatkową informacją (metody sendError). Metody te mogą być uzyte tylko wtedy gdy odpowiedź nie został dotąd zatwierdzona.
Odpowiedż jest zatwierdzona (commited), gdy bufor strumienia odpowiedzi jest wymiatany (wielkość bufora możemy ustalić za pomoca metody setBuffer). To ważna informacja, bowiem po zatwierdzeniu odpowiedzi pewne akcje ze strony serwletu są niedozwolone np. ustalanie nagłówków czy - własnie wspomniane przed chwilą użycie metod sendErroe).

Serwlet może również zamiast generowania treści:
Pora teraz na praktyczne przykłądy, dzięki którym będziemy mogli bliżej omówić niektóre szczegóły przedtsaiwonego tu "modelu" działania serwletów.

Zacznijmy od serwletu witającego użytkownika i pokazującego aktualną datę i czas.
Oczywiście, musimy uwzględnić wymagania internacjonalizacji: serwlet powinien przywitać użytkownika w jego języku (ściślej języku ustalonym w przeglądarce) i odpowiednio sformatować datę i czas. Wymaga to:
Klasy zasobów lokaloizacyjnych przygotujemy i umieścimy w pakiecie international (zatem w naszym katalogu WEB-INF\classes musi znaleźć się odpowiedni podkatalog). Przykładowo może:my mieć m.in. takie klasy

package international;
import java.util.*;

public class Messages_en extends ListResourceBundle {
     public Object[][] getContents() {
         return contents;
     }

    static final Object[][] contents = {
       { "hello", "Hello!" },
       { "now", "Now is: " },
       { "charset", "ISO-8859-1" }
    };
}
package international;
import java.util.*;

public class Messages_pl extends ListResourceBundle {
     public Object[][] getContents() {
         return contents;
     }

    static final Object[][] contents = {
       { "hello", "Dzień dobry!" },
       { "now", "Teraz będzie, a właściwie już jest" },
       { "charset", "ISO-8859-2" }
    };
}
Uwagi:
  1. W zasobach dla danego języka określimy właściwą stronę kodową, nie polegając na kodowaniu, które ew. moglibyśmy odczytać z nagłówka zlecenia (getCharacterEncoding()), bo mogłoby okazać się fałszywe.
  2. Zasoby zlokalizowane przygotowujemy jako klasy ListResourceBundle, bowiem - z niejasnych  powodów - serwlety nie umieją właściwie przekodować do Unicode'u tekstów zapisanych w plikach .properties (PropertiesBundle).
  3. Musimy dostarczyć wszystkich klas z odpowiednimi przyrostkami lokalizacyjnymi (Messages_pl, Messages_en). Inaczej bowiem niż w normalnych apliakcjach w serwletach przy braku klasy dla danej lokalizacji nie jest ładowany domyślny zasób (Messages) o ile tylko nie zrestartowano serwera. Ma to zapewne związek z obsługą ładowania klas przez Tomcat.

Nasz serwlet może wyglądac tak.
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.text.*;

public class Time1 extends HttpServlet {

  public void doGet(HttpServletRequest request,
                       HttpServletResponse response)
                 throws ServletException, java.io.IOException
  {
     // Jaki jest preferowany język klienta?
     Locale locale = request.getLocale();

     // Uzyskanie odpowiedniego zlokalizowanego zasobu
     ResourceBundle msg = ResourceBundle.getBundle(
                             "international.Messages", locale);

     // Zlokalizowane komunikaty i odpowiednia strona kodowa
     String hello = msg.getString("hello");
     String now = msg.getString("now");
     String charset = msg.getString("charset");

     // Ustalenie typu i kodowania odpowiedzi
     // Musi być ustalone przed uzyskaniem strumienia wyjściowego
     response.setContentType("text/html; charset=" + charset);

     // Pobranie strumienia wyjściowego
     // z zapewnieniem własciwego kodowania
     // czasami wystarcza samo: PrintWriter out = response.getWriter()

     PrintWriter out = new PrintWriter(
        new OutputStreamWriter(response.getOutputStream(), charset),
        true);



     out.println("<h2>" + hello + "<br>" + now + "<br>" );
     out.println(getDate(locale) + "</h2>");
     out.close();
  }


  private String getDate(Locale loc) {
    Date data = new Date();
    DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                   DateFormat.MEDIUM,
                                                   loc);
    return df.format(data);
  }


}
a wynik jego działania będzie zależał nie tylko od aktualnej daty i czasu, ale również od ustawień językowych przeglądarki klienta.
Tutaj mamy bardzo wyraźny, prosty przykład prawdziwie dynamicznej generacji treści.
W przeglądarce ustawionej na język polski uzyskamy:

r

a w ustawionej na język angielski:

r


Z tym przykładowym serwletem mamy jednak przynajmniej dwa kłopoty.
Zacznijmy od zlecń. Dwa najbardziej popularne rodzaje zleceń HTTP to GET i POST.
Zlecenie GET jest generowane, gdy:
Zlecenie POST - służące głównie przesyłaniu przez klienta większej liczby informacji - jest generowane, gdy uzytkownik wysyła formularz z wyspecyfikowaną metodą POST (METHOD="POST").

Oczywiście, każde z tych rodzajów zleceń może też być wysłane przez dowolnych klientów HTTP (nie tylko przeglądarki), którzy wyspecyfikują jako metodę zlecenia GET lub POST.

Okazuje się więc, że oba rodzaje zleceń mogą dotyczyć naszego pokazywacza czasu.
Np. z takiej strony WWW:
r

której kod HTML wygląda tak:
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=windows-1250">
 <title>Pokazuje date i czas</title>
</head>

<body>
<center><h2>Data i czas</h2></center>
<hr>
<form method="post" action ="http://localhost:8080/serwlety1/Time">
Aby zobaczyć datę i czas wciśnij przycisk&nbsp&nbsp
<input type="submit" value="Data i czas">
</form>
</body>
</html>

możemy wywołać nasz serwlet (jako część aplikacji o kontekście serwlety1 z mapowaniem nazwy serwletu na odwołanie /Time)

Oczywiście, w tym przypadku metoda doGet nie zostanie wywołana, a doPost nie jest przedefiniowana. Otrzymamy w wyniku:

r
Jeżeli zatem mamy serwlety, do których można odwoływać się zarówno za pomocą metody GET jak i POST powinniśmy kontruować je w taki sposób, by obsługa zlecenia była wykonywana niezależnie od tego czy jest to zlecenie GET czy POST.
W tym celu można np. wprowadzić ogólną metodę serviceRequest i wywoływac ją zarówno z doPost, jak i doGet. W tej konwencji nasz przykładowy serwlet, pokazujący datę i czas wyglądałby tak.
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.text.*;

public class ShowTime0 extends HttpServlet {

  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
     Locale locale = req.getLocale();
      // ...
      out.close();
  }

// ...

//--- Stanadardowa część serwletu -----------------------------------
  public void doGet(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

  public void doPost(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

}
Oczywiście, w tym przykładzie nie ma żadnego sensu (choć jest możliwe) posyłanie zlecenia POST, bo jak wspomniano służy ono głównie do przekazywania informacji zawartych w formularzach (a tu żadnej informacji użytkownik nie dostarcza). Ale również zlecenie GET może być użyte w tym samym celu.
Pora więc na przyjrzenie się parametrom zleceń.

5. Parametry zleceń


Skąd się biorą parametry? Zazwyczaj z formularzy, ale - jak zobaczymy - niekoniecznie.
Utwórzmy stronę z formularzem:
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=windows-1250">
 <title>Testowanie</title>
</head>
<body>
<center><h2>Testowanie parametrów</h2></center>
<hr>
<form method="get" action="http://localhost:8080/serwlety1/params1">
id<input type="text" size="50" name="ident"><br>
p1<input type="text" size="50" name="p1"><br>
p2<input type="text" size="50" name="p2"><br>
p3<input type="text" size="50" name="p3"><br>
p4<input type="text" size="50" name="p4"><br>
p5<input type="text" size="50" name="p5"><br>
p6<input type="text" size="50" name="p6"><br>
 _
<br><input type="submit" value="Wyślij formularz">
</form>
</body></html>
Mamy tu 7 pól tekstowych, każde z nich ma nadaną nazwę (ident, p1, ...p6).
Te nazwy będą nazwami parametrów, do których będziemy mogli się odwołać w serwlecie.
Po kliknięcu w przycisk "Wyślij formularz" formularz jest posyłany za pomocą metody GET do serwletu oznaczonego /params1.

Prześledźmy najpierw dzialanie. Na stronie WWW wpisujemy jakieś wartości do pól tekstowych i - wysyłamy:
r

Po kliknięciu w "Wyślij..." URI zlecenia GET zostanie uzupełnione o wartości parametrów -  będziemy mogli to zobaczyć na pasku adresu przeglądarki:

http://localhost:8080/serwlety1/params1?ident=Pies&p1=a&p2=b&p3=c&p4=d&p5=e&p6=f

co spowoduje przekazanie zlecenia do serwletu

Serwlet może odczytać  tzw. queryString (czyli część URLa zlecenia GET zawierającą parametry), będzie też mógł przejrzeć wszystkie parametry, odwoływać się do ich wartości po nazwach, uzyskać mapę tych parametrów z kluczami = ich nazwom itp.

Jego metoda serviceRequest (którą, jak wcześniej, wywołujemy z metody doGet i doPost) wygląda tak:
public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
      PrintWriter out = resp.getWriter();
      out.println("Metoda: " + req.getMethod());

      out.println("Query : " + req.getQueryString());
      out.println("Parametry:");
      Enumeration pnams = req.getParameterNames();
      while (pnams.hasMoreElements()) {
        String name = (String) pnams.nextElement();
        String value = req.getParameter(name);
        out.println(name + " = "  + value);
      }
      out.println("Dostęp przez mapę");
      Map map = req.getParameterMap();
      String[] val = (String[]) map.get("ident");
      out.println("Parametr o nazwie ident");
      out.println("- z mapy uzyskujemy tablice String[]");
      out.println("- jej rozmiar " + val.length );
      out.println("- jej elementy:" );
      for (int i=0; i<val.length; i++) out.println(val[i]);

     out.close();
  }
Widzimy tu różne sposoby uzyskiwania wartości parametrów. Warte szczególnej uwagi jest to, że wartości mapy parametrów są tablicami typu String[] (oczywiście klucze to nazwy parametrów).

Ten serwlet (przy podanym formularzu) wygeneruje stronę o następującej treści:
Metoda: GET
Query : ident=Pies&p1=a&p2=b&p3=c&p4=d&p5=e&p6=f
Parametry:
p6 = f
p5 = e
p4 = d
p3 = c
ident = Pies
p2 = b
p1 = a
Dostep przez mape
Parametr o nazwie ident
- z mapy uzyskujemy tablice String[]
- jej rozmiar 1
- jej elementy:
Pies

Zwykle metodę GET stosuje się, gdy sumaryczna długość przekazywanej informacji nie jest zbyt duża (np. mniejsza od 256 znaków, choć obecnie przeglądarki dopuszczają duże zlecenia GET). Podkreślmy jeszcze raz, że - w zleceniu GET - pary nazwy-wartości parametrów sa dołączane do URLa zlecenia, a wobec tego ciało (treść) zlecenia jest puste.

Kiedy chcemy dostarczyć serwletowi informacji o nielimitowanych rozmiarach (jako parametrów lub jako dowolnej treści - ciała zlecenia) stososujemy metodę POST.
Przy tym parametry nie sa już dołączane do URLa zlecenia i getQueryString() da w serwlecie null, mogą natomiast być odczytane przez metody uzyskiwania parametrów, albo (ale nie równocześnie) - przez odczytanie treści zlecenia z jego strumienia wejściowego.

Gdy zmienimy nagłówek formularza w naszym pliku HTML
...
<form method="post" action="http://localhost:8080/serwlety1/params1">
...
i znowy poślemy go do przykładowego serwletu uzyskamy nieco inny wynik:
Metoda: POST
Query : null
// ... dalej jak w metodzie GET
Ale możemy również bezpośrednio odczytać treść zlecenia ze strumienia związanego z obiektem zlecenia:
  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
    PrintWriter out = resp.getWriter();
    out.println("Metoda: " + req.getMethod());

    // Przy zleceniu POST
    // Albo uzyskujemy parametry przez metody getParameter...
    // albo czytamy je ze strumienia jako "ciało" zlecenia
    // ale nie równocześnie i to i to

    boolean readBodyStream = true;  // czytamy ze strumienia

    if (!readBodyStream) {
       // ... poprzedni kod
    }
    else {
      out.println("Czytanie tresci (ciała) zlecenia ze strumienia:");

      BufferedReader br = req.getReader();
      String line;
      while ((line = br.readLine()) != null) out.println(line);
      br.close();
    }

    out.close();
  }
co da w wyniku:
Metoda: POST
Czytanie tresci (ciala) zlecenia ze strumienia:
ident=Pies&p1=a&p2=b&p3=c&p4=d&p5=e&p6=f
Oczywiście, treść zlecenia nie musi mieć nic wspólnego z formularzami i parametrami. Klient HTTP może posłać za pomocą metody POST dowolną informację, o dowolnym rozmiarze
Dlatego należy uważać przy obsłudze zlecenia POST (sprawdzać wielkość przesyłanej informacji), bez tego bowiem ktoś może posłac naszemu serwletowi miliony megabajtów, co oczywiście będzie stanowić problem.

Przy przesyłaniu parametrów z formularzy zetkniemy się z problemem kodowania.
Otóż dane z pól tekstowych formularza przy przesłaniu są przez przeglądarkę kodowane w taki sposób, by zastąpić "niebezpieczne bajty" specjalnie kodowanymi sekwencjami bajtów. Jest to tzw. URL-kodowanie (URLencoding). Gdybyśmy np. w naszym przykładowym serwlecie odczytującym parametry wprowadzili w polu id napis "To jest ident", a w polu p1 napis 2 > 3 (pozostawiając pola p2-p6 puste), niebezpieczne znaki spacji i > zostałuby URL-zakodowane, a parametry zlecenia wyglądałyby następująco:
ident=To+jest+ident+&p1=2+%3E+3&p2=&p3=&p4=&p5=&p6=  

Metody pobierania parametrów z klasy HttpServletRequest (m.in. getParameter()) automatycznie dokonują dekodowania paranetrów. Przy tym jednak pojawia sie poważny problem internacjonalizacji. Przeglądarka posyłając url-kodowane dane ustala nagłówek Content-Type jako application/x-www-form-urlencoded i nie przesyła żadnych danych o stronie kodowej (czy to jest np. ISO-8859-2 czy Windows-1250). Metody pobierania parametrów po stronie serwletu przyjmują przy dekodowaniu stronę ISO-8859-1, co oczywiście prowadzi do błędów, gdy dane w formularzu wymagają innej strony kodowej (np. polskie znaki w formacie ISO-8859-2). 

Trzeba zatem jakoś powiadomić metody pobierania parametrów, by przyjęły właściwą stronę kodową.

Dla właściwego pobierania paranetrów zlecenia przed użyciem metod pobierania lub czytaniem parametrów ze strumienia wejściowego należy ustalić stronę kodową zlecenia za pomocą metdosy setCharacterEncoding(...) z klasy HttpServletRequest.


Rozważmy teraz inny przykład - serwlet do testowania wyrażeń regularnych.
Stosując metodę find() ma on znaleźć w tekście (dostarczonym przez użytkownika) wszystkie podłańcuchy pasujące do wzorca (dostarczonego przez użytkownika) i przekazać użytkownikowi wyniki wyszukiwania.

Pierwsza przymiarka do rozwiązania tego zadania polega na dostarczeniu użytkownikowi strony, na której mógłby on wpisać potrzebne dane, przeprowadzeniu wyszukiwania, a następnie wygenerowaniu strony wynikowej.

Serwlet (niech będzie dostępny w kontekście serwlety1 jako regex1) pobierze parametry z następującej strony HTML
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=windows-1250">
 <title>Testowanie</title>
</head>
<body>
<center><h2>Testowanie wyrażeń regularnych</h2></center>
<hr>
<form method="post" action="http://localhost:8080/serwlety1/regex1">
Wzorzec: <br>
<input type="text" size="30" name="regex"><br>
Tekst:<br>
<input type="text" size="50" name="input"><br><br>  
<input type="submit" value="Pokaż wynik wyszukiwania">
</form>
</body></html>
i wygeneruje stronę wynikową. Metoda serviceRequest (już tradycyjnie będziemy ją stosowac jako cel odwołań zarówno z doGet jak i doPost) wygląda następująco:

  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {

    String charset = "windows-1250";

    // Uwaga. Należy ustalić właściwą stronę kodową zlecenia
    // bez tego parametry nie będą właściwie odczytane
    // Tu - ustalamy stronę windows-1250, bo nasz formularz
    // jest zapisany w takim właśnie kodowaniu

    req.setCharacterEncoding(charset);

    resp.setContentType("text/html; charset=" + charset);

    PrintWriter out = resp.getWriter();

    String regex = req.getParameter("regex");
    String input = req.getParameter("input");

    if (regex == null || input == null) {
      out.println("<h2>Wadliwe argumenty wywołania</h2>");
      out.close();
      return;
    }

    out.println("<h3>Wyrażenie:<br>\"" + regex + "\"</h3>");
    out.println("<h3>Tekst:<br>\"" + input + "\"</h3>");
    out.println("<hr>");

    try {
      Pattern pattern = Pattern.compile(regex);
      Matcher matcher = pattern.matcher(input);
      boolean found = matcher.find();
      if (!found)
         out.println("<h3>Nie znaleziono żadnego podłańcucha " +
                      "pasującego do wzorca</h3>");
      else {
          out.println("<h3>Dopasowano:</h3>");
          out.println("<ol>");
        do {
          out.println("<li>podłańcuch \"" + matcher.group() +
                   "\" od pozycji " + matcher.start() +
                   " do pozycji " + (matcher.end()-1) + "</li>");
        } while(matcher.find());
        out.println("</ol>");
      }
    } catch (PatternSyntaxException exc) {
        out.println("<h2>Błąd w wyrażeniu</h2>");
    } finally {
        out.close();
    }
  }

a działanie  serwletu ilustrują rysunki:

- wprowadzanie danych
r


- po kliknięciu "Pokaż" (strona wygenerowana przez serwlet)
r


To rozwiązanie działa, ale  ma dwie wady. Pierwsza jest natury funkcjonalnej: użytkownik dla wprowadzenia nowego wyszukiwania zmuszony jest cofać się (za pomocą przycisku Back) do poprzedniej strony, Duga jest znacznie poważniejsza, dotyczy konstrukcyjnej natury rozwiązania - zajmiemy się tym w następnym punkcie.

Aby poprawić funkcjonalność możemy skorzystać z następującej właściwości formularzy HTML: jeżeli w formularzu nie podano parametru ACTION, to domyślnie wynikiem posłania formularz jest "zwrócenie" tej samej strony na której był formulraz. Bardzo często korzysta się z tego np. przy walidacji jakichś danych rejestracyjnych.

Dla naszego serwletu wyrażeń regularnych oznacza to, że musi on generować stronę z formularzem, a jeżeli w formularzu wprowadzono parametry - uzupełniąć ją  o wyniki wyszukiwania.

Moglibyśmy to zrobić tak:
<center><h2>Testowanie wyrażeń regularnych</h2></center>
<hr>
<form method="post">
Wzorzec: <br>
<input type="text" size="30" name="regex"><br>
Tekst:<br>
<input type="text" size="50" name="input"><br><br>
<input type="submit" value="Pokaż wynik wyszukiwania">
</form>
Plik z szablonem formularza nie powinien być dostępny dla użytkownika (umieścimy go w katalogu WEB-INF). Możemy go łatwo wczytać do serwletu, korzystając ze znanej nam metody kontekstu aplikacji getResourceAsStream (zob. podpunkt 2). Nazwy tego pliku nie warto jednak statycznie wpisywać w kodzie serwletu (możemy miec różne warianty takich plików, a każda zmiana wariantu wymagałaby rekompilacji serwletu). Dostarczymy więc jej jako inicjalny parametr serwletu.

Każdy z inicjalnycch parametów serwletu specyfikujemy jako element init-param zagnieżdżony w elemencie servlet w pliku deskryptora wdrożenia web.xml
Ma on następującą postać:

       <init-param>
          <param-name>nazwaParametru</param-name>
          <param-value>wartośćParametru</param-value>
        </init-param>

Wartości parametrów inicjalnych możemy uzyskać stosując metodę klasy serwletu (ściślej  dziedziczoną z klasy GenericServlet):

    String getInitParameteter(nazwaParametru)

Możemy też uzyskać nazwy wszystkich parametrów inicjalnych za pomocą metody:

    Enumeration getInitParameterNames()

Uwaga: należy odróżniać parametry inicjalne konkretnego serwletu od inicjalnych parametrów kontekstu (czyli całej) aplikacji (na którą może składać się wiele serwletów i innych komponentów WEB).


Powiedzmy, że nasz szablon formularza umieściliśmy w pliku regexform.html w katalogu WEB-APP. Udostępnimy nazwę tego pliku jako parametr o nazwie regexFormFile, zatem w definicji serwletu w pliku web.xml dodamy odpowiedni element:

    <servlet>
       <servlet-name>RegexTest</servlet-name>
       <description>Regularne wyrazenia 1</description>
       <servlet-class>RegexTest</servlet-class>
       <init-param>
          <param-name>regexFormFile</param-name>
          <param-value>regexform.html</param-value>
       </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>RegexTest</servlet-name>
        <url-pattern>/regex1</url-pattern>
    </servlet-mapping>
Nowy kod serwletu wygląda w następujący sposób:

public class RegexTest extends HttpServlet {

  private PrintWriter out;
  private void printEndTag() { out.println("</body></html>"); }

  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {

    String charset = "ISO8859-2";
    req.setCharacterEncoding(charset);
    resp.setContentType("text/html; charset=" + charset);
    out = resp.getWriter();
    out.println("<html>");
    out.println("<head><title>Testowanie</title></head>");
    out.println("<body>");

    // Nazwę pliku z formularzem dostarczymy
    // jako parametr inicjalny serwletu
    String formFile = getInitParameter("regexFormFile");

    // Przeczytamy go i wpiszemy na generowaną stronę

    ServletContext context = getServletContext();
    InputStream in = context.getResourceAsStream("/WEB-INF/"+formFile);
    BufferedReader br = new BufferedReader( new InputStreamReader(in));
    String line;
    while ((line = br.readLine()) != null) out.println(line);

    // Pobieramy parametry formularza
    String regex = req.getParameter("regex");
    String input = req.getParameter("input");

    // Przy pierwszym odwołaniu do serwletu - parametrów nie ma
    // zatem poprzestajemy na wygenerowaniu formularza

    if (regex == null || input == null) {
      printEndTag();
      out.close();
      return;
    }

    // W przeciwnym razie jakieś parametry (chcoćby puste) już są
    // Przetwarzamy je i uzupełniamy stronę z formularzem

    out.println("<hr>");
    out.println("Wzorzec: \"" + regex + "\"<br>");
    out.println("Tekst  : \"" + input + "\"<br>");

    try {
      Pattern pattern = Pattern.compile(regex);
      Matcher matcher = pattern.matcher(input);
      boolean found = matcher.find();
      if (!found)
         out.println("<h3>Nie znaleziono żadnego podłańcucha " +
                      "pasującego do wzorca</h3>");
      else {
          out.println("<h3>Dopasowano:</h3>");
          out.println("<ol>");
        do {
          out.println("<li>podłańcuch \"" + matcher.group() +
                   "\" od pozycji " + matcher.start() +
                   " do pozycji " + (matcher.end()-1) + "</li>");
        } while(matcher.find());
        out.println("</ol>");
      }
    } catch (PatternSyntaxException exc) {
        out.println("<h2>Błąd w wyrażeniu</h2>");
    } finally {
        printEndTag();
        out.close();
    }
  }

// ... metody doGet o doPost wołające serviceRequest

}
Teraz wywołujemy serwlet bezpośrednio (proszę zwrócić uwagę na pasek adresu na poniższym rysunku) i wpisujemy dane:
r

Po wysłaniu formularza  uzyskamy nie tylko wyniki, ale poprzedzający je formularz przygotowany do wpisywania nowych danych:

r


Oczywiście, tę aplikację można funkcjonalnie rozwijać. Np. zapewniając zachowanie wprowadzanych tekstów w polach tekstowych, albo ich przywoływanie (być może z całą historią). To wszystko będzie coraz bardziej komplikowac kod naszego serwletu.
Tymczasem już teraz - jak przed chwilą wspomniano - ten prosty programik ma poważną konstrukcyjną usterkę: miesza ze sobą fragmenty kodu zajmującego się prezentacją i  fragmenty odpowiedzialne za czystą logikę (w tym przypadku wyszukiwania), Taka architektura prowadzi do serwletów dużych, zawikłanych, trudnych w modyfikacjach, nieelastycznych.
Trzeba coś z tym zrobić.


6.  Serwlety i architektura MVC


Programistycznu interfejs Servlet API nie jest dobrze przygotowany na łatwą separację modeli i widoków. Jak już mówiliśmy na samym początku rozdziału było to jednym z powodów pojawienia się technologii JSP i Java Server Faces.
Zanim jednak przyjrzymy sie tym technologiom, warto na prostym przykładzie zobaczyć i poczuć na czym taka separacja może polegać, bo - jednak -= nawet na poziomie Servlet API mamy możliwości jej dokonania.

Wróćmy do kodu serwletu wyrażeń regularnych, choćby jego następującego fragmentu, zaznaczając przez Logika lub L - fragmenty odpowiedizalne za logikę przetwarzania danych, a przez Prezentacja lub P - za prezentację.

    try {
      Pattern pattern = Pattern.compile(regex);                   // Logika
      Matcher matcher = pattern.matcher(input);                   // Logika
      boolean found = matcher.find();                             // Logika
      if (!found)                                                 // Logika
         out.println("<h3>Nie znaleziono żadnego podłańcucha " +  // Prezentacja
                      "pasującego do wzorca</h3>");
      else {                                                      // Logika
          out.println("<h3>Dopasowano:</h3>");                    // Prezentacja
          out.println("<ol>");                                    // Prezentacja
        do {
          out.println("<li>podłańcuch \"" + matcher.group() +     // L + P
                   "\" od pozycji " + matcher.start() +           // L + P
                   " do pozycji " + (matcher.end()-1) + "</li>"); // L + P
        } while(matcher.find());
        out.println("</ol>");                                     // Prezentacja
      }
    } catch (PatternSyntaxException exc) {                        // Logika
        out.println("<h2>Błąd w wyrażeniu</h2>");                 // Prezentacja
    } finally {                                                   // Logika
        printEndTag();                                            // Prezentacja
        out.close();                                              // Prezentacja
    }

Widzimy jak bardzo zmieszane ze sobą są fragmenty odpowiedzialne za logikę i prezentację.
Co się stanie, gdy będziemy chcieli dokonać prezentacji w innej formie?
Co się stanie, gdy np. wyniki wyszukiwania w ogóle nie powinny podlegać prezentacji bezpośredniej, lecz raczej winny być przekazywane jako strumień do jakiegoś klienta HTTP?
Takie zmiany będa wymagały pisania całego kodu od początku.

Zanim jednak spróbujemy stworzyć bardziej elastyczne i uniwersalen rozwiązanie, oparte na separacji kodu odpowiedzialnego za  wygląd i kodu odpowiedzialnego za logikę działania potrzebne nam będą pewne dodatkowe informacje o możliwościach współdziałania serwletów.

Mianowicie:

Poniższe tabele zawiarają syntetyczną informację o w/w cechach Servlet API.

Różnica pomiędzy parametrami i atrybutami kontekstu
Parametry
Atrybuty
Parametry mogą być ustalone wyłacznie w pliku web.xml  za pomocą elementu context-param Atrybuty mogą być ustalane dynamicznie przez serwlety (albo przez serwer).  
Wartości są typu String. Wartości są referencjami do dowolnych obiektów (ogólnie klasy Object), klucze (nazwy atrybutów) są typu String. 


Metody dotyczące atrybutów
voidsetAttribute(String name, Object value)
ObjectgetAttribute(String name)
EnumerationgetAttributeNames()
void
removeAttribute(String name)
W klasach:
ServletRequest  - dotyczą danego zlecenia
HttpSession  - dotyczą danej sesji (tego samego klienta)
 ServletContext - dotyczą całej aplikacji (wszystkich sesji i klientów)


Przekazywanie zleceń
RequestDispatcher

Uzyskujemy od kontekstu (ServletContext) lub zlecania (ServletRequest) za pomocą metody getRequestDispatcher(), podając odniesienie do zasobu (np. innego serwletu, który ma przejąć obsługę zlecenia)

 void forward(ServletRequest, ServletResponse)

Przekazuje zlecenie do obsługi przez inny aktywny komponent (serwlet). Odpowiedź nie może być zatwierdzona (commited).
Sterowanie nie wraca do "wywołującego" serwletu.


 void include(ServletRequest, ServletResponse)

Przekazuje zlecenie do obsługi  tymczasowo, po czym można kontynuować dalszą obsługę tego zlecenia w "wywołującym" serwlecie.
Również pozwala na włączanie statycznych zasobów (np. stron HTML).

Przykłady zastosowania w/w konstrukcji zostaną pokazane przy przebudowie aplikacji "testowania wyrażeń regularnych" zgodnie z wymogami architektury MVC.

Aplikację podzielimy na cztery zasadnicze części:
Chcielibyśmy przy tym zapewnić, aby:
  1. serwlet-kontroler mógł bez rekompilacji obsługiwać dowolne klasy działań (!) oraz przekazywać zadania pobierania parametrów i prezentacji wyników dowolnym serwletom pobierania parametrów i pokazywania wyników,
  2. serwlet pobierania parametrów nie był zależny od nazw i opisu parametrów (w szczególności ich zlokalizowanych opisów),
  3. serwlet prezentacji mógł być wykorzystywany do prezentacji dowolnych wyników.
Zacznijmy od trudnego (wydawałoby się) zadania uniezaleznienia kontrolera od rodzaju wykonywanych działań. W tym celu wykorzystamy znany z literatury wzorzec projektowy Command (m.in. przedstawiany przez Bruce'a Tate w ciekawej książce "Bitter Java"; tutaj zdecydowanie jednak rozbudujemy zawarte tam sugestie). Mianowicie, wprowadzimy interfejs Command, który opisuje funkcjonalność szerokiej klasy (różnorodnych) działań.

import java.util.*;

public interface Command {
   void init();
   void setParameter(String name, Object value);
   Object getParameter(String name);
   void execute();
   List getResults();
   void setStatusCode(int code);
   int getStatusCode();
}
Zatem każda klasa dzialania powinna implementować metody inicjacji, ustalania i pobierania ew. parametrów, wykonania działania, ustalenia i pobrania kodu wyniku, pobrania wyniko działania. Umówimy się, że wyniki będą dostępne jako lista.

Dla ułatwienia implementacji tych metod w konkretnych klasach dostarczymy ich standardowej implementacji, która m.in. zawiera  mapę parametrów i listę wyników i dostarcza gotowych i wystarczających definicji metod ustalania, pobierania parametrów i wyników, a także dodatkowych metod pozwalających tworzyć elementy listy  wyników. Przyjmieny, że w tej standardowej implementacji każdy element wyników stanowi tablicę dowolnych obiektów, uwzględniając przy tym, że częstym przypadkiem elementu wyników będzie zwykły napis (stąd przeciążana metoda addResult). Klasę moglibyśmy uczynić abstrakcyjną, bowiem nieznane są jeszcze metody inicjacji (init()) oraz  wykonania działań (execute()), ale wygodnie będzie potraktowac ją raczej jako adapter, dostarczając pustych definicji tych metod.

import java.util.*;
import java.io.*;

public class CommandImpl implements Serializable, Command {

  private Map parameterMap = new HashMap();
  private List resultList = new ArrayList();

  private int statusCode;

  public CommandImpl() {}

  public void init() {}

  public void setParameter(String name, Object value) {
    parameterMap.put(name, value);
  }

  public Object getParameter(String name) {
    return parameterMap.get(name);
  }

  public void execute() {}

  public List getResults() {
    return resultList;
  }

  public void addResult(Object o) {
    resultList.add(o);
  }

  public void addResult(String s) {
    addResult(new Object[] { s } );
  }


  public void clearResult() {
    resultList.clear();
  }

  public void setStatusCode(int code) {
    statusCode = code;
  }

  public int getStatusCode() {
    return statusCode;
  }

}

Taką standardową implementację interfejsu Command może teraz odziedziczyć nasza konkretna klasa wyszukiwania wyrażeń regularnych.

Działanie które wykonuje obiekt tej klasy ma (niewątpliwie) dwa parametry: regularne wyrażenia i przeszukiwany tekst. Wyniki wyszukiwania będą przedstawiane na liście, której kolejne elementy to tablice trzyelementowe, zawierające kolejny znaleziony podłańcuch, pozycję na której się on zaczyna i pozycję na której się kończy. Ustalimy również kody wyniku: 0 - znaleziono jedno lub więcej dopasowań, 1 -  brak parametrów, 2 - błąd w wyrażeniu, 3 - brak dopasowania.
import java.util.*;
import java.io.*;
import java.util.regex.*;

public class FindCommand extends CommandImpl implements Serializable {

  public FindCommand() {}

  public void execute() {
    clearResult();
    String regex = (String) getParameter("regex");
    String input = (String) getParameter("input");
    if (regex == null || input == null) {
      setStatusCode(1);
      return;
    }
    Pattern pattern;
    try {
      pattern = Pattern.compile(regex);
    } catch (PatternSyntaxException exc) {
       setStatusCode(2);
       return;
    }
    Matcher matcher = pattern.matcher(input);
    boolean found = matcher.find();
    if (!found) setStatusCode(3);
    else {
      setStatusCode(0);
      do {
        addResult( new Object[] { "\"" + matcher.group() + "\"",
                                  new Integer(matcher.start()),
                                  new Integer(matcher.end()-1)
                                });

      } while(matcher.find());
    }
  }

}
Tę klasę możemy wykorzystać w najprzeróźniejszy sposób. Teraz zastosujemy ją jako element aplikacji WEB.
Dzialaniem aplikacji będzie zarządzał serwlet-kontroler, przy czym - jak wspomniano - jego kod uczynimy niezależnym od sposobu pobierania parametrów (serwletu pobierania parametrów), wykonywanych działań (klasy działania) oraz sposobu prezentacji wyników (serwletu-prezentacji). Niezależność tę uzyskamy dostarczając inicjalnych parametrów kontekstu w deskryptorze wdrożenia (web.xml):

     .....
    <context-param>
       <param-name>presentationServ</param-name>
       <param-value>/presentation</param-value>
    </context-param>

    <context-param>
       <param-name>getParamsServ</param-name>
       <param-value>/getparams</param-value>
    </context-param>

    <context-param>
       <param-name>commandClassName</param-name>
       <param-value>FindCommand</param-value>
    </context-param>
    .....
 i pobierając je przy inicjacji serwletu

public class ControllerServ extends HttpServlet {

  private ServletContext context;
  private Command command;            // obiekt klasy dzialania
  private String presentationServ;    // nazwa serwlet prezentacji
  private String getParamsServ;       // mazwa serwletu pobierania parametrów
  private Object lock = new Object(); // semafor dla synchronizacji
                                      // odwołań wielu wątków
  public void init() {

    context = getServletContext();

    presentationServ = context.getInitParameter("presentationServ");
    getParamsServ = context.getInitParameter("getParamsServ");
    String commandClassName = context.getInitParameter("commandClassName");

    // Załadowanie klasy Command i utworzenie jej egzemplarza
    // który będzie wykonywał pracę
    try {
      Class commandClass = Class.forName(commandClassName);
      command = (Command) commandClass.newInstance();
    } catch (Exception exc) {
        throw new NoCommandException("Nie mogę stworzyć obiektu klasy " +
                                      commandClassName);
    }
  }

// ...

Zwróćmy uwagę, że piszemy ten kod w kategoriach interfejsu Command. Dynamicznie ładujemy klasę podaną jako parametr kontekstu (tu FindCommand) i tworzymy jej obiekt. Zmiana wykonywanego działania (np. zamiast wyszukiwania jakieś obliczenia matematyczne) wymaga tylko podania innej wartości parametru kontekstu commandClassName. Może się okazać, że podamy niewłaściwą nazwę klasy lub będzie ona źle zbudowana (np. brak konstruktora bezparaemtrowego). Wtedy - jak już wiemy - wystąpi wyjątek ClassNotFoundException lub InstantiantionException. Obsługujemy oba te wyjątki poprzez zgłoszenie własnego wyjątku NoComamndException.
public class NoCommandException extends RuntimeException {
  public NoCommandException() { super(); }
  public NoCommandException(String msg) { super(msg); }
}
Jak widać, klasę wyjątku uczyniliśmy pochodną od RunTimeException, dzięki czemu nie musimy go ani obsługiwać w tym miejscu ani zgłaszać w klauzuli throws metody init(), co byłoby niedozowolone (bowiem metoda init() w klasie GenericServlet deklaruje możliwość zgłaszania wyjątku klasy ServletException, a jej przedefiniowanie nie może tego zmienić). Ten sposób oprogramowania umożliwia np. przygotowanie strony HTML z komunikatem o przyczynach błędu, która będzie automatycznie ładowana jeśli użyjemy elementu error-pages w pliku deskryptora wdrożenia.

Obsługa przychodzących zleceń polega na wywołaniu serwletu pobierania parametrów, ustaleniu tych parametrów dla obiektu typu Command, wykonaniu działań (metoda execute() z Command), pobraniu wyników i udostępnieniu ich serwletowi prezentacji, któremu na samym końcu przekażemy sterowanie. Parametry będą zapisywane przez serwlet pobierania parametrów jako atrybuty sesji z przedrostkiem param_.  Uniezależnimy kody serwletów od nazw parametrów: wygodnym rozwiązaniem będzie zastosowanie ResourceBundle, bo przy okazji zinternacjonalizujemy całą aplikację. Po to by wygodnie sięgać po zlokalizowaną i sparametryzowaną informację z różnych serwletów naszej aplikacji przygotowano własną klasę BundleInfo, która tę informację porządkuje (więcej o tym za chwilę). Wreszcie serwlet-kontroler musi udostępnić listę wyników serwletowi prezentacji. Tu również zastosujemy atrybut sesji (naturalnie - parametry i wyniki są związane ze zleceniami od jednego i tego samego klienta).
Kod obsługi zleceń w serwlecie-kontrolerze wygląda więc w następujący sposób.
public class ControllerServ extends HttpServlet {

  private ServletContext context;
  private Command command;            // obiekt klasy wykonawczej
  private String presentationServ;    // nazwa serwlet prezentacji
  private String getParamsServ;       // mazwa serwletu pobierania parametrów
  private Object lock = new Object(); // semafor dla synchronizacji
                                      // odwołań wielu wątków
  public void init() {

    context = getServletContext();

    presentationServ = context.getInitParameter("presentationServ");
    getParamsServ = context.getInitParameter("getParamsServ");
    String commandClassName = context.getInitParameter("commandClassName");

    // Załadowanie klasy Command i utworzenie jej egzemplarza
    // który będzie wykonywał pracę
    try {
      Class commandClass = Class.forName(commandClassName);
      command = (Command) commandClass.newInstance();
    } catch (Exception exc) {
        throw new NoCommandException("Couldn't find or instantiate " +
                                      commandClassName);
    }
  }

  // Obsługa zleceń
  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {

    resp.setContentType("text/html");

    // Wywolanie serwletu pobierania parametrów
    RequestDispatcher disp = context.getRequestDispatcher(getParamsServ);
    disp.include(req,resp);

    // Pobranie bieżącej sesji
    // i z jej atrybutów - wartości parametrów
    // ustalonych przez servlet pobierania parametrów
    // Różne informacje o aplikacji (np. nazwy parametrów)
    // są wygodnie dostępne poprzez własną klasę BundleInfo

    HttpSession ses = req.getSession();

    String[] pnames = BundleInfo.getCommandParamNames();
    for (int i=0; i<pnames.length; i++) {

      String pval = (String) ses.getAttribute("param_"+pnames[i]);

      if (pval == null) return;  // jeszcze nie ma parametrów

      // Ustalenie tych parametrów dla Command
      command.setParameter(pnames[i], pval);
    }

    // Wykonanie działań definiowanych przez Command
    // i pobranie wyników
    // Ponieważ do serwletu może naraz odwoływać sie wielu klientów
    // (wiele watków) - potrzebna jest synchronizacja
    // przy czym rrygiel zamkniemy tutaj, a otworzymy w innym fragmnencie kodu
    // - w serwlecie przentacji (cały cykl od wykonania cmd do poazania wyników jest sekcją krytyczną) 

    Lock mainLock = new ReentrantLock();

    mainLock.lock();
      // wykonanie
      command.execute();

      // pobranie wyników
      List results = (List) command.getResults();

      // Pobranie i zapamiętanie kodu wyniku (dla servletu prezentacji)
      ses.setAttribute("StatusCode", new Integer(command.getStatusCode()));

      // Wyniki - będą dostępne jako atrybut sesji
      ses.setAttribute("Results", results);
      ses.setAttribute("Lock", mainLock);    // zapiszmy lock, aby mozna go było otworzyć później


    // Wywołanie serwletu prezentacji
    disp = context.getRequestDispatcher(presentationServ);
    disp.forward(req, resp);
  }


  public void doGet(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

  public void doPost(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

}
Widzimy, że opisuje on wyłącznie logikę działania, bez elementów prezentacji (drobnym, niestety nieuniknionym wyjątkiem jest ustalenie content-type odpowiedzi na "text/html"; w przeciwnym razie metoda include RequestDispatchera włączy źrólo generowanej przez serwlet pobierania parametrów strony, a nie zinterpertowany HTML).
Praktycznie ten serwlet-kontroler jest na tyle niezależny od konkretów, że nadaje się do zastosowania w niemal dowolnych sytuacjach pobierania danych wejściowych, wykonania na nich jakichś działan i prezentacji ich wyników.

Serwlety pobierania parametrów i  prezentacji wyników są już  bardziej skonkretyzowane.
Zakładamy, że parametry będą pobierane z formularza, a wyniki prezentowane po tym formularzu jako lista. Będziemy jednak chcieli uniezależnić oba serwlety od liczby, nazw, opisów pobieranych parametrów oraz liczby, rodzaju i opisów wyników.
Taką sparametryzowaną informację dostarczymy poprzez ResourceBundle, który - dla danej lokalizacji (języka) zlecenia  będzie odczytywany (tylko przy zmianie sesji) przez dodatkowy serwlet. Przy okazji odczytaną informację zapiszemy w obiekcie klasy BundleInfo, przez co będziemy mieli wygodny do niej dostęp z innych serwletów.

Oczywiście, trzeba przyjąć jakąś konwencje opisu aplikacji w ResourceBundle.
Wyróżnimy następujące elementy:
Przygotowana zgdnie z tą konwencją dla lokalizacji polskiej klasa zasobowa wygląda następująco:

public class RegexParamsDef_pl extends ListResourceBundle {
     public Object[][] getContents() {
         return contents;
     }

    static final Object[][] contents = {
       { "charset", "ISO-8859-2" },
       { "header", new String[] { "Testowanie wyrażeń regularnych" } },
       { "param_regex", "Wzorzec:" },
       { "param_input", "Tekst:" },
       { "submit", "Pokaż wyniki wyszukiwania" },
       { "footer", new String[] { } },
       { "resCode", new String[]
                    { "Dopasowano", "Brak danych",
                      "Wadliwy wzorzec", "Nie znaleziono dopasowania" }
                    },
       { "resDescr",
            new String[] { "podłańcuch", "od poz.", "do poz.", "" } },
    };
}

a serwlet czytający ResourceBundle i gromadzący informację w klasie pomocniczej BundleInfo przedstawia poniższy fragmeny:

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;


class BundleInfo {

  static private String[] commandParamNames;
  static private String[] commandParamDescr;
  static private String[] statusMsg;
  static private String[] headers;
  static private String[] footers;
  static private String[] resultDescr;
  static private String charset;
  static private String submitMsg;

  static void generateInfo(ResourceBundle rb) {

    synchronized (BundleInfo.class) {  // konieczne ze względu
                                       // na możliwość odwołań
      List cpn = new ArrayList();      // z wielu egzemplarzy serwletów
      List cpv = new ArrayList();
      Enumeration keys = rb.getKeys();
      while (keys.hasMoreElements()) {
        String key = (String) keys.nextElement();
        if (key.startsWith("param_")) {
          cpn.add(key.substring(6));
          cpv.add(rb.getString(key));
        }
        else if (key.equals("header")) headers = rb.getStringArray(key);
        else if (key.equals("footer")) footers = rb.getStringArray(key);
        else if (key.equals("resCode")) statusMsg = rb.getStringArray(key);
        else if (key.equals("resDescr")) resultDescr = rb.getStringArray(key);
        else if (key.equals("charset")) charset = rb.getString(key);
        else if (key.equals("submit")) submitMsg = rb.getString(key);
      }
      commandParamNames = (String[]) cpn.toArray(new String[0]);
      commandParamDescr = (String[]) cpv.toArray(new String[0]);
    }
  }

  public static String getCharset() {
    return charset;
  }

  public static String getSubmitMsg() {
    return submitMsg;
  }

    public static String[] getCommandParamNames() {
    return commandParamNames;
  }

  public static String[] getCommandParamDescr() {
    return commandParamDescr;
  }

  public static String[] getStatusMsg() {
    return statusMsg;
  }

  public static String[] getHeaders() {
    return headers;
  }

  public static String[] getFooters() {
    return footers;
  }

  public static String[] getResultDescr() {
    return resultDescr;
  }

}


// Serwlet włączany wyłącznie z serwletu pobierania parametrów
// Ładuje  ResourceBundle i przekazuje go klasie BundleInfo,
// która odczytuje info i daje wygodną formę jej pobierania
// w innych serwletach.
// Ładowanie zasobów i ich przygotowanie przez klasę BundleInfo
// następuje tylko raz na sesję.


public class ResourceBundleServ extends HttpServlet {

  private String resBundleName;

  public void init() {
    resBundleName = getServletContext().getInitParameter("resBundleName");
  }

  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
    HttpSession ses = req.getSession();
    ResourceBundle paramsRes = (ResourceBundle) ses.getAttribute("resBundle");

    // W tej sesji jeszcze nie odczytaliśmy zasobów
    if (paramsRes == null) {
       Locale loc = req.getLocale();
       paramsRes = ResourceBundle.getBundle(resBundleName, loc);
       ses.setAttribute("resBundle", paramsRes);

       // Przygotowanie zasobów w wygodnej do odczytu formie
       BundleInfo.generateInfo(paramsRes);
    }

    // ... a jeśli sesja się nie zmieniła - to nie mamy nic do roboty
  }
//...
}

Serwlet ten zostanie uruchomiony na wstępie serwletu pobierania parametrów. Przygotowana informacja posłuży do wygenerowania strony z formularzem.

// SERWLET POBIERANIA PARAMETRÓW

public class GetParamsServ extends HttpServlet {

  private ServletContext context;
  private String resBundleServ;    // nazwa serwletu przygotowującego
                                   // sparametryzowaną informacje


  // Inicjacja
  public void init() {
    context = getServletContext();
    resBundleServ = context.getInitParameter("resBundleServ");
  }

  // Obsługa zleceń
  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {


    // Włączenie serwletu przygotowującego informacje z z zasobów
    // (ResourceBundle). Informacja będzie dostępna poprzez
    // statyczne metody klasy BundleInfo

    RequestDispatcher disp = context.getRequestDispatcher(resBundleServ);
    disp.include(req, resp);

    // Pobranie potrzebnej informacji
    // ktora została wczesniej przygotowana
    // przez klasę BundleInfo na podstawie zlokalizowanych zasobów

    // Zlokalizowana strona kodowa
    String charset = BundleInfo.getCharset();

    // Napisy nagłówkowe
    String[] headers = BundleInfo.getHeaders();

    // Nazwy parametrów (pojawią się w formularzu,
    // ale również są to nazwy parametrów dla Command)
    String[] pnames = BundleInfo.getCommandParamNames();

    // Opisy parametrów - aby było wiadomo co w formularzu wpisywać
    String[] pdes   = BundleInfo.getCommandParamDescr();

    // Napis na przycisku
    String submitMsg = BundleInfo.getSubmitMsg();

    // Ew. końcowe napisy na stronie
    String[] footers = BundleInfo.getFooters();

    // Ustalenie właściwego kodowania zlecenia
    // - bez tego nie będzie można własciwie odczytać parametrów
    req.setCharacterEncoding(charset);

    // Pobranie aktualnej sesji
    // w jej atrybutach są/będą przechowywane
    // wartości parametrów

    HttpSession session = req.getSession();

    // Generowanie strony

    resp.setCharacterEncoding(charset);
    PrintWriter out = resp.getWriter();

    out.println("<center><h2>");
    for (int i=0; i<headers.length; i++)
       out.println(headers[i]);
    out.println("</center></h2><hr>");

       // formularz
    out.println("<form method=\"post\">");
    for (int i=0; i<pnames.length; i++) {
     out.println(pdes[i] + "<br>");
     out.print("<input type=\"text\" size=\"30\" name=\"" +
                   pnames[i] +  "\"");

       // Jezeli są już wartości parametrów - pokażemy je w formularzu
      String pval = (String) session.getAttribute("param_"+pnames[i]);
      if (pval != null) out.print(" value=\"" + pval + "\"");
      out.println("><br>");
    }
    out.println("<br><input type=\"submit\" value=\"" + submitMsg + "\">");
    out.println("</form>");

    // Pobieranie parametrów z formularza

    for (int i=0; i<pnames.length; i++) {
      String paramVal = req.getParameter(pnames[i]);
         // Jeżeli brak parametru (ów) - konczymy
      if (paramVal == null) return;

      // Jest parametr - zapiszmy jego wartość jako atrybut sesji.
      // Zostanie on pobrany przez Controller
      // który ustali te wartości dla wykonania Command

      session.setAttribute("param_" + pnames[i], paramVal);

    }
  }

  //..metody doGet i doPost - wywołują serviceRequest
}

Serwlet przentacji najpierw przekazuje zadanie wygenrowania formularza serwletowi pobierania parametrów, po czym prezentuje wyniki zapisane w atrybutach sesji przez serwer-kontroler.
public class ResultPresent extends HttpServlet {


  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
    ServletContext context = getServletContext();

    // Włączenie strony generowanej przez serwlet pobierania parametrów
    // (formularz)
    String getParamsServ = context.getInitParameter("getParamsServ");
    RequestDispatcher disp = context.getRequestDispatcher(getParamsServ);
    disp.include(req,resp);

    // Uzyskanie wyników i wyprowadzenie ich
    // Controller po wykonaniu Command zapisał w atrybutach sesji
    // - referencje do listy wyników jako atrybut "Results"
    // - wartośc kodu wyniku wykonania jako atrybut "StatusCode"

    HttpSession ses = req.getSession();
    Lock mainLock = (Lock) ses.getAttribute("Lock");
    mainLock.unlock();
    List results = (List) ses.getAttribute("Results");
    Integer code = (Integer) ses.getAttribute("StatusCode");


    PrintWriter out = resp.getWriter();
    out.println("<hr>");

    // Uzyskanie napisu właściwego dla danego "statusCode"
    String msg = BundleInfo.getStatusMsg()[code.intValue()];
    out.println("<h2>" + msg + "</h2>");

    // Elementy danych wyjściowych (wyników) mogą być
    // poprzedzane jakimiś opisami (zdefiniowanymi w ResourceBundle)
    String[] dopiski = BundleInfo.getResultDescr();

    // Generujemy raport z wyników
    out.println("<ul>");
    for (Iterator iter = results.iterator(); iter.hasNext(); ) {
      out.println("<li>");

      int dlen = dopiski.length;  // długość tablicy dopisków
      Object res = iter.next();
      if (res.getClass().isArray()) {  // jezeli element wyniku jest tablicą
        Object[] res1 = (Object[]) res;
        int i;
        for (i=0; i < res1.length; i++) {
          String dopisek = (i < dlen ? dopiski[i] + " " : "");
          out.print(dopisek + res1[i] + " ");
        }
        if (dlen > res1.length) out.println(dopiski[i]);
      }
      else {                                      // może nie być tablicą
        if (dlen > 0) out.print(dopiski[0] + " ");
        out.print(res);
        if (dlen > 1) out.println(" " + dopiski[1]);
      }
      out.println("</li>");
    }
    out.println("</ul>");
  }


  public void doGet(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

  public void doPost(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

}
Uruchamiając tę aplikację uzyskamy znany nam już obraz  dzialania (stronę z formy\ularzem , która po wpisaniu regularnego wyrażenia i przeszukiwanego tekstu zostanie uzupełniona o listę komunikatów o odnalezionych dopasowaniach).
r

 Oczywiście, cała aplikacja jest teraz dość rozbudowana, a nawet skomplikowana. Jednak dzięki  dodatkowemu wysiłkowi wlożonemu w separacje logiki działania, prezentacji oraz elementów opisowych zmiany jej funkcjonalności są obecnie bardzo łatwe, Wyniki możemy np. prezentować w tabeli (co wymaga tylko niewielkich modyfikacji kodu serwletu prezentacji, inne komponenty nie ulegają zmianom). Możemy inaczej pobierać parametry (co wymaga zmian tylko w serwlecie GetParamServ). Nawet całkowita zmiana wykonywanego przez aplikację zadania jest niezwykle prosta i nie zabierze więcej niż kilka minut. Zobaczmy to na przykładzie zadania wykonywania kilku operacji na wprowadzanych tekstach (powiedzmy połączenie dwóch tekstów i wykonaniu wybranej operacji - zmiany wielkości liter lub wyodrębnienia słów).
Kody wszystkich serwletów pozostają bez zmian. Musimy jedynie dostarczyć nowej implementacji interfejsu Command  (klasę nazwiemy StringComamnd i podamy te nazwę w parametrze kontekstu) oraz pliku zasobowego z opisem aplikacji (dla lokalizacji polskiej - StringCmdDef_pl).

import java.util.*;

public class StringCmdDef_pl extends ListResourceBundle {
     public Object[][] getContents() {
         return contents;
     }

    static final Object[][] contents = {
       { "charset", "ISO-8859-2" },
       { "header", new String[] { "Działania na Stringach" } },
       { "param_input1", "Tekst 1:" },
       { "param_input2", "Tekst 2:" },
       { "param_cmd", "Polecenie:" },
       { "submit", "Wykonaj" },
       { "footer", new String[] { } },
       { "resCode", new String[]
                    { "Wynik:", "Brak danych",
                      "Wadliwe polecenie, dostępne: upper, lower, words" }
                    },
       { "resDescr",
            new String[] { "" } },
    };
}

import java.io.*;
import java.util.*;

public class StringCommand extends CommandImpl implements Serializable {

  public StringCommand() {}

  public void execute() {
    clearResult();
    String input1 = (String) getParameter("input1");
    String input2 = (String) getParameter("input2");
    String cmd =   (String) getParameter("cmd");
    if (input1 == null || input2 == null || cmd == null) {
      setStatusCode(1);
      return;
    }


   String input = input1 + " " + input2;


   setStatusCode(0);
   if (cmd.equals("upper")) addResult(input.toUpperCase());
   else if (cmd.equals("lower")) addResult(input.toLowerCase());
   else if (cmd.equals("words")) {
     StringTokenizer st = new StringTokenizer(input);
     while (st.hasMoreTokens())  addResult(st.nextToken());
   }
   else setStatusCode(2);
 }

}

Nasza aplikacje bedzie teraz działać tak:

r


Naturalnie ten sposób programowania (separującego działanie i prezentację) nie jest w przypadku "czystego" Servlet API bardzo łatwy. Dlatego właśnie pojawiły się technologie JSP, a szczególnie Java Server Faces. Powiemy o nich kilka słów już za chwilę.


7. Serwlety i bazy danych

Aplikacje WEB mogą łączyć się z bazami danych, pobierać i prezentować informacje bazodanowe, udostępniac interfejsy modyfikacji baz. Jest to bardzo naturalne zastosowanie aplikacji WEB.
Przy programowaniu webowych aplikacji bazodanowych należy jednak zwrócić uwagę na pewne specyficzne cechy ich dzialania.
Zobaczmy najpierw jak nie należy takich aplikacji programować.

Będziemy korzystać ze znanej już nam bazy danych książek (dostęp przez jdbc/ksidb - zob. rozdział o JDBC) i bazy Derby w trybie klient serwer.
Uwaga: aby mieć dostęp z poziomu aplikacji WEB w środowisku Tomcata pakiet sterownika (np.  derbyclient.jar) należy umieścić w katalogu lib Tomcata.

Przeniesiony z jakichś prostych, niesieciowych doświadczeń dostępu do baz danych sposób programowania mógłby opierać sie na kolejnych krokach: w trakcie inicjacji serwletu załadować sterownik JDBC i uzyskać połączenie, przy obsłudze zleceń generować zapytania i pokazywać wyniki, przy usunięciu serwletu - zamknąć połaczenie.
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.sql.*;
import javax.sql.*;

public class DbServlet1 extends HttpServlet {

  String url = "jdbc:derby://localhost/ksidb";
  String uid = "APP";
  String pwd = "APP";
  Connection con;


  public void init() throws ServletException {
    try {
     con = DriverManager.getConnection(url, uid, pwd);  //  JDBC 4.0 - dostęp przez SPI
    } catch (Exception exc) {
        throw new ServletException("Nie ustanowiono połaczenia z bazą", exc);
    }
  }


  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
   resp.setContentType("text/html; charset=windows-1250");
   PrintWriter out = resp.getWriter();
   out.println("<h2>Lista dostępnych książek</h2>");
   String sel = "select * from pozycje";
   out.println("<ol>");
   try  {
      Statement stmt = con.createStatement();
      ResultSet rs = stmt.executeQuery(sel);
      while (rs.next())  {
        String tytul = rs.getString("tytul");
        float cena  = rs.getFloat("cena");
        out.println("<li>" + tytul + " - cena: " + cena + "</li>");
      }
      rs.close();
      stmt.close();
    } catch (SQLException exc)  {
       out.println(exc.getMessage());
    }
    out.close();
  }

   public void destroy() {
     try {
       con.close();
     } catch (Exception exc) {}
   }


  public void doGet(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

  public void doPost(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

}

To rozwiązanie jest wadliwe, gdyż:
Zatem na pewno potrzebne jest inne podejście: otwieranie połączenia bazodanowego przy każdym zleceniu, wykonanie zlecenia (dostęp do BD), zamknięcia połączenia.

 // Drugie wadliwe rozwiązanie
 
 public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
   resp.setContentType("text/html; charset=windows-1250");
   PrintWriter out = resp.getWriter();
   out.println("<h2>Lista dostępnych książek</h2>");

   Connection con;
   String sel = "select * from pozycje";
   try  {
      con = DriverManager.getConnection(...);

      Statement stmt = con.createStatement();
      ResultSet rs = stmt.executeQuery(sel);
      out.println("<ol>");
      while (rs.next())  {
        String tytul = rs.getString("tytul");
        float cena  = rs.getFloat("cena");
        out.println("<li>" + tytul + " - cena: " + cena + "</li>");
      }
      rs.close();
      stmt.close();
      con.close();
    } catch (Exception exc)  {
       out.println(exc.getMessage());
    }
    out.close();
  }

To rozwiązanie jest jednak również niewłaściwe. Nie tylko dlatego, że (tak jak poprzednio) zawiera statycznie zapisane w kodzie nazwy zasobów i nie przestrzega reguł architektury MVC (z tym umiemy sobie już radzić), ale przede wszystkim dlatego, że jest niefektywne i nieskalowalne. Uzyskanie połączenia z bazą danych jest bowiem operacją relatywnie kosztowną czasowo, zatem przy dużej, rosnącej liczbie zleceń ta aplikacja będzie znacząco tracić na efektywności działania.

Właściwym rozwiązaniem problemu jednak jest uzyskiwania połaczeń przy każdym zleceniu, a po jego obsłudze "zamykanie" połączenia, ale bez strat efektywności. Można to osiągnąć poprzez dynamiczne prowadzenie puli połączeń i ponowne wykorzystanie połączeń z puli. Fizyczne  (czasowo kosztowne) łączenie jest przy tym minimalizowane: uzyskane połączenia znajdują się w puli połączeń i mogą być ponownie wykorzystane, jeśli są akurat wolne. Zamknięcie połaczenie (z perspektywy serwletu) jest tak naprawdę zwróceniem połączenia do puli.

Można oczywiście samodzielnie napisać "broker" obsługujący pulę połaczeń i dostarczający ich serwletom. Są jednak już gotowe, ogólniedostępne rozwiązania, np.DbConnectionBroker, który można pobrać z http://www.javaexchange.com (godny polecenia, szczególnie ze względu na możliwość przyjrzenia się kodowi tego programu, który odznacza się prostotą i funkcjonalnością).

Jest też inny sposób. Mianowicie JDBC daje możliwość uzyskiwania połaczeń z BD poprzez tzw. źródło danych (obiekt typu DataSource). Służy to z jednej strony separacji  parametrów połączeń od kodu interfejsu bazodanowego, z drugiej odpowiednie implementacje DataSource zapewniają automatyczny pooling połączeń.

Takie implementacje są dostarczane praktycznie przez wszystkie serwery aplikacji.  Również Tomcat w wersji5 daje nam możliwość korzystania z dynamicznej puli połączeń. Wymaga to jednak pewnych zabiegów konfiguracyjnych - i oczywiście - już innego kodu uzyskania połaczenia.

Przede wszystkim źródła danych muszą być odpowiednio zdefiniowane i opisane w plikach konfiguracyjnych. W szczególności można to zrobić dostarczając odpowiednich elementów w pliku-deskryptorze kontekstu.
Co musimy podać:
Odpowiedni plik deskryptora kontekstu (który może posłużyc do instalacji aplikacji za pomocą zadania Anta install-config lub jej wdrożenia, chociażby poprzez umieszczenie deksryptora kontekstu w katalogu webapps) wygląda następująco (aplikacja rezyduje  w katalogu E:/Programming/webaps/SERWLETY/db/build i będzie dostępna w kontekście /db):
<Context path="/db" docBase="E:/Programming/webaps/SERWLETY/db/build">

  <Resource name="jdbc/ksidb" auth="Container" 
            type="javax.sql.DataSource"
            description="Baza danych ksiazek" 
            driverClassName="org.apache.derby.jdbc.ClientDriver"
            url="jdbc:derby://localhost/ksidb"
            username="APP"
            password="APP"
            maxActive="20" /> 
</Context>


Dostęp do żródła danych z poziomu aplikacji uzyskujemy za pomocą JNDI (java naming and directory interface).
Jest to technologia, która pozwala na poziomie logicznym, odseparowanym od fizycznych obiektów, identyfikować i uzyskiwac dostęp do różnorodnych zasobów (komputerów, użytkowników, baz danych, serwisów, systemów plikowych, aplikacji).

Zasoby są identyfikowane przez nazwy, te zaś są wiązane z konkretnymi obiektami poprzez tzw. konteksty JNDI.

Odnalezienie zasobu polega na wywołaniu metody lookup() w danym kontekście JNDI.

Ważnym kontekstem jest kontekst inicjalny (initial context), który pozwala odnajdywać inne konteksty.
W szczególności w środowisku Tomcat istnieje specjalny kontekst JNDI, który stanowi zbiór powiązań pomiedzy nazwami zasobów a odpowiadającymi im konkretnymi obiektami.
Kontekst ten nazywa się java:comp/env .
To właśnie "pod" tym kontekstem znajdować będą się żródła danych związane z bazami.

Uwaga: oczywiście nie należy mylić kontekstów JNDI z kontektsami aplikacji WEB


Zatem najpierw musimy uzyskać inicjalny kontekst, od niego - kontekst java:comp/env, i w tym kontekście odszukać powiązanie naszej bazy danych i uzyskać właściwy dla niej DataSource: 
      Context init = new InitialContext();
      Context contx = (Context) init.lookup("java:comp/env");
      DataSource dataSource = (DataSource) contx.lookup("jdbc/ksidb");

Te dzialania (relatywnie kosztowne czasowo) wykonamy w części inicjacyjnej serwletu.

Uzyskany DataSource zapewnia automatyczny pooling połaczeń, zatem łączenia z bazą będziemy spokojnie dokonywać przy każdym zleceniu, a po jego obsłudze - połączenie zamykać.
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import javax.naming.*;
import java.sql.*;
import javax.sql.*;

public class DbServlet3 extends HttpServlet {

  DataSource dataSource;  // źrodło danych

  public void init() throws ServletException {
    try {
      Context init = new InitialContext();
      Context contx = (Context) init.lookup("java:comp/env");
      dataSource = (DataSource) contx.lookup("jdbc/ksidb");
     } catch (NamingException exc) {
        throw new ServletException(
          "Nie mogę uzyskać źródła java:comp/env/jdbc/ksidb", exc);
     }
  }

  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
    resp.setContentType("text/html; charset=windows-1250");
    PrintWriter out = resp.getWriter();
    out.println("<h2>Lista dostępnych książek</h2>");

    Connection con = null;
    try {
      synchronized (dataSource) {
        con = dataSource.getConnection();
      }
      Statement stmt = con.createStatement();
      ResultSet rs = stmt.executeQuery("select * from pozycje");
      out.println("<ol>");
      while (rs.next())  {
        String tytul = rs.getString("tytul");
        float cena  = rs.getFloat("cena");
        out.println("<li>" + tytul + " - cena: " + cena + "</li>");
      }
      rs.close();
      stmt.close();
    } catch (Exception exc)  {
       out.println(exc.getMessage());
    } finally {
        try { con.close(); } catch (Exception exc) {}
    }

    out.close();


  }


  public void doGet(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

  public void doPost(HttpServletRequest request,
                    HttpServletResponse response)
                 throws ServletException, IOException
  {
      serviceRequest(request, response);
  }

}
W powyższym kodzie zwróćmy uwagę na potrzebę synchronizacji: obiekt DataSource może być wspóldzielony przez wiele wątków, zatem musimy synchroniziwać odwołania do niego.

Możemy się przekonać, że ta aplikacja dziala - po wywołaniu serwletu uzyskamy następujący wynik:
r


Bardzo łatwo możemy użyć przedstawionego w poprzednim punkcie szablonu "serwletów MVC" w zastosowaniach bazodanowych. Trzeba tylko dostarczyć odpowiedniej implementacji interfejsu Command. Załóżmy, że nasza klasa działania na BD może wykonywać polecenia select i insert przekazane przez parametr "command". Wybrane dzialanie będzie wykonywane przy wywołaniu metody execute() na skutek obsługi zlecenia. W metodzie init () - która powinna być z serwletu-kontrolera wołana jeden tylko raz - uzyskamy źródło danych (obiekt DataSource) na podstawie parametru, spect\yfikującego nazwę bazy.

Klasę działań na bazie danych przedstawia poniższy wydruk (kod klasy CommandImpl oraz cały schemat separacji przedstawiono w poprzednim podrozdziale).
import java.io.*;
import java.util.*;
import javax.naming.*;
import java.sql.*;
import javax.sql.*;

public class DbAccess extends CommandImpl {

  private DataSource dataSource;

  public void init() {
    try {
      Context init = new InitialContext();
      Context jndiCtx = (Context) init.lookup("java:comp/env");
      String dbName = (String) getParameter("dbName");
      dataSource = (DataSource) jndiCtx.lookup(dbName);
     } catch (NamingException exc) {
         setStatusCode(1);
     }
  }

  public void execute() {
    clearResult();
    setStatusCode(0);
    Connection con = null;
    try {
      synchronized(this) {
        con = dataSource.getConnection();
      }

      Statement stmt = con.createStatement();

      String cmd =  (String) getParameter("command");

      if (cmd.startsWith("select")) {
        ResultSet rs = stmt.executeQuery(cmd);

        // Będziemy zapisywać wynik jako skonkatenowane
        // wartości z kolumn ResultSetu
        // Oczywiście, w różnych kwerendach będą różne kolumny
        // zatem korzystamy z ResultSetMetaData, by do nich dotrzeć
 
        ResultSetMetaData rsmd = rs.getMetaData();
        int cols = rsmd.getColumnCount();
        while (rs.next()) {
          String wynik = "";
          for (int i=1; i<=cols; i++)
             wynik += rs.getObject(i) + " ";
          addResult(wynik);
        }
        rs.close();
      }
      else if (cmd.startsWith("insert")) {
        int upd = stmt.executeUpdate(cmd);
        addResult("Dopisano " + upd + " rekordów");
        }
      else setStatusCode(3);
    } catch (SQLException exc) {
          setStatusCode(2);
          throw new DbAccessException("Błąd w dostępie do bazy lub w SQL", exc);
    } finally {
        try {
          con.close();
        } catch (Exception exc) {}
    }
  }

}
Serwlet-kontroler w znany nam już sposób odczytuje wyniki i przekazuje do prezentacji. Ze względu na naturę dostępu przez DataSource musieliśmy jednak poczynić drobną zmianę w metodzie inicjacji serwletu-kontrolera, a mianowicie - ustalić nazwę bazy danych (pobraną z parametrów kontekstu) jako parametr dla klasy działania i zainicjować jej obiekt. Te dodatki są na poniższym wydruku zaznaczone pogrubionym tekstem..
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.text.*;

public class ControllerServ extends HttpServlet {

  private ServletContext context;
  private Command command;            // obiekt klasy dzialania (wykonawczej)
  private String presentationServ;    // nazwa serwlet prezentacji
  private String getParamsServ;       // mazwa serwletu pobierania parametrów

  public void init() {

    context = getServletContext();

    presentationServ = context.getInitParameter("presentationServ");
    getParamsServ = context.getInitParameter("getParamsServ");
    String commandClassName = context.getInitParameter("commandClassName");
    String dbName = context.getInitParameter("dbName");

    // Załadowanie klasy Command i utworzenie jej egzemplarza
    // który będzie wykonywał pracę
    try {
      Class commandClass = Class.forName(commandClassName);
      command = (Command) commandClass.newInstance();
      // ustalamy, na jakiej bazie ma działać Command i inicjujemy obiekt
      command.setParameter("dbName", dbName);
      command.init();
    } catch (Exception exc) {
        throw new NoCommandException("Couldn't find or instantiate " +
                                      commandClassName);
    }
  }

  // ... dalej znany już kod obsługi zleceń

Przygotujemy oczywiście zasoby definicyjne:
import java.util.*;

public class DbAccessDef_pl extends ListResourceBundle {
     public Object[][] getContents() {
         return contents;
     }

    static final Object[][] contents = {
       { "charset", "ISO-8859-2" },
       { "header", new String[] { "Baza danych książek" } },
       { "param_command", "Polecenie (select lub insert):" },
       { "submit", "Wykonaj" },
       { "footer", new String[] { } },
       { "resCode", new String[]
                    { "Wynik:", "Brak bazy", "Błąd SQL",
                      "Wadliwe polecenie; musi zaczynać się od select lub insert" }
                    },
       { "resDescr",
            new String[] { "" } },
    };
}
a w serwletach pobierania parametrów i prezentacji zmienimy szerokość pola tekstowego oraz rodzaj listy (niech teraz będzie numerowana). W sumie 10 minut pracy i mamy dość uniwersalną i elastyczną aplikaację bazodanową (zob. rys).

r


Przy okazji tej aplikacji przyjrzymy sie kwestiom związanym z obsługą błędów. Zauważmy, że wprowadzone kody i opisy błędów są mało precyzyjne. Gdy otrzymamy "Błąd SQL" - chcielibyśmy wiedzieć jaki to błąd.

W tym celu wprowadzamy własną klasę wyjątków DBAccessException,
public class DbAccessException extends RuntimeException {
  public DbAccessException(String msg, Throwable cause) {
     super(msg, cause);
   }
} 
Wyjątki tej klasy będziemy zgłaszać w reakcji na wystąpienie wyjątku SQLException, przy czym zastosujemy łańcuchowanie wyjątków: przyczyna powstania naszego wyjątku - czyli wyjątek SQLExecption zostanie dowiązany do naszego wyjątku:

 public void execute() {
   //....
   try {

     // łączenie z bazą i operacje na niej
     // ...
   } catch (SQLException exc) {
          setStatusCode(2);
          throw new DbAccessException("Błąd w dostępie do bazy lub w SQL", exc);
    } finally {
        try {
          con.close();
        } catch (Exception exc) {}
    }
  }
Zatem oprócz enigmatycznego "statusCode" 2 ( opisanego jako "Błąd w SQL") mamy zgłoszony wyjątek, precyzyjnie opisujący przyczyny błędu. Ale jak go obsłużyć? Przecież serwlet-kontroler, który wywołuje execute(...) nie będzie zajmował się prezentacją opisów błędów.
Oczywiście, mógłby on obsługiwać ten wyjątek i zapisać informacje o błędach do wykorzystania w serwlecie prezentacji. Byłoby to nawet dość eleganckie.

Istnieje jednak i inne rozwiązanie - strony opisów błędów deklarowane w pliku web.xml.
Element opisujący stronę błędu (<error-page>) definiuje typ błędu (klasę wyjątku) oraz lokalizację strony, która na skutek błędu ma być "załadowana".

Mogą to być strony statyczne, ale również odniesienia do serwletów (czy JSP), które dynamicznie wygenerują treść. Takim serwletom przekazywane jest zlecenie, dla którego ustalono atrybuty, opisujące sytuację. W szczegóności dostępne są następujące atrybuty:

javax.servlet.error.messageKomunikat o błędzie (String)
javax.servlet.error.servlet_nameNazwa serwletu, w którym powstał bład
javax.servlet.error.exception_typeKlasa wyjątku (Class)
javax.servlet.error.exceptionObiekt wyjątku (ogólnie klasy Throwable)

Uwaga: strony opisu błędów mogą być stosowane również do obsługi błędów HTTP oraz powstających po stronie samego serwera (a nie tylko naszych klas, generujących jakieś wyjątki).

Utwórzmy więc serwlet generacji stron opisu błędów SQL:
public class ErrorHandler extends HttpServlet {

  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
    String charset = BundleInfo.getCharset();
    resp.setContentType("text/html; charset=" + charset);
    Throwable exc = (Throwable)
                     req.getAttribute("javax.servlet.error.exception");

    if (exc != null) {
      PrintWriter out = resp.getWriter();
      out.println("<h2>" + exc.getMessage() + "</h2><hr>");
      Throwable cause = exc.getCause();
      if (cause instanceof SQLException) {
        SQLException sqlexc = (SQLException) cause;
        out.println(sqlexc.getMessage() + "<br><br>");
        out.println("Error code: " + sqlexc.getErrorCode() + "<br>");
        out.println("SQL state : " + sqlexc.getSQLState() + "<br>");
      }
      out.close();
    }

  }
//... metody doGet i doPost wołają serviceRequest
a w deskryptorze wdrożenia zaznaczmy, że to właśnie on powinien otrzymać sterowanie, gdy wystąpi nasz wyjątek DbAccessException:

   // ...
   <servlet>
       <servlet-name>ErrorHandler</servlet-name>
       <servlet-class>ErrorHandler</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ErrorHandler</servlet-name>
        <url-pattern>/errorhandler</url-pattern>
    </servlet-mapping>

   <error-page>
     <exception-type>DbAccessException</exception-type>
     <location>/errorhandler</location>
   </error-page>
   //..


Teraz po wprowadzeniu w przykładowej aplikacji polecenia: select * from poz możemy uzyskac następujący wynik (numery błędów są zależne od bazy):
r
 

Pokazany sposób oprogramowania WEB-aplikacji bazodanowej z uwzględnieniem separacji działań na bazie, części interakcyjnej oraz prezentacyjnej jest elastyczny i uniwersalny. Choć daje łatwe możliwości modyfikacji treści i funkcjonalności, to jednak może nieco "straszyć"  dość rozbudowaną architekturą (mamy tu 5 serwletów: kontroler, pobieracz parametrów, prezenter, zbieracz informacji z klas zasobowych oraz serwlet obsługi błędów SQL, a do tego klasy interfejsu bazodanowego oraz własnych wyjątków).

Z tego też powodu pojawiły się technologie JSP i - szczególnie - Java Server Faces. Pozwalają one nieco uprościć budowanie aplikacji WEB zgodnie z regułami architektury MVC, ale z kolei same wprowadzają dużą liczbę nowych pojęć i elementów, które trzeba opanować.

8. Cookies

Serwer HTTP może posyłać klientowi (przeglądarce) tzw. cookies - dowolne fragmenty informacji, zazwyczaj identyfikujące stany transakcji z klientem (np. autoryzację użytkownika, jego preferencje co do treści i wyglądu prezentowanego serwisu WWW). Te kawałki informacji są przechowywane przez przeglądarke i zwracane serwerowi HTTP przy kolejnych połączeniach. W ten sposób informacja jest odtwarzana (i można np. dokonywać odpowiedniej personalizacji stron, czy też kontynuować zakupy w sklepie internetowym).

Mechanizm cookies jest dostępny z  poziomu serwletów za pomocą klasy  Cookie.
Aby stworzyć "cookie"  wywołujemy konstruktor tej klasy:

    Cookie(String name, String value)

Stworzone "cookie" przesyłamy klientowi za pomocą metody addCookie(Cookie c) z klasy  HTTPServletResponse.

"Cookies" są jednym ze sposobów rejestrowania i prowadzenia sesji. Czyni to niewidocznie dla nas tzw. "session tracking API", na które składa sie klasa HttpSession oraz metody getSession() z klasy HttpRequest. Przy wyłączonych w przeglądarce cookies stosowane są inne sposoby rejestracji i prowadzenia sesji, np. tzw. url-rewriting.
Odczytujemy przeslane uprzednio cookies (przy nowej transakcji, zleceniu) za pomocą metody getCookies() z klasy HttpServletRequest. Metoda zwraca tablicę Cookie[]. Każde "cookie" z tej tablicy możemy odpytać o jego nazwę (getName()), wartość (getValue()) oraz "maksymlany wiek" (getMaxAge()). Maksymalny wiek "cookie" możemy ustalić wczśsniej za pomocą metody setMaxAge(...) - po wygaśnięciu podanego jako argument czasu serwer przestaje posyłać dane cookie do klienta.

Poniższy przykładowy program posluguje sie cookie o nazwie "count", zliczając w ten sposób kolejne połączenia klienta.
public class CookieAndSess extends HttpServlet {

  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {

      resp.setContentType("text/html");
      PrintWriter out = resp.getWriter();

      Cookie[] cookies = req.getCookies();
      Cookie countCookie = null;
      HttpSession session = null;

      if (cookies != null) {
        for (int i=0; i<cookies.length; i++) {
          String name = cookies[i].getName();
          String value = cookies[i].getValue();
          out.println("<br>" + name + " " + value);
          if (name.equals("count")) {
            countCookie = cookies[i];
            int count = Integer.parseInt(countCookie.getValue()) + 1;
            countCookie.setValue(String.valueOf(count));
            if (count > 3) session = req.getSession();
            }
          }
        }
      if (session != null) {
        out.println("<hr>");
        out.println("Sesja: " + session.getId());
      }
      if (countCookie == null) countCookie = new Cookie("count", "1");
      resp.addCookie(countCookie);
      out.close();
  }

}
Przy trzecim polączeniu tworzona jest sesja (metoda getSession() tworzy sesje, jeśli jeszcze nie została utworzona)  i od tego momentu widzimy, że wraz z naszym cookie-licznikiem przychodzi też cookie (o nazwie JSESSIONID) z identyfikatorem sesji:

r

9. Słuchacze i filtry

Java Servlet API definiuje kilka rodzajów zdarzeń oraz interfejsów ich obsługi.
Można tu wyróżnić:

Uwagi:
Zdarzenie
Klasa zdarzenia
Interfejs obsługi
Metody obsługi
inicjacja i usuwanie kontekstu aplikacjiServletContextEventServletContextListenercontextInitialized
contextDestroyed
zapis, zmiana i usuwanie atrybutów kontekstuServletContextAttributeEventServletContextAtrributeListenerattributeAdded
attributeRemoved
attributeReplaced
inicjacja i zakończenie obsługi zlecenia ServletRequestEventServletRequestListenerrequestInitialized
requestDestroyed
zapis, zmiana i usuwanie atrybutów zleceniaServletRequestAttributeEventServletRequestAttributeListeneratrributeAdded
attributeRemoved
attributeReplaced
utworzenie
i zamknięcie
sesji

HttpSessionEvent

HttpSestionActivationListener
sessionCreated
sessionDestroyed
aktywacja i pasywacja sesjiHttpSessionEventHttpSestionActivationListenersessionDidActive
sessionWillPassivated
zapis, zmiana i usuwanie atrybutów sesjiHttpSessionBindingEventHttpSessionAttributeListenerattributeAdded
attributeRemoved
attributeReplaced
zmiana dowiązania obiektu do sesjiHttpSessionBindingEventHttpSessionBindingListenervalueBound
valueUnbound

  1. wszystkie metody reagujące na inicjację (kontekstu, zlecenia, sesji) otrzymują sterowanie po inicjacji; wszystkie metody reagujące na deaktywację (kontekstu, zlecenia, sesji) otrzymują sterowanie tuż przed deaktywacją,
  2. aktywacja/deaktywacja sesji oznacza przesunięcie jej do innej maszyny wirtualnej lub jej serializację/deserializację. HttpSessionActivationListener nie obsługuje zdarzeń tworzenia lub zamykania sesji.
  3. zdarzenie HttpSessionBindingEvent jest posyłane do obsługi słuchaczowi HttpSessionAttributeListener, gdy jakikolwiek atrybut sesji ulega zmianie, natomiast do obsługi przez obiekt implementujący HttpSessionBindingListener gdy ten właśnie obiekt jest dowiązywany do sesji (metodą setAttribute) lub odwiązywany -  metodą removeAttribute()

W przypadku serwletów programowanie obsługi zdarzeń wygląda zupelnie inaczej niż np.  w aplikacjach GUI. Mianowicie:


Zapewnienie obsługi w/w zdarzeń wymaga stworzenia klas implementujących odpowiednie interfejsy, skompilowania ich i umieszczenia w strukturze katalogowej apliakacji (tam gdzie klasy lub pakiety klas - czyli w WEB-INF/classes lub WEB-INF/classes/nazwa_pakietu.
Wymaga także dodania do deskryptora wdrożenia przed definicjami serwletów, a po definicjach inicjalnych parametrów konteksu (i  filtrów) elementu listener.
Element ten ma postać:
    <listener>
        <listener-class>[nazwa_pakietu.]nazwa_klasy_słuchacza</listener-class>
    </listener>

Można również definiować słuchaczy w deskryptorze kontekstu, dodając element Listener:

<Context path="/jakas" ...>
  ...
  <Listener className="[nazwa_pakietu.]nazwa_klasy_słuchacza" ... >
  ...
</Context>
W tym elemencie można umieścić dodatkowe definicje właściwości JavaBeans (oczywiście klasa słuchacza musi spełniać protokół JavaBeans), podając nazwy właściwości i ich wartości.

Przetestujmy dzialanie słuchaczy. Zbudujemy trzy klasy słuchaczy: kontekstu, strybutów kontekstu i atrybutów sesji. Przy obsłudze poszczególnych zdarzeń będziemy dodawać komunikaty do listy, prowadzonej w klasie Report. Wszystkie te klasy umieścimy w pakiecie listeners.
package listeners;
import java.util.*;

public class Report {
   private static List rep = new ArrayList();

   public static void add(String s) {
     rep.add(s);
   }

   public static List get() { return rep; }
}
package listeners;
import javax.servlet.*;

public class TestContextListener implements ServletContextListener{


  public void contextInitialized(ServletContextEvent p0) {
     Report.add("Kontekst utworzony");
     ServletContext context = p0.getServletContext();
     context.setAttribute("Liczba", new Integer(1));
  }

  public void contextDestroyed(ServletContextEvent p0) {
  }
}
import javax.servlet.*;

public class TestContextAttributeListener implements ServletContextAttributeListener{

  public void attributeAdded(ServletContextAttributeEvent p0) {
    Report.add("Do kontekstu dodano atrybut " + p0.getName() +
    " = " + p0.getValue());
  }

  public void attributeRemoved(ServletContextAttributeEvent p0) {
    Report.add("Z kontekstu usunięto atrybut " + p0.getName() +
    " = " + p0.getValue());
  }

  public void attributeReplaced(ServletContextAttributeEvent p0) {
    Report.add("W kontekście zmieniono atrybut " + p0.getName() +
    " = " + p0.getValue());
  }

}
package listeners;
import javax.servlet.http.*;

public class TestSesAttrListener implements HttpSessionAttributeListener{



  public void attributeAdded(HttpSessionBindingEvent p0) {
    Report.add("Do sesji dodany atrybut " + p0.getName() + " = " + p0.getValue());
  }

  public void attributeRemoved(HttpSessionBindingEvent p0) {
    Report.add("Z sesji usunięty atrybut " + p0.getName() + " = " + p0.getValue());
  }

  public void attributeReplaced(HttpSessionBindingEvent p0) {
    Report.add("Zmieniony sesji atrybut " + p0.getName() + " = " + p0.getValue());
  }
}
Zauważmy, że od przekazanych zdarzeń, za pomoca metod ich klas uzyskujemy niezbędne informacje:
Aby klasy słuchaczy zostały załadowane, ich obiekty utworzone i powiązane z włąściwymi źródłami w pliku web.xml umieszczamy odpowiednie  elementy listener (podelementy elementuw web-app)

<?xml version="1.0" encoding="UTF-8"?>
    <web-app version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-app_2_4.xsd">

    <display-name>Adds</display-name>
    <description>Dodatkowi słuchacze</description>

    <listener>
        <listener-class>listeners.TestContextListener</listener-class>
    </listener>
    <listener>
        <listener-class>listeners.TestContextAttributeListener</listener-class>
    </listener>
    <listener>
        <listener-class>listeners.TestSesAttrListener</listener-class>
    </listener>


    <servlet>
       <servlet-name>ListenersTest</servlet-name>
       <description></description>
       <servlet-class>ListenersTest</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ListenersTest</servlet-name>
        <url-pattern>/listen</url-pattern>
    </servlet-mapping>

</web-app>
Zdefiniowany w deskryptorze serwlet ListenersTest służy do testowania w/w słuchaczy (jego kod zobaczymy za chwilę). Oprócz testowania działania wyżej wymienionych słuchaczy zilustrujemy też ciekawą właściwość: obiekty klas implementujących interfejs HttpSessionBindingListener uzyskują swoistą samowiedzę o tym kiedy są  ustalane jako atrybuty sesji i kiedy są - jako atrybuty - z sesji usuwane. Takie "świadome" obiekty stają się słuchaczami własnego losu (jako atrybutów sesji).
Dla przykladu dostarczymy następującej klasy (również w pakiecie listeners).
package listeners;
import javax.servlet.http.*;

public class SwiadomyAtrybut  implements HttpSessionBindingListener{

  private String val;

  public SwiadomyAtrybut(String val) {
    this.val = val;
  }


  public void valueBound(HttpSessionBindingEvent p0) {
    Report.add("Jestem \"świadomym\" atrybutem " +
         p0.getName() + " i wiem, że własnie zostałem dodany do sesji");
  }

  public void valueUnbound(HttpSessionBindingEvent p0) {
    Report.add("Jestem \"świadomym\" atrybutem " +
         p0.getName() + " i wiem, że własnie zostałem usunięty z sesji");

  }

  public String toString() { return val; }
}
Obiekt klasy SwiadomyAtrybut będzie dopisywał do listy raportowej  (klasa Report) komunikaty o tym, że został ustalony jako atrybut sesji (gdy wystąpi takie zdarzenie), jak również informację o swoim usunięciu z atrybutów sesji.

Serwlet testujący włącza formularz (zapisany w pliku html) za pomocą ktorego, będziemy mogli wykonywać następujące działania:
Na szybko sporządzony formularz wygląda następująco:
<form>
<p><input type="submit" name="setsesatt1" value="Normalny atrybut sesji (set)" style="background: white">
<p><input type="submit" name="remsesatt1" value="Usu˝ normalny atrybut sesji " style="background: white">
<p><input type="submit" name="setsesatt2" value="îwiadomy atrybut sesji (set)" style="background: white">
<p><input type="submit" name="remsesatt2" value="Usu˝ ťwiadomy atrybut sesji" style="background: white">
<p><input type="submit" name="chgatt" value="Zmien atrybut kontekstu" style="background: white">
</form>
 a jego włączenie (za pomocą include z klasy RequestDispatcher) wyświetli:

r


Kod sewletu testującego wygląda następująco:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import listeners.Report;
import listeners.SwiadomyAtrybut;

public class ListenersTest extends HttpServlet {

  static int count = 0;  // dla zmian "Liczby" - atrybutu kontekstu


  SwiadomyAtrybut sa = new SwiadomyAtrybut(
                       "jestem atrybutem, co wie kiedy go dodają lub usuwają"
                       );


  public void init() {
    Report.add("Inicjacja serwletu");
  }

  public void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
      resp.setContentType("text/html; charset=windows-1250");
      PrintWriter out = resp.getWriter();
      out.println("<center><h2>Testowanie słuchaczy</h2></center>");
      req.getRequestDispatcher("form.html").include(req,resp);

      HttpSession ses = req.getSession();
      if (req.getParameter("setsesatt1") != null) {
          Report.add("<b><i>--- wybrano 'Normalny atrybut sesji (set)'</b></i>");
          ses.setAttribute("atr_ses1", "jestem zwykłym atrybutem sesji");
      }
      else if (req.getParameter("remsesatt1") != null) {
        Report.add("<b><i>--- wybrano 'Usuń normalny atrybut sesji'</b></i>");
          ses.removeAttribute("atr_ses1");
      }
      else if (req.getParameter("setsesatt2") != null) {
        Report.add("<b><i>--- wybrano 'Świadomy atrybut sesji (set)'</b></i>");
        ses.setAttribute("atr_ses2", sa);
      }
      else if (req.getParameter("remsesatt2") != null) {
        Report.add("<b><i>--- wybrano 'Usuń świadomy atrybut sesji'</b></i>");
        ses.removeAttribute("atr_ses2");
      }
      else if (req.getParameter("chgatt") != null) {
        count++;
        if (count > 1) {
           Report.add("<b><i>--- wybrano 'Zmień atrybut kontekstu'</b></i>");
           getServletContext().setAttribute("Liczba", new Integer(count));
        }
      }

      out.println("<hr>");
      out.println("<u>Co się działo ?</u>");
      List list = Report.get();
      out.println("<ol>");
      for (Iterator it = list.iterator(); it.hasNext(); ) {
        out.println("<li>" + it.next() + "</li>");
      }
      out.println("</ol>");
      out.close();
  }
}
Gdy serwer tworzy obiekt-serwlet tworzony jest jednocześnie obiekt SwiadomyAtrybut (bo zapisaliśmy to w definicji pola sa klasy serwletu). Okazuje się, że to wystarcza (bez podawania definicji tego słuchacza w deskryptorze wdrożenia) do obsługi przez HttpSessionBindingListener (którym jest nasz SwiadomyAtrybut) przytrafiających mu się zdarzeń HttpBindingEvent.
Nasz serwlet testujący sprawdza który z przycisków w formularzu został wybrany i odpowiednio do tego wykonuje działania (dodanie atrybutu, usunięcie, zmiana). Działania zapisuje na liście klasy Report, ale rownież na te działania reagują odpowiedni słuchacze (dopisując swoje informacje do listy). Obsługa każdego zlecenia kończy się wyprowadzeniem  listy z klasy Report (zawierajacej kumulające sie informacje o tym co do tej pory sie działo). 

Oto przykładowy wynik - cześć wygenerowanej strony HTML, znajdująca siię po formularzu.


Co się działo ?
  1. Kontekst utworzony
  2. Do kontekstu dodano atrybut Liczba = 1
  3. Inicjacja serwletu
  4. --- wybrano 'Normalny atrybut sesji (set)'
  5. Do sesji dodany atrybut atr_ses1 = jestem zwykłym atrybutem sesji
  6. --- wybrano 'Świadomy atrybut sesji (set)'
  7. Jestem "świadomym" atrybutem atr_ses2 i wiem, że własnie zostałem dodany do sesji
  8. Do sesji do dany atrybut atr_ses2 = jestem atrybutem, co wie kiedy go dodają lub usuwają
  9. --- wybrano 'Usuń normalny atrybut sesji'
  10. Z sesji usunięty atrybut atr_ses1 = jestem zwykłym atrybutem sesji
  11. --- wybrano 'Usuń świadomy atrybut sesji'
  12. Jestem "świadomym" atrybutem atr_ses2 i wiem, że własnie zostałem usunięty z sesji
  13. Z sesji usunięty atrybut atr_ses2 = jestem atrybutem, co wie kiedy go dodają lub usuwają
  14. --- wybrano 'Zmień atrybut kon tekstu'
  15. W kontekście zmieniono atrybut Liczba = 1
  16. --- wybrano 'Zmień atrybut kontekstu'
  17. W kontekście zmieniono atrybut Liczba = 2

Komunikaty 1 i 2 pochodzą od słuchacza kontekstu i sluchacza atrybutów kontekstu (odpowiednio).
Komunikat 3 zapisała metoda init() serwletu.
Komunikat 5 i 10 wygenerowal słuchacz atrybutów sesji na skutek naszych manipulacji "normalnym atrybutem" sesji.
Komunikaty 7 i 12 pochodzą od naszego SwiadomegoAtrybutu, który tutaj zadziałał jako słuchacz HttpSessionBindingListener, obsługujący zdarzenie ustalenia/usunięcia samego siebie jako atrybutu sesji.
Komunikaty 8 i 13 powstały na skutek tych samych zdarzeń co 7 i 12 odpowiednio. W tym przypadku na ustalenie i usunięcie atrybutu SwiadomyAtrybut sa zareagowawł HttpSessionAttributeListener.
Wreszcie ostatnie komunikaty pokazują reakcje ServletContextAttributeListenera na zmiany wartości atrybutu "Liczba".

Zastosowania sewrletowych słuchaczy mogą być bardzo różnorodne. Zapewne do najczęstszych  należy wykonanie pewnych dzialań inicjacyjnych (np. uzyskania żrodła danych skojarzonego z bazą danych) na starcie całej aplikacji (czyli przy tworzeniu kontekstu) i uporządkowanie środowiska (zwolnienie jakichś zasobow) na zakończenie jej  pracyi. Zwrócmy uwagę, że w świecie serwletów bez słuchaczy i w środowiskach dostępnych poprzez wiele serwletów naraz takie dzialania inicjacyjne musiałyby być duplikowane w części inicajacyjnej  każdego z serwletów (z dodatkową logiką sprawdzającą czy nie zostały już wykonane przez inny serwlet), a dla działań porządkujących nie ma sensownej alternatywy (bo nie wiadomo który z równolegle działających serwletów ostatni skończy pracę).

Java Servlet API dostarcza jeszcze jednego mocnego narzędzia uelastyczniania i upraszczania WEB-aplikacji - filtrów.

Filtr jest obiektem, który może modyfikować zlecenia kierowane do komponentów WEB,  blokować je lub przekierowywać, modyfikować odpowiedzi posyłane prtzez kompoennty WEB do klientów. Stanowi dodatkową fiunkcjonalność, która może być przyłączona do dowolnego WEB-komponentu, ale jest od konkretnych komponentów niezależna i może być konfigurowana dla wybranych lub wszystkich komponentów aplikacji


Typowe zastosowania filtrów to: kompresje, szyfrowanie, kodowanie, konwersje obrazów, analiza składniowa.

Filtry programujemy jako klasy implementujące interfejs Filter, dostarczając definicji trzech metod:

Metoda doFilter ma trzy parametry:
Cóż to jest ten łańcuch filtrów? Otóż z komponentem-WEB może być skojarzone wiele filtrów. Zlecenie do takiego komponentu przechodzi wtedy przez kolejne filtry, stanowiące właśnie łańcuch filtrów. Na końcu tego łańcucha znajduje się komponent, do którego kierowano zlecenie.
Łańcuch filtrów jest obiektem klasy implementującej interfejs FilterChain. Jego metoda doFilter() służy do wywołania kolejnego elementu łańcucha.

Pokazuje to poniższy rysunek
r  





Typowa implementacja metody filtrowania w klasie filtra
(kolejne kroki)


    doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)

  1. Analiza zlecenie req.
  2. Opcjonalnie: opakowanie zlecenia we własną implementację interfejsu ServletRequest (zwykle klasę dziedziczącą HttpServletRequestWrapper) i modyfikacja jego nagłówków i/lub treści.
  3. Opcjonalnie: opakowanie odpowiedzi we własną implementację interfejsu ServletResponse (zwykle klasę dziedziczącą HttpServletResponseWrapper) i modyfikacja jej nagłówków i/lub treści.
  4. Albo wywołanie następnego elementu łańcucha filtrów  (chain.doFilter()),
  5. Albo zablokowanie/przekierowanie zlecenia (nie wywołujemy metody chain.doFilter()


Zbudujmy przykładowy filtr. Jego zadaniem będzie dodanie na początku każdej odpowiedzi "paska reklamowego" - ogłoszenia. Przy obsłudze każdego zlecenia ogłoszenia mają sie zmieniać. Zestaw ogłoszeń jest czytany z pliku. Również kodowania jest ustalane na podstawie "locale" zlecania, traktowanego jako klucz w tablicy kodowań, ładowanych z pliku do obiektu klasy Properties.

package filters;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class TestFilter implements Filter{

  private static int ind = 0;  // indeks ogłoszenia

  // szablon ogłoszenia
  private String szablon =
  "<table cellpadding=\"2\" cellspacing=\"2\" border=\"1\" width=\"100%\">"+
  "<tbody><tr><td valign=\"Top\" bgcolor=\"#000099\">" +
  "<div align=\"Center\"><font color=\"#ffffff\">@</font></div></td>"+
  "</tr></tbody></table>";

  // Lista ogłoszeń
  private List oglosz = new  ArrayList();

  // Tablica kodowań dla różnych "locale"
  private Properties encodings = new Properties();

  public void init(FilterConfig fc) throws ServletException {

    ServletContext sc = fc.getServletContext();

    // Strumień dla tablicy kodowań
    InputStream props = sc.getResourceAsStream("WEB-INF/encodings.properties");
    try {
      // załadowanie tablicy kodowań
      if (props != null) encodings.load(props);

      // Plik z ogłoszeniami
      InputStream is = sc.getResourceAsStream("WEB-INF/ogloszenia.txt");
      BufferedReader br = new BufferedReader(
                          new InputStreamReader(is)
                          );
      String line;
      while ((line = br.readLine()) != null) oglosz.add(line);
      br.close();
    } catch (Exception exc) { oglosz.add("Witamy!"); }
  }


  public void doFilter(ServletRequest req, ServletResponse resp,
                       FilterChain chain)
                       throws IOException,ServletException
  {
    // Ustalenie kolejnego ogłoszenia
    String msg;
    synchronized(this) {
      msg = szablon.replaceFirst("@", (String) oglosz.get(ind));
      if (ind < oglosz.size() - 1) ind++;
      else ind = 0;
    }

    // Ustalenie kodowania
    Locale locale =  req.getLocale();
    String charset = (String) encodings.get(locale);
    if (charset == null) charset = "windows-1250";
    resp.setContentType("text/html; charset=" + charset);

    // Generacja początku strony
    PrintWriter out = ((HttpServletResponse) resp).getWriter();
    out.println(msg);

    // Wywołanie następnego elementu FilterChain
    // zwykle już bezpośrednio serwletu

    chain.doFilter(req, resp);
  }

  public void destroy() {

  }
}
Uwagi:
Skompiliwaną klasę umieścimy w pakiecie filters (i w podkatalogu katalogu classes o tej samej nazwie).

Trzeba jeszcze zapewnić by klasa filtru była ładowana i związać filtr z kompenentem lub komponentami WEB. Do tego służą elementy filter i filter-mapping deskryptora wdrożenia aplikacji.
Element filter dostarcza definicji klasy filtra, kojarzy tę klasę z nazwą filtra. Ustalenie do jakich komponentów WEB odnosi się dany filtr uzyskujemy poprzez kojarzenie odwołań z nazwą filtra  (praktycznmie takie samo jak dla serwletów) w elemencie filter-mapping.

Naszą klasę TestFilter zdefiniujemy jako filtr w następujący sposób:

    <filter>
        <filter-name>HeaderFilter</filter-name>
        <filter-class>filters.TestFilter</filter-class>
    </filter>

Następnie powinniśmy pwoiedzieć jakich kompoentów dotyczy filtrowanie. Możemy specyfikować konkretne odwołania do konkretnego serwletu lub strony JSP, grup serwletów lub stron itp. Np. jeśli chcemy, by fiktowanie dotyczyło serwletu, który wywołujemy za pomocą odniesienia go, napiszemy:

    <filter-mapping>
        <filter-name>HeaderFilter</filter-name>
        <url-pattern>/go</url-pattern>
    </filter-mapping>
Możemy dodać dowolną liczbę mapowań, np. oprócz serwletu go, również interakcja z serwletem run ma podlegać filtrowaniu:
    <filter-mapping>
        <filter-name>HeaderFilter</filter-name>
        <url-pattern>/go</url-pattern>
    </filter-mapping>

    <filter-mapping>
        <filter-name>HeaderFilter</filter-name>
        <url-pattern>/run</url-pattern>
    </filter-mapping>

Oczywiście, możemy też stosować "widlcards" i np. w nazej testowej apliakcji powiemy, że nasz filtr ma być stosowany wobec wszystkicj jej komponentów (czyli dowolnego do niej odwołania:

    <filter>
        <filter-name>HeaderFilter</filter-name>
        <filter-class>filters.TestFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HeaderFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


Ogólnie, wiele filtrów może być stosowane wobec danego komponentu, i wiele komponentów może być skojarzone z tym samym filtrem. Pokazuje to rysunek, na którym literami F oznaczono filtry, a S - serwlety lub inne aktywne komponenty WEB.

r


Ważne jest też umiejscowienie elementów związanych z filtrami w deskryptorze wdrożenia.

Elementy filter, a po nich elementy filter-mapping muszą występować po elemantach context-param i przed elementami listener (które z kolei poprzedzają definicje serwletów)


Zatem, jeśli nasza aplikacja składa się ze znanychnam już serwletów Cookies oraz ListenersTest (oraz słuchaczy, o których mówiliśmy poprzednio), to dodając do niej testowy filtr deskryptor wdrożenia zapiszamy w następujący sposób.

<?xml version="1.0" encoding="UTF-8"?>
  <web-app version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-app_2_4.xsd">

    <display-name>Adds</display-name>
    <description>Dodatkowe</description>

    <filter>
        <filter-name>HeaderFilter</filter-name>
        <filter-class>filters.TestFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HeaderFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>listeners.TestContextListener</listener-class>
    </listener>
    <listener>
        <listener-class>listeners.TestContextAttributeListener</listener-class>
    </listener>
    <listener>
        <listener-class>listeners.TestSessionListener</listener-class>
    </listener>
    <listener>
        <listener-class>listeners.TestSesAttrListener</listener-class>
    </listener>



    <servlet>
       <servlet-name>CookieAndSess</servlet-name>
       <description></description>
       <servlet-class>CookieAndSess</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>CookieAndSess</servlet-name>
        <url-pattern>/cook</url-pattern>
    </servlet-mapping>

    <servlet>
       <servlet-name>ListenersTest</servlet-name>
       <description></description>
       <servlet-class>ListenersTest</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ListenersTest</servlet-name>
        <url-pattern>/listen</url-pattern>
    </servlet-mapping>

</web-app>

I w tym momencie uzyskujemy efekt, który inaczej wynagałby modyfikacji każdego z serwletów naszaj aplikacji. Dzięki założonemu filtrowi, odwołanie do każdego z serwletów (bez zmiany jego kodu) będzie powodowąło dodanie na początku strony-odpowiedzi "paska ogłoszenia",

Gdy zawołamy znany nam już, stary, niezmieniony test słuchaczy w przeglądarce pojawi się taki obrazek (kolejne odwołania będą zmieniać treść paska ogłoszeń).

r


A kiedy wywołamy testowanie cookies (kod taki sam jak był), to również pojawi się pasek "reklamowy" (z kolejnym wybranym z zestawu ogłoszeniem).

r


Zauważmy, że udało nam się zmodyfikować odpowiedź wysyłaną przez serwlety tylko dlatego, że nie została ona jeszcze zatwierdzona (committed). Innymi słowy mogliśmy coś dodać na początku odpowiedzi i nie zatwierdając jej (nie zamykając strumienia wyjściowego, nie wymiatając buforów) przekazać zlecenie do obsługi dalej umożliwiając serwletom dopisanie dalszego ciągu odpowiedzi.
Jeżeli jednak chcemy w filtrze zmodyfikowac odpowiedź otrzymaną od serwletu, to natrafimy na problem: odpowiedź (normalnie) jest już zatwierdzona, zatem nie możemy jej zmienić ani nic do niej dopisać. Co zrobić w takiej sytuyacji?

Rozwiązanie jest niezwykle prosta: należy przekazać dalej (po łańcuchu filtrów i w końcu
do serwletu) nie oryginalny obiekt typu ServletResponse (w szczególności, gdy posługujemy się protokołem HTTP - HttpServletResponse), ale obiekt własnej klasy implementującej ten interfejs. W naszym obiekcie za strumienie wyjściowe podstawimy własne strumienie, które później będziemy mogli odczytac , zmodyfikiwac ich zawartośc i zwrócić wołającemu nas elementowi (innemu filtrowi czy też kontenerowi serwletów) jako odpowiedź.

Implementacja interfejsu HttpServletResponse od podstaw byłaby uciążliwa. Dostarczona w pakiecie javax.servlet.http klasa HttpSevletResponseWrapper implmentuje ten interfejs i bardzo ułatwia nam zadanie.

Zatem klasa naszej odpowiedzi powinna:
Dla przykładu, stwórzmy filtr, który "założony" na wszystkie komponenty aplikacji dopisuje do każdej ich odpowiedzi stopkę z jakąś informacją (np. aktualną datą i czasem) 

Jako obiekt w którym umieszczona ma być odowiedź (HttpServletResponse) będziemy podsuwać serwletom obiekt naszej własnej klasy StringResponseWrapper.

Klasa ta wygląda w następujący sposób:
package filters;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;


public class StringResponseWrapper extends HttpServletResponseWrapper {

  // Strumien do którego będą pisane odpowiedzi
  private StringWriter stringWriter = null;

  public StringResponseWrapper(HttpServletResponse response) {
    super(response);
  }

  // Przedefiniowanie metody getWriter()
  // każdy kto ejj użyje - otrzyma nasz strumień StringWriter
  // i nic o tym nie wiedząc będzie pisał do niego
  // a nie do strumienia
  // związanego z oryginalnym obiektem HttpServletResponse


  public PrintWriter getWriter() throws IOException {
    if (stringWriter == null) stringWriter = new StringWriter();
    return new PrintWriter(stringWriter);
  }

  // Nie jesteśmy przygotowani na obsługę strumieni binarnych
  // - wykluczamy ich zastosowanie (chociaż nie musimy tego robić)

  public ServletOutputStream getOutputStream() throws IOException {
    throw new IllegalStateException(
      "getOutputStream() not allowed for StringResponseWrapper"
      );
  }

  // Nasza własna metoda, pozwlająca uzyskać dostęp do strumioenia
  // i do jego zawartości

  public StringWriter getStringWriter() {
    return stringWriter;
  }

}
Komentarze w kodzie szczehółowo wyjaśniają jego działanie. Zwróćmy jeszcze tylko uwagę, że tworzymy strumień wyjściowy "w sposób leniew" - czyli tylko wtedy, gdy naprawdę komuś będzie potrzebny (gdy użyje metody getWriter()).


Pokazaną klasę wykorzystamy w filtrze generującym "stopki". Przy przekazywaniu zlecenia po łańcuchu zleceń podstawimy jej obiekt jako obiekt-odpowiedź (ServletResponse w metodzie chain.doFilter()):

import javax.servlet.*;
import javax.servlet.http.*;

public class FooterFilter implements Filter{

  public void init(FilterConfig p0) throws ServletException {
  }

  public void doFilter(ServletRequest req, ServletResponse resp,
                       FilterChain chain) throws IOException,ServletException {

    Locale locale = req.getLocale();
    StringResponseWrapper newResp = new StringResponseWrapper(
                                        (HttpServletResponse) resp
                                     );
    chain.doFilter(req, newResp);
    StringWriter sw = newResp.getStringWriter();

    // Uzyskujemy treść wygenrowanej odpowiedzi
    String cont = sw.toString();

    // Teraz możemy zrobić cokolwiek z odpowiedzią
    // tu tylko dopiszemy do niej "stopkę"

    // Bierzemy strumień oryginalnej odpowiedzi
    PrintWriter out = resp.getWriter();

    // Przepisujemy otrzymaną odpowiedź
    out.println(cont);

    // Dopisujemy stopkę
    DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                   DateFormat.MEDIUM,
                                                   locale);

    out.println("<hr><i><b>" + df.format(new Date()) + "</i></b>");
    out.close();
  }

  public void destroy() {
  }
}
W tym fragmencie kodu (też objaśnionym przez komentarze) należy zwrócić uwage na konieczność wykonania konwersji: konstruktor klasy HttpResponseWrapper, a w konsekwencji i naszej klasy StringResponseWrapper, ma parametr typu HttpServletResponse. Tymczasem metoda doFilter otrzymuje argument ogólniejszego typu ServletResponse. Dlatego musieliśmy napisać:

    new StringResponseWrapper((HttpServletResponse) resp);


Dodamy definicję filtra do pliku deskryptora wdrożenia.

    <filter>
        <filter-name>HeaderFilter</filter-name>
        <filter-class>filters.TestFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HeaderFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>FooterFilter</filter-name>
        <filter-class>filters.FooterFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>FooterFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

Po reinstalacji aplikacji wszystkie zlecenia-odpowiedzi do wszystkich jej komponentów będą filtrowane przez oba filtry: generujący nagłówek i generujący stopkę.
Przykładowo nasz serwlet testujący słuchaczy pojawi się teraz w nastepującej postaci:

r


Te proste przykłady powinny wystarczyć do budowy własnych, różnorodnych filtrów.
Pomóc w tym mogą jeszcze następujące uwagi:

10. Rozszerzerzenia serwletów: Java Server Pages i Java Server Faces (syntetyczna informacja)

Na koniec rozdzialu w bardzo skrótowej formie przedstawione zostaną niektóre informacje o technologich Java Server Pages i Java Server Faces.

Jak widzieliśmy, serwlety pozwalają na  dynamiczne generowanie stron WWW poprzez
użycie metod "piszących" do strumieni odpowiedzi. Czyli tworzenie prezentacji zapisane jest jako fragmenty programu w języku Java. Stwarza to - znane nam już - problemy separacji logiki i prezentacji, a ponadto:
Technologia Java Server Pages jest - w pewnym stopniu - odpowiedzią na wymienione wyżej problemy.

Strona JSP jest plikiem tekstowym,  w którym można zapisać wraz ze zwykłą statyczną treścią HTML również dynamiczne fragmenty za pomocą znaczników JSP (które stanowią swoisty uproszczony sposób programowania logiki i zależnej od niej dynamicznej treści aplikacji).

Strona JSP jest przy jej ładowaniu, niejako w locie, tłumaczona przez serwer na kod odpowiedniego serwletu, po czym serwlet jest kompilowany i obsługuje odwołania do tej strony JSP. Z punktu widzenia autora strony sama strona JSP obsługuje zlecenia, a tekst zapisany na tej stronie (elementy JSP) opisuje sposób obsługi.

Podstawowe elementy "języka" JSP to: Bardzo mocnym narzędziem jest standardowa bibliteka znaczników (JSTL), która udostępnia standardowe znaczniki, umożliwiające wykonywanie wielu użytecznych zadań.
Składa sie ona z różnych funkcjonalnych obszarów, w każdym z których znajdziemy standardowe znaczniki wykonujące określone funkcje. Poniższa tabela przedstawia te obszary i funkcje oraz standardowe prefiksy jakimi należy poprzedzać znaczniki na stronie JSP.

 
Obszar
Funkcjonalność
Prefiks
Core
Operacje na zmiennych
c
Instrukcje sterujące
Zarządzanie URLami 
Różne
XML
Podstawowe przetwarzanie
x
Sterowanie
Transformacje
I18n
Locale
fmt
Formatowanie komunikatow
Formatowanie liczb i dat
Database
SQL
sql
Functions
Długość kolekcji
fn
Manipulacje na Stringach

Aby używać znaczników JSTL na stronach JSP trzeba podać odniesienie do odpowiedniej części tej biblioteki w znaczniku <%@ taglib  ...  %>
Te odneisienia są następujące:

  • Core: http://java.sun.com/jsp/jstl/core
  • XML: http://java.sun.com/jsp/jstl/xml
  • Internationalization: http://java.sun.com/jsp/jstl/fmt
  • SQL: http://java.sun.com/jsp/jstl/sql
  • Functions: http://java.sun.com/jsp/jstl/functions

  • Np. gdy chcemy użyć bazowych znaczników (Core) piszemy:

    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" 
        prefix="c" %>
    Rozważmy prosty przykład, które pokazują stosunkowo nowe elementy JSP czyli  zarówno język wyrażeń EL (wprowadzony w wersji JSP 2.0), jak i zastosowanie JSTL oraz wlasnej (niestandardowej) biblioteki znaczników.
     
    Zbudujemy stronę JSP, która pobierze (z formularza) wprowadzony parametr i wypisze go pod spodem. Do wypisywania informacji zastosujemy własny znacznik msg.
    Ma on dwie właściwosci: pre (napis przed właściwym komnikatem) oraz info (komunikat), a jego definicja wygląda tak:
    <%--
     Znacznik wpisujący podany jako atrybut msg tekst
     poprzedzony jakąś (nieobowiązkową) informacją
    --%>
    <%@ attribute name="pre" required="false"%>
    <%@ attribute name="info" required="true"%>
    <h2>${pre}<br>${info}</h2>
    
    
    Jak widać znacznik ten po prostu wpisuje "w strone" tekst pobrany z atrybutów.
    Definicję tego znacznika umieścimy w pliku msg.tag w katalogu WEB-INF/tags. 

    Strona JSP  wykorzysta standardowe znaczniki (ustalanie wartości zmiennych oraz instrukcję if, funkcję, zwracającą długość napisu),  a także i nasz własny znacznik. Dlatego najpierw musimy powiedzieć z jakich obszarów standardowych tagów chcemy skorzystać i gdzie szukać naszych własnych znaczników ( a także jakim prefiksem będą one poprzedzane).
    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
    <%@ taglib tagdir="/WEB-INF/tags" prefix="m" %>
    
    Dalsza część strony JSP tradycyjnie przeplata kod HTML z "dynamicznymi " znacznikami JSP (wszystko razem zapiszemy w pliku dialog.jsp).
    <html>
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=windows-1250">
      <title>Test</title>
    </head>
    <c:set var="fmsg" value="Wprowadź informację" />
    <c:set var="preinfo" value="Wprowadziłeś:" />
    <h2>Aplikacja testowa<br>${fmsg}</h2>
    
    <form>
    <input type="text" name="info" size="50">
    <p></p>
    <input type="submit" value="Submit">
    <input type="reset" value="Reset">
    </form>
    
    <c:if test="${fn:length(param.info) > 0}" >
      <m:msg pre="${preinfo}" info="${param.info}"/>
    </c:if>
    </body>
    </html>
    
    Komentarze:
    Warto zwrócić uwagę na to, że ten kod JSP jest zdecydowanie krótszy od kodu  odpowiadającego mu serwletu. I dodatkowo nie wymaga od nas żadnego programowania w Javie (trochę szkoda :-) - a w konsekwensji żadnych "ręcznie" przeprowadzanych rekompilacji (nb można łatwo się przekonać o tym, że serwer automatycznie tłumaczy kody JSP na kody serwletów - wystarczy zajrzeć do katalogo work).

    Ha, to już jest cała nasz aplikacja WEB. Pozostaje tylko ją wdrożyć,
    Oczywiście musimy przygotować deskryptor wdrożenia. Tak naprawdę strona JSP jest serwletem - zatem musimy zdefiniować serwlet. Tym razem jednak zamiast podelementu class-name podajemy podelement jsp-file.
      <servlet>
        <display-name>dialog</display-name>
        <servlet-name>dialog</servlet-name>
        <jsp-file>/dialog.jsp</jsp-file>
      </servlet>
    
    I dalej, możemy dokonać dowolnego mapowania tej strony JSP na odwołanie do niej.
    Na przykład:
      <servlet-mapping>
        <servlet-name>dialog</servlet-name>
        <url-pattern>/first</url-pattern>
      </servlet-mapping>
    
    Ale to nie wszystko.  Musimy jeszcze zapewnić odnalezienie bibliotek znaczników (jeśli ich używamy).
    Standardowe biblioteki znaczników (pliki jstl.jar i standard.jar) można w Tomcacie odnaleźć w katalogu webapps/jsp-examples/lib. Można je przekopiować do naszego lib pod WEB-INF (będą dostępne dla danej aplikacji), albo umieścić w katalogu lib Tomcata (będą dostępne dla wszystkich aplikacji).
    A co z naszymi znacznikami? Powiedzieliśmy - u początku strony JSP - że znajdą się w katalogu WEB-INF/tags. I tam powinniśmy umieścić plik z definicją znacznika msg.tag.

    Te wszystkie czynności wykona za nas Ant, jeśli tylko w zadaniu "build" dostarczymy odpowiednich instrukcji kopiowania. 

    Jak zwykle trzeba pokazać efekty.
    Po wywołaniu z przeglądarki  http://localhost:8080/jsp-test/first uzyskamy:

     r


    a po kliknięciu Submit (przy wprowadzonym napisie):

    r


    Na koniec tego (pobieżnego) wprowadzenia do JSP warto zwrócić uwagę na dwa tagi z JSTL:
    (powyżej zastosowano standardowe prefiksy, można je zmienić, ale to raczej nie wskazane).

    Jeśli dobrze się przyjrzeć, to JSP nie wnosi nic nadzwyczajnego w stosunku do zwykłych serwletów (może jakieś uproszczenia, ale kosztem opanowania zupełnie nowej, chciałoby się nawet powiedzieć - nieco dzikiej - składni).
    Szczerze mówiąc, alternatywne wobec JSP, opracowane w ramach projektu Jakarta Velocity (zob. http://jakarta.apache.org/velocity/) wydaje się  prostsze i bardziej naturalne w użyciu.

    A gdy chodzi o prostotę to na pewno możemy się z nią pożegnać w środowisku Java Server Faces. Ale tu nie chodzi o uproszczenia.
    Podstawową koncepcją jest dostarczenie uniwersalnych środków:
    JSF jest bardzo mocną technologią. Jej zalet nie dostrzeżemy na prostych przykładach,
    Wręcz przeciwnie - najprostsze zastosowania wykazują istotne nadmiary (w programowaniu, w definicjach deskryptorów wdrożenia itp.).
    Ale - bez wątpienia - większe, poważne i (powiedzmy - prawdziwe) aplikacje WEB - skorzystają na tej technologii,
    Java Server Faces nie sposób omówić na kilku stronach. Do tego są raczej całe książki,
    Zatem  temat ten tylko sygnalizujemy - odsylając do literatury, np. książki Caya Horstmanna "Core Java Faces".

    11. Zadania i ćwiczenia


    1. Zainstalować i przetestowac wszystkie apliakcje tego rozdziału.
    2. Napisać aplikację WEB, która po przedstawieniu tytułów książek z bazy książek pozwala na wybór książki i pokazanie szczegółowej informacji o niej (autor, isbn, cena). Przygotować dwie wersje tej aplikacji: z wykorzystaniem i bez wykorzystania JSP.
    3. Napisać apliakcję WEB, która służy do przeprowadzania testów z języka Java. Użyć narzędzi bazodanowych.