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.:
- dynamiczne generowanie treści stron WWW,
- proste, bezpośrednie przetwarzanie zleceń klienckich,
- kontrolowanie komunikacji z klientami - statystyki, zabezpieczenia, odtwarzanie informacji (sesje, cookies),
- dostarczanie informacji z baz danych,
- pośredniczenie pomiędzy programami klienckimi, a systemami informacyjnymi
firmy w wykonywaniu zadań z zakresu złożonej logiki biznesowej.
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:
- dostarcza wyspecjalizowanych technologii tworzenia tzw. web-komponentów,
czyli komponentów, które obsługują zlecenia, a w szczególności potrafią reagować
na nie dynamiczną generacją treści,
- implementuje koncepcje programowania kompnentowego (JavaBeans), co
sprzyja nie tylko prostocie tworzenia aplikacji (np. w środowiskach programistycznych),
ale również, czy może przede wszystkim, elastycznemu separowaniu treści i
prezentacji,
- zapewnia uniwersalny, spójny z innymi środkami języka, mechanizm dostępu do baz danych (JDBC),
- wzmacnia to wszystko bogactwem standardowych pakietów Javy, które
- oczywiście - możemy wykorzystywać na poziomie apliakacji WEB.
Przykładowy schemat działania aplikacji WEB pokazuje rysunek.
Rys. Działanie aplikacji WEB
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.
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:
-
stworzeniu pliku tzw. deskryptora wdrożenia (web deployment descriptor) o nazwie web.xml,
-
ew. stworzenie dodatkowych specjalnych plików opisowych (np. dla bibliotek znaczników JSP),
-
umiejscowienie wszystkich komponentów aplikacji (klas, stron HTML, stron
JSP, plików graficznych i dźwiękowych) oraz pliku deskryptora wdrożenia w
odpowiedniej, ściśle określonej, strukturze katalogowej,
-
dostarczenie serwerowi ścieżki kontekstu aplikacji (context-path).
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:
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:
- plik deskryptora wdrożenia (web.xml) - koniecznie!
- pliki deskryptorów znaczników bobliotek JSP (z rozszerzeniem *.tld) - o ile stosujemy biblioteki znaczników,
- plik konfiguracji Java Server Faces (faces-config.xml) - o ile stosujemy JSF,
- podkatalog lib (5), zawierający dzielone biblioteki (pakiety) Javy
w postaci plików JAR - jeśli tylko nasze klasy tego wymagają ,
- podkatalog classes (4), który zawiera skompilowane klasy Javy
(m.in. serwlety, ale również inne); jeżeli klasy należą do nazwanych pakietów,
to muszą być umieszczane w odpowiednich podkatalogach katalogu class.
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:
- skopiowanie tej struktury do katalogu okreslanego przez właściwość appbase konfiguracji serwera; standardowo- katalogu webapps, znajdującym
się w katalogu instalacyjnym Tomcata - ścieżką kontekstu
dla schematu z rysunku będzie wtedy /MojaAp (lub - jeśli nie kopiujemy całego katalogu
MojaAp, lecz tworzymy w webapps katalog o innej nazwie i kopiujemy do niego
całą zawartość katalogu MojaAp - nazwa nowoutworzonego katalogu poprzedzona
ukośnikiem).
- spakowanie tej struktury do archiwum o nazwie, mającej rozszerzenie WAR (od Web Application Archive) i skopiowaniu tego WAR-a do katalogu webapps - ścieżką kontekstu dla schematu z rysunku będzie /MojaAp,
- utworzenie deskryptora kontekstu aplikacji, w którym m.in podajemy
odwzorowanie pomiędzy ścieżką kontekstu a katalogiem aplikacji; deskryptor
ten można wpisac do pliku konfiguracyjnego serwera conf/server.xml, albo
stworzyć go w odrębnym pliku XML o dowolnej nazwie i umieścić ten plik w
katalogu webapps - wtedy ścieżka kontekstu będzie pobrana z deskryptora kontekstu.
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:
- definicje filtrów (dodatkowo przetwarzających, filtrującyhc, zlecenia dla serwletów i ich odpowiedzi)
- definicje słuchaczy (obsługujących zdarzenia związane z kontekstem i sesją),
- definicje serwletów (i ich parametrów inicjalnych) lub stron JSP składających się na aplikację oraz tzw. ich mapowanie,
- konfiguracja sesji (czyli połączenia lub takich sekwencji połączeń
użykownika z aplikacją, które zachowują tożsamość użytkownika)
- parametry kontekstu (dostępne dla wszystkich komponentów aplikacji),
- mapowanie typów MIME.
- strony powitalne,
- strony opisu błędów,
- lokalizacj deskryptorów bibliotek znaczników JSP (plików *.TLD)
- ograniczenia ze względu na bezpieczeństwo.
- definicje odniesień do zasobów (np. baz danych),
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.
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:
- kontekst - jest nazwą kontekstu aplikacji ( /kontekst jest ścieżką kontekstu)
- strona_jsp - plik tekstowy w formacie HTML z elementami JSP (nazwa.jsp)
- mapowanie_nazwy_serwletu - ustalona w pliku web.xml
nazwa lub szablon nazwy (stosujący tzw. "wildcards" i określający wiele nazw
pasujących do szablonu) za pomocą której (lub których) może być wywołany
serwlet
Jeżeli w odwołaniu podano tylko http://url_serwera/kontekst, to aplikacja zostanie uruchomiona w następujących przypadkach:
- gdy w głównym katalogu wdrożonej aplikacji znajduje się plik index.jsp,
to zostanie on wywołany (ale to zależy od serwera i jego ustawień),
- gdy w pliku web.xml zdefiniowano listę plików powitalnych w następującej postaci
<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),
- gdy mapowanie nazwy serwletu ma postać /*, oznaczającą dowolną nazwę,
to zostanie wywołany serwlet, którego nazwa została w pliku web.xml przyporządkowana
tej formie odwołania.
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:
- aby być serwletem HTTP, klasa musi odziedziczyć klasę HTTPServlet,
- przedefiniowana metoda doGet(HTTPServletRequest zlecenie, HTTPServletResponse odpowiedz)
obsługuje zlecania GET (czyli np. "idące" z paska adresów przeglądarki),
- odpowiedź generujemy w postaci strony HTML (lub zwyklego tekstu), pisząc
go do strumienia związanego z obiektem HTTPServletResponse; w szczególności możemy
wykorzystać strumień PrintWriter.
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:
- nazwa identyfikująca serwlet.
- nazwa klasy serwletu,
- mapowanie nazwy sewrletu na nazwę, której użyje klient w specyfikacji URL zlecenia GET.
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:
- dla celów identyfikacyjnych nazwaliśmy nasz serwlet HTMLMsg (inaczej niż nazwa klasy),
- możemy podać (choć niekoniecznie) krótki opis serwletu w znacznikach <description>
- nazwę klasy serwletu podajemy w znacznikach <servlet-class>;
nasza klasa Msg, zawarta w pliku Msg.java została skompilowana do pliku Msg.class
- podaliśmy zatem nazwę klasy Msg
- i - uwaga - gdy serwlet jest w nazwanym pakiecie (a zatem i w podkatalogu
katalogu classes) podajemy też nazwę pakietu np. controller.Msg
Mapowanie uzyskujemy dzięki elementowi <servlet_mapping>
<servlet-mapping>
<servlet-name>HTMLMsg</servlet-name>
<url-pattern>/msg</url-pattern>
</servlet-mapping>
Przy tym:
- element servlet musi poprzedzać element servlet-mapping,
- dla danego serwletu jego nazwy w tagu <servlet-name> w obu elmentach muszą byc takie same,
- element url-pattern określa sposób odwołania do tego serwletu
(być może jednego z wielu w ramach aplikacji); używając przy tym gwiazdek
możemy dostarczyć wielu sposobów wywołania serwletu (np. podanie /*.html
będzie powodować wywołanie serwletu w sytuacji gdy użytkownik wprowadzi dowolną
nazwę, zakończoną .html
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:
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ę:
Trzeci sposób wdrożenia - za pomocą deskryptora kontekstu - ma pewne zalety wobec omówionych:
- nie wymaga kopiowania katalogów lub WARów,
- można za jego pomocą ustalić dowolną (ale jednoznaczną) nazwę kontekstu
i związać ją z dowolnym fizycznym katalogiem, w którym rezyduje nasza apliakacja/
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:
- ścieżka_kontekstu - ściezka kontekstu, np. /gs
- ścieżka_do_katalogu_aplikacji - lokalizacja katalogu
aplikacji lub pliku WAR w systemie plikowym; ścieżkę możemy podac w postaci
absolutnej np. C:/przyklady/MojaAp, albo relatywnie w stosunku do tzw. "bazy aplikacji" hosta (standardowo katalogu <katalog_intalacyjny_Tomcata>/webapps).
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.
- jaki jest URL zlecenia (metoda getRequestURL()),
- jaka jest ścieżka kontekstu - metoda getContextPath(),
- jaka jest ścieżka uruchamiająca serwlet, który po raz pierwszy otrzymał to zlecenie - metoda getServletPath().
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.:
- informacji o serwerze (getServerInfo()),
- nazwy aplilkacji podanej w omawianiym wcześniej tagu <display-name> pliku web.xml - metoda getServletContextName(),
- jaka jest prawdziwa (np. po części fizyczna w systemie plikowym hosta)
ścieżka dla podanej ścieżki "wirtualnej" (takiej jak np. /serwlety1) - metoda
getRealPath(String),
- jakie komponenty (i zasoby) składają się na aplikacje (metoda getResourcePaths(String))
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.:
- tworzenie niezbędnych katalogów,
- kompilację kodów źródłowych,
- kopiowanie plików do odpowiedniej struktury katalogowej,
- instalację lub wdrożenie aplikacji za pomocą specyficznych zadań, kierowanyh do managera aplikacji Tomacta.
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:
- <katalog_aplikacji>
- src <--- podkatalog z kodami żródłowymi
- web <--- podkatalog z komponentami WEB (np. strony powitalne itp, opisy znaczników.)
-
WEB-INF <--- w tym podkatalogi
umieścimy deskryptor wdrożenia web.xml
- build.xml <--- to będzie zapis działań dla Anta
- build.properties <--- ew. definicje różnych własciwości danej aplikacji
- context.xml <--- opcjonalny plik deskryptora kontekstu aplikacji.
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:
- default - oznacza domyślne zadanie, gdy anta wywołamy bez argumentów (jako argument możemy podać zadanie do wykonania),
- basedir - oznacza katalog (tu bieżący) wobec którego "rozwiązywane" są relatywne odniesienia do katalogów i plików
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:
- za pomoca depends ustalamy zależność zadań. Zadanie prepare będzie
wykonane tylko wtedy gdy zadanie init zakończyło się poprawnie,
- katalog tags jest przeznaczony na własne biblioteki znaczników JSP,
nie zawsze musi być potrzebny, tak samo jak lib (ogolne biblioteki klas dla
całej apliakcji), ale tworzymy tu ogólną, przygotowaną na wszystkie przypadki
programowania serwletów i JSP strukturę katalogową.
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:
- wygodną intsrukcją Anta jest fileset
- definiowanie zbiorów plików.
Tutaj mówimy, że pod identyfikatorem classpath ma się znaleźć zbiór
wszystkich
plików z rozszerzeniem jar z katalogu /lib katalogu instalacyjnego
Tomcata (podanego w pliku properties pod nazwą catalina.home); użycie
include **/*.ext włączyłoby do zbioru wszystkie pliki z
rozszerzeniem
ext z katalogu dir i całego drzewa podkatalogów (rekursywnie)
zaczynających
się w katalogu dir.
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:
- install - instalacja aplikacji - bardzo wygodne przy rozwijaniu (w obecnych wersjach Tomcata install działa jak deploy),
- reload - przeładowanie aplikacji (np, po zmianie kodu klas Javy; zmiana web.xml wymaga jednak ponownej instalacji),
- remove - usunięcie zainstalowanej aplikacji,
- deploy - wdrożenie aplikacji.
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:
- zmienna url zawiera lokator menedżera aplikacji, gdzieś wcześniej
mogliśmy zapisać <property name="url" value="http://localhost:8080/manager"/>
lub pobrać wartość tej właściowości z pliku .properties,
- zmienne username i password to nazwa użytkownikia i hasło potrzebne
do dostępu do menedżera aplikacji (w Tomcacie specyfikujemy te
wartości w pliku conf/tomcat-users.xml), zapewne wpiszemy je do pliku build.properties,
- context.path - to określona wcześniej ścieżka kontekstu,
- war - określa umiejscowienie aplikacji (w naszym przypadku jest
to niespakowana struktura katalogowa, znajdująca
się pod katalogiem ${build}
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:
- najpierw ją usunąć - ant remove
- a poźniej zainstalować ponownie - ant install
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.
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:
- załadowanie klasy serwletu,
- utworzenie obiektu,
- inicjację.
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:
- nazwy metody zlecenia,
- URL-a zlecenia,
- nagłówków HTTP zlecenia,
- treści zlecenia (inaczej: ciała)
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
|
TRACE | Zwraca 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:
Obiekt-zlecenie możemy wykorzystać m.in. do:
- pobrania śeieżek i dodatkowych informacji zawartych w URLu zlecenia (widzieliśmy już to na przykładach poprzednio),
- pobrania nagłówków HTTP zlecenia (ogólne metody: getHeaders(), getHeaderNames()
getHeader(String) oraz przygotowane na często używane nagłówki np: getLocale(),
getContentType(), getCharacterEncoding(), getContentLength()),
- pobrania parametrów zlecenia (metody getParameterMap(), getParameterNames(),
getParameterValues(), getParameter(String) - o parametrach będziemy mówić
za chwilę
- pobrania lub ustalenia atrybutów zlecenia - czyli związancyh ze zleceniem
obiektów, które mogą być tworzone przez serwer lub ustalane programistycznie
przy przekazywaniu zlecenia do obsługi dalej, np. do innego serwletu (metody
getAttributeNames(), getAttribute(), setAttribute(), removeAttribute()),
- pobrania strumiena wejściowego (bajtowego - getInputStream() lub
znakowego - getReader()) poprzez który można przeczytać treść (ciało) zlecenia.
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.
Odpowiedzi składają się m.in. z :
- kodu wyniku (np. 200 - wszystko OK, 404 - zasób nie znaleziony),
- nagłówków HTTP,
- treść (ciało) odpowiedzi.
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:
- zwrócić tzw. przekierowanie adresu (redirected URL) za pomocą metody
sendRedirect(...) - serwer HTTP przekieruje klienta na podany jako argument
metody adres; użyć metody można tylko wtedy, gdy odpowiedź nie została zatwierdzona;
po przekierowaniu nie wolno już nic pisać do strumienia wyjściwego,
- przekazać obsługę zlecenia do innego komponentu WEB (serwletu, strony JSP) używając obiektu RequestDispatcher.
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:
- przygotowania zlokalizowanych tekstów w ResourceBundle,
- odczytania z nagłówków zlecenia preferowanego języka użytkownika
(nagłówek accept-language, dostarczany też przez metodę getLocale()),
- wybrania tekstu odpowiedzi z odpowiedniego ResourceBundle,
- ustalenie typu treści i kodowanie odpowiedzi,
- (opcjonalnie, w zalezności od śrdowiska kodowania tej odpowiedzi w odpowiedniej stronie kodowej.
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:
- 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.
- 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).
- 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:
a w ustawionej na język angielski:
Z tym przykładowym serwletem mamy jednak przynajmniej dwa kłopoty.
- po pierwsze, przedefiniowaliśmy tylko metodę doGet(...) i powstaje
pytanie czy to wystarczy aby serwlet działal w każdych okolicznościach?
Musimy chyba bliżej przyjrzeć się metodom zleceń HTTP i zaraza to zrobimy.
- po drugie, pobieranie zasobów lokalizacyjnych umieszczone jest w
metodzie doGet() i jest wykonywane za każdym połączeniem. Być może warto
zoptymalizowac nieco kod pod kątem wielokrotnego łączenia się tego samego
użytkownika (który normalnie nie zmienia przy każdym połączeniu języka przeglądarki).
Tu na pomoc przyjdzie nam pojęcie sesji i metody związane z zarządzaniem
sesjami. O tym będziemy mówić nieco poźniej.
Zacznijmy od zlecń. Dwa najbardziej popularne rodzaje zleceń HTTP to GET i POST.
Zlecenie GET jest generowane, gdy:
- użytkownik wypelni informacje na pasku adresu i naciśnie ENTER,
- kliknie w link z adresem,
- wyśle formularz (np. przez kliknięcie w przycisk "Wyślij"), który
albo nie specyfikuje metody (brak paarmetru METHOD, alb specyfikuje jako
metodę - GET).
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:
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  
<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:
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:
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
- po kliknięciu "Pokaż" (strona wygenerowana przez serwlet)
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:
- przygotować część pliku HTML z formularzem w postaci (swoisty szablon)
<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>
- w serwlecie odczytać ten plik i wpisać jego zawartość na generowaną stronę,
- jeżeli serwlet nie może uzyskać parametrów (pierwsze odwołanie do serwletu) poprzestajemy na wygenerowaniu strony z formularzem,
- jeżeli są parametry - serwlet przetwarza wyrażenie regularne i dopisuje do generowanej strony wyniki wyszukiwania.
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:
Po wysłaniu formularza uzyskamy nie tylko wyniki, ale
poprzedzający je formularz przygotowany do wpisywania nowych danych:
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:
- dla każdej aplikacji możemy ustalić inicjalne parametry kontekstu
(element context-param w pliku web.xml); parametry te będą dostępne dla
wszystkich serwletów w aplikacji poprzez metodę getInitParameter klasy ServletContext,
- dla całej aplikacji można dynamicznie ustalać atrybuty kontekstu
, zawierające referencje do dowolnych obiektów; ustalać, odczytywac i usuwać
atrybuty może każdy z serwletów aplikacji za pomocą metod setAttribute(Object),
Object getAttribute(), removeAttribute(Object) klasy ServletContext,
- serwlety mogą tworzyć sesje i poslugiwac się atrybutami sesji;
sesja identyfikuje połączenie z danym klientem (które może być przez jakiś,
ustalony czas nieaktywne); bieżącą sesję uzyskujemy jako obiekt klasy HttpSession
za pomocą metody getSession() z klasy HttpServletRequest()
- atrybuty sesji pozwalają zapisywac i wynieniać informacje
dotyczące danej sesji - tu również mamy metody setAttribute(..), getAttribute,
removeAttribute() tym razem z klasy HttpSession, które to metody może wywoływac
każdy z serwletów aplikacji.
- zlecenie może być przekazane przez serwlet do obsługi (całkowitej lub częściowej) innemu serwletowi; służy temu obiekt RequestDispatcher, uzyskiwany od aktualnego zlecenia (lub kontekstu).
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
|
void | setAttribute(String name, Object value) |
Object | getAttribute(String
name) |
Enumeration | getAttributeNames() |
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:
-
klasę odpowiedzialną za wykonanie pracy (nazwiemy ją klasą działania); zbudujemy ją przy tym w ogólny sposób,
tak że będzie mogła być wykorzystana i w innych sytuacjach, niekoniecznie
w środowisku aplikacji WEB,
-
klasę GetParamServ - serwlet pobierający wartości parametrów (np. z formularza HTML),
-
klasę ResultPresent - serwlet prezentacji wyników
-
klasę ControllerServ, która bedzie stanowić serwlet-kontroler, przyjmujący
zlecenie i uaktywniający inne komponenty (w tym pobieranie parametrów, wykonanie
pracy, prezentację).
Chcielibyśmy przy tym zapewnić, aby:
- 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,
- serwlet pobierania parametrów nie był zależny od nazw i opisu parametrów (w szczególności ich zlokalizowanych opisów),
- 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:
- strona kodowa właściwa dla danej lokalizacji (klucz "charset").
- napisy nagłówkowe, które mają sie pojiwić na generowanej przez serwlet pobierania parametrów stronie (klucz "headers"),
- parametry - ich nazwy i opisy (każdy parametr jest dany przez klucz
param_nazwaparametru, a wartość zapisana pod ty kluczen będzie stanowić opsi
parametru, pojawiający się np. w formulrazu),
- napis na przycisku wysyłania formularza (klucz "submit"),
- ew. napisy pojawiające się na końcu generowanej strony z formularzem (klucz "footers"),
- kounikaty związane z kodami wyniku (klucz "resCode"),
- dodatkowe lementy opisowe dla wyników (klucz "resDescr").
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).
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:
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ż:
- obiekt Connection może być współdzielony przez wiele wątków, a nasz
program nie jest na to przygotowany (dostęp do Connection nie jest wielowątkowo
bezpieczny); w środowisku WEB aplikacji jest to szczególnie ważne, bo aplikacja
powinna móc obsługiwać równocześnie dużą liczbę połączeń,
- połaczenie z bazą danych jest otwierane przy inicjacji serwletu i
utrzymywane przez cały czas życia serwletu (co oczywiście wcale nie jest
potrzebne i prowadzi do zmniejszenia efektywności w środowiskach, gdzie działa
wiele serwletów bazodanowych),
- połączenie może wygasnąć z jakichś powodów (np. serwer bazodanowy
zamknie je ze względu na długą nieaktywność), a nasz serwlet nie jest przygotowany
do odtworzenia połączenia przy ew. kolejnym zleceniu,
- wreszcie, w tym programiku występuje znany nam już problem zmieszania
kodu odpowiedzialnego za pracę i kodu prezentacji (na razie pominiemy to
zagadnienie, aby było widać wyraźniej całościowe kody programów; później
zajmiemy się przebudową właściwej aplikacji w konwencji MVC),
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ć:
- w ramach elementu Resource: nazwę zasobu (tu nasza baza identyfikowana
przez jdbc/ksidb), jego autoryzację - czyli kto będzie odpowiadał za rejestrację
zasobu u manadżera zasobów (podając "Container" powiemy, że kontener serwletów,
czyli część serwera, zajmującego się obsługą serwletów) , typ zasobu - czyli
klasę jego obiektów - tu będzie to DataSource),
- w ramach elementu ResourceParams - paranetry tworzenia zasobu (w
naszym przypadku - klasę sterownika JDBC, url zasobu, nazwę użytkownika,
hasło).
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:
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).
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.message | Komunikat o błędzie (String)
|
javax.servlet.error.servlet_name | Nazwa serwletu, w którym powstał bład
|
javax.servlet.error.exception_type | Klasa wyjątku (Class) |
javax.servlet.error.exception | Obiekt 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):
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:
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 aplikacji | ServletContextEvent | ServletContextListener | contextInitialized
contextDestroyed
|
zapis, zmiana i usuwanie atrybutów kontekstu | ServletContextAttributeEvent | ServletContextAtrributeListener | attributeAdded
attributeRemoved
attributeReplaced
|
inicjacja i zakończenie obsługi zlecenia | ServletRequestEvent | ServletRequestListener | requestInitialized
requestDestroyed
|
zapis, zmiana i usuwanie atrybutów zlecenia | ServletRequestAttributeEvent | ServletRequestAttributeListener | atrributeAdded
attributeRemoved
attributeReplaced
|
utworzenie
i zamknięcie
sesji
|
HttpSessionEvent
|
HttpSestionActivationListener
| sessionCreated
sessionDestroyed
|
aktywacja i pasywacja sesji | HttpSessionEvent | HttpSestionActivationListener | sessionDidActive
sessionWillPassivated
|
zapis, zmiana i usuwanie atrybutów sesji | HttpSessionBindingEvent | HttpSessionAttributeListener | attributeAdded
attributeRemoved
attributeReplaced
|
zmiana dowiązania obiektu do sesji | HttpSessionBindingEvent | HttpSessionBindingListener | valueBound
valueUnbound
|
- 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ą,
- aktywacja/deaktywacja sesji oznacza
przesunięcie jej do innej maszyny wirtualnej lub jej serializację/deserializację.
HttpSessionActivationListener nie obsługuje zdarzeń tworzenia lub zamykania
sesji.
- 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:
- dostarczamy definicji klas słuchaczy (a w nich metod obsługi zdarzeń), ale w naszych programach nie tworzymy obiektów tych klas; to zadanie pozostawiamy serwerowi podając w elemencie listeners pliku deskryptora wdrożenia (web.xml) jakie klasy ma załadować i jakie obiekty stworzyć,
- w naszych programach nie dodajemy słuchaczy do źródeł zdarzeń
metodami addXXXLIstener; to zadanie również spełina serwer, bowiem dostarczone
definicje klas - poprzez implementację odpowiedniego interfejsu nasłuchu
- jednoznacznie określają jakich żródeł dotyczy nasłuch zdarzeń, a przy tym
serwer dba o dynamiczne zmiany przyłączeń (np. przy zmianach sesji),
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:
- getServletContext() z klasy ServletContextEvent zwraca kontekst aplikacji,
- getName() i getValue() z klas ServletContextAttributeEvent i HttpSessionBindingEvent
zwracają nazwę i wartość atrybutu, którego dotyczy zdarzenie.
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:
- ustalenie atrybutu do sesji.
- usunięcie atrybutu z sesji,
- ustalenie obiektu klasy SwiadomyAtrybut jakio atrybutu sesji,
- usuniecie go z zestawu atrybutów sesji,
- zmianę atrybutu kontekstu (ma ona nazwę "Liczba", jest ustalony na
1 przy tworzeniu kontekstu przez słuchacza kontekstu, po czym każdy wybór
opcji "zmiana atrybutu" powoduje zwiększenie tej liczby o 1).
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:
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 ?
- Kontekst utworzony
- Do kontekstu dodano atrybut Liczba = 1
- Inicjacja serwletu
- --- wybrano 'Normalny atrybut sesji (set)'
- Do sesji dodany atrybut atr_ses1 = jestem zwykłym atrybutem sesji
- --- wybrano 'Świadomy atrybut sesji (set)'
- Jestem "świadomym" atrybutem atr_ses2 i wiem, że własnie zostałem dodany do sesji
- Do sesji do
dany atrybut atr_ses2 = jestem atrybutem, co wie kiedy go dodają lub usuwają
- --- wybrano 'Usuń normalny atrybut sesji'
- Z sesji usunięty atrybut atr_ses1 = jestem zwykłym atrybutem sesji
- --- wybrano 'Usuń świadomy atrybut sesji'
- Jestem "świadomym" atrybutem atr_ses2 i wiem, że własnie zostałem usunięty z sesji
- Z sesji usunięty atrybut atr_ses2 = jestem atrybutem, co wie kiedy go dodają lub usuwają
- --- wybrano 'Zmień atrybut kon
tekstu'
- W kontekście zmieniono atrybut Liczba = 1
- --- wybrano 'Zmień atrybut kontekstu'
- 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:
- doFilter - w której wykonujemy działania związane z analizą
zlecenia i ew. jego blokowaniem lub modyfikacją (w szcególności nagłówków),
a także ew. modyfikacji odpowiedzi generowanych przez WEB-komponenty, do
których zlecenie jest kierowane,
- init - która jest wolana zaraz po utworzeniu obiektu-filtra i m.in. pozwala pobrać inicjalne parametry dla filtra
- destroy - wołana przed usunięciem filtra, pozwla na wykonanie prac porządkowych (np. zwolnienie zasobów alokowanych przez filtr)
Metoda doFilter ma trzy parametry:
- obiekt-zlecenie (ServletRequest)
- obiekt-odpowiedż (ServletResponse)
- łańcuch filtrów (FilterChain)
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
Typowa implementacja metody filtrowania w klasie filtra
(kolejne kroki)
doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
- Analiza zlecenie req.
- Opcjonalnie: opakowanie zlecenia we własną implementację interfejsu
ServletRequest (zwykle klasę dziedziczącą HttpServletRequestWrapper) i modyfikacja
jego nagłówków i/lub treści.
- Opcjonalnie: opakowanie odpowiedzi we własną implementację interfejsu ServletResponse
(zwykle klasę dziedziczącą HttpServletResponseWrapper) i modyfikacja jej
nagłówków i/lub treści.
- Albo wywołanie następnego elementu łańcucha filtrów (chain.doFilter()),
- 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:
- od FilterConfig (parametr metody init()) można pobrać kontekst apliakacji
i za jego pomocą uzyskać dostęp do zasobow (jak to robimy w powyższym programie)
lub np. ystalić jakieś atrybuty kontekstu - do wykorzystania przez inne filtry
lub serwlety w łańcuchu,
- programując filtry musimy pamiętac o synchronizacji; tak samo jak
serwlety dostęp do obiektów-filtrów może mieć równocześnie wiele wątków (w
naszym przykłądzie synchronizujemy podstawienie ogłoszenia do szablonu oraz
zmianę indeksu ogłoszenia).
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.
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ń).
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).
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:
- odziedziczyć HttpServletResponseWrapper,
- w konstruktorze z argumentem HttpServletResponse wyowłać konstruktor nadklasy przekazują ten argument (oryginalną odpowiedź),
- przedefiniować te metody klasy, dla których chcemy dostarczyć własnej
funkcjonalności, w szczeglności np. podmiany strumienia do którego mają pisać
swoją odpowiedź serwlety na nasz własny strumień, który w filtrze będziemy
mogli odczytać i dzieki temu zmodyfikować treść odpowiedzi przekazywanej
dalej, z powrotem po łąńcuchu filtrów.
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:
Te proste przykłady powinny wystarczyć do budowy własnych, różnorodnych filtrów.
Pomóc w tym mogą jeszcze następujące uwagi:
- tak samo jak odpowiedzi, również zlecenia (HttpServletRequest) możemy
w filtrach modyfikować, opakowując je obiektami własnych klas dziedizczących
HttpServletRequestWrapper,
- zablokowanie dostepu przez filtr jest bardzo proste - wystarczy nie
wyowłać metody doFilter łańcucha filtrów, a zamiast tego dostarczyć jako
odpowiedzi jakiejś strony z wyjaśneiniem dlaczego dostep jest zabroniony;
zwykle będzie to zależeć od jakichś warunków, które możemy sprawdzić analizując
zlecenie (np. host skąd prztszło), od identyfikacji użytkownika, jakichś
parametrów czasowych,
- filtry mogą przekierowywac zlecenia (np. tymczasowo jakieś komponenty
są wyłączone z pracy, zlecenia do nich mogą przekierowane do stron informacyjnych
lub innych komponentów zastępczych),
- filtry i opakowania zleceń i odpowiedzi można budować w ogólniejszy
niż pokazano sposób, np. uzwględniając inne niż HTPP protokoły (dlatego parametry
metody doFilter mają ogólniejsze typy: ServletRequest i ServletResponse)
czy też - oprócz strumieni znakowych - również strumienie binarne (uzyskiwane
np. w serwletach przez getOutputStream()).
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:
- wymaga znajomości Javy do przygotowania prezentacyjnej czesci aplikacji
(a często taką prezentacją zajmują sie ludzie nie znający programowania),
- wymaga rekompilacji serwletu przy zmianie sposobu prezentacji treści.
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:
- dyrektywy - zapisywane w znacznikach < %@ .... % >
- dotyczą całej strony opisują sposób jej translacji i wykonania przez serwer
- elementy skryptowe:
- deklaracje
< %! .... % >
deklaracje zmiennych,obiektów i metod
- wyrażenia
<%= .... %>
wartości wyrażeń zwracane
przez serwer (zwykle dotyczą informacji uzyskiwanych od kontekstu
aplikacji,
zlecenia itp., niejako erplikują znane nam metody klas
ServletContext, HttpRequest,
HttpSession itp.)
- skryplety
<% ....
%> frgamenty kodu w Javie
- akcje - oznaczane tagami < jsp: ... / > m.in.
- <jsp:useBean... /> - tworzenie obiektu JavaBean
- <jsp:setProperty... /> - ustalenie właściwosci JavaBean
- <jsp:getProperty... /> - pobranie właściwości JavaBean
- <jsp:include|forward..> - użycie RequestDispatchera do włączenia
inengo kompoenetu WEB lub przekazania sterowania do innego kompoenntu WEB
- elementy języka wyrażeń (EL) - wyrażenia zapisane w znacznikach ${...}-
pozwalają m.in. wykonywać obliczenia i w latwy sposób odwoływać się do właściwości
JavaBeans
- definiowane znaczniki - własne znaczniki, niejako własne rozszerzenia
języka JSP, dostarczane w bibliotekach znaczników (taglibs), które wiążą
elementy-znaczniki z obiektami, które definiują i implementują ich funkcjonalność.
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:
- tagi są poprzedzane prefiksami dla właściwych bibliotek,
- set z Core JSTL ustala wartości zmiennych
- w języku wyrażeń EL możemy się do nich odwoływać przez ${nazwa_zmiennej}
- do parametrów zlecenia odwołujemy się przez param.nazwa_parametru,
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:
a po kliknięciu Submit (przy wprowadzonym napisie):
Na koniec tego (pobieżnego) wprowadzenia do JSP warto zwrócić uwagę na dwa tagi z JSTL:
- c:forEach - pozwala na przebieganie tablic i - uwaga - dowolnych kolekcji
- sql:query - pozwala na posylanie kwerend do baz danych,
(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:
- reprezentacji komponemtów UI w sposób niezależny od modeli danych
- obsługi zdarzeń,
- walidacji danych po stronie serwera,
- konwersji danych
- nawigacji pomiędzy stronami (html, jsp, serwletami),
- internacjoonalizacji treści,
- oraz dowolnych rozszerzeń tych funkcji,
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
- Zainstalować i przetestowac wszystkie apliakcje tego rozdziału.
- 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.
- Napisać apliakcję WEB, która służy do przeprowadzania testów z języka Java. Użyć narzędzi bazodanowych.