W niniejszym rozdziale:
Analiza parametrów
Wysyłanie poczty elektronicznej
Stosowanie wyrażeń regularnych
Uruchamianie programów
Stosowanie rdzennych metod
Występowanie jako klient RMI
Usuwanie błędów
Poprawa wydajności
Rozdział 19.
Informacje dodatkowe
W każdym domu znajduje się szuflada na szpargały — szuflada załadowana do pełna rzeczami, które nie do końca pasują do żadnej zorganizowanej szuflady, ale nie mogą też zostać wyrzucone, ponieważ kiedy są potrzebne, to są naprawdę potrzebne. Niniejszy rozdział pełni funkcję takiej szuflady. Zawiera spory zestaw przydatnych przykładów serwletów i porad, które nie pasują do żadnego innego miejsca. Zawarto tu serwlety, które analizują parametry, wysyłają pocztę elektroniczną, uruchamiają programy, wykorzystują mechanizmy wyrażeń regularnych i rdzenne metody oraz działają jako klienty RMI. Rozdział ten zawiera również demonstrację technik usuwania błędów, a także pewne sugestie dotyczące poprawy wydajności serwletów.
Analiza parametrów
Osoby, które próbowały tworzyć własne serwlety podczas lektury niniejszej książki przypuszczalnie zauważyły, że pobieranie i analiza parametrów żądania może być dość nieprzyjemnym zajęciem, zwłaszcza jeżeli parametry te muszą zostać przekonwertowane do formatu innego niż String. Na przykład, konieczne jest odczytanie parametru count i zmiany jego wartości na format int. Poza tym, obsługa warunków błędów powinna być wykonywana przez wywołanie obslugaBezCount(), jeżeli count nie jest podany, a obslugaZlyCount(), jeżeli count nie może zostać przekształcony na liczbę całkowitą. Wykonanie tego działania przy pomocy standardowego Servlet API wymaga następującego kodu:
int count;
String param = zad.getParameter("count");
if (param=null || param.length() == 0) {
obslugaBezCount();
}
else {
try {
count = Integer.parseInt(param);
}
catch (NumberFormatException w) {
obslugaZlyCount();
}
}
Czy to wygląda jak porządny kod? Nie jest zbyt piękne, prawda? Lepszym rozwiązaniem jest przekazanie odpowiedzialności za pobieranie i analizę parametrów klasie narzędziowej. Klasa com.oreilly.servlet.ParameterParser jest właśnie taką klasą. Przy pomocy ParameterParser możliwe jest przepisanie powyższego kodu w bardziej elegancki sposób:
int count;
ParameterParser analiza = new ParameterParser(zad);
try {
count = analiza.getIntParameter("count");
}
catch (NumberFormatException w) {
obslugaZlyCount();
}
catch (ParameterNotFoundException w) {
obslugaBezCount();
}
Analizująca parametry metoda getIntParameter() zwraca wartość określonego parametru jako int. Zgłasza wyjątek NumberFormatException, jeżeli parametr nie może zostać przekonwertowany na int oraz ParameterNotFoundException, jeżeli parametr nie jest częścią żądania. Zgłasza również ParameterNotFoundException, jeżeli wartość parametru to pusty łańcuch. Często dzieje się tak w przypadku wysyłanych formularzy, gdy nie wpisano żadnych wiadomości do pól tekstowych, co we wszystkich przypadkach powinno być traktowane w ten sam sposób, co brakujący parametr.
Jeżeli wystarczy, aby w przypadku problemów z parametrem serwlet wykorzystywał domyślną wartość, co jest częstą praktyką, kod może zostać uproszczony w jeszcze większym stopniu:
ParameterParser analiza = new ParameterParser(zad);
int count = analiza.getIntParameter("count", 0);
Powyższa druga wersja getIntParameter() pobiera domyślną wartość 0, która jest zwracana zamiast zgłoszenia błędu.
Istnieje również możliwość sprawdzenia, czy w żądaniu brakuje jakichkolwiek parametrów:
ParameterParser analiza = new ParameterParser(zad);
String[] wymagane = { "nazwa1", "nazwa2", "konto" };
String[] brakujace = analiza.getMissingParameters(wymagane);
Powyższa metoda zwraca null, jeżeli nie brakuje żadnego parametru.
ParameterParser obsługuje także internacjonalizację poprzez metodę setCharacterEncoding(). Określa ona kodowanie, które powinno zostać wykorzystane podczas interpretacji wartości parametrów. Wartość może pochodzić z cookie użytkownika, ukrytego pola formularza lub sesji użytkownika:
ParameterParser analiza = new ParameterParser(zad);
analiza.setCharacterEncoding("Shift_JIS");
String wartoscJaponia = analiza.getStringParameter("nazwaLatin");
Wewnętrznie ParameterParser wykorzystuje sztuczkę getBytes() przedstawioną w rozdziale 13, „Internacjonalizacja” do obsługi konwersji. Nazwy parametrów muszą być ciągle podane w kodowaniu Latin1, ponieważ mechanizm poszukiwania wykorzystuje jeszcze nie zinternalizowane metody Servlet API getParameter() i getParameterValues().
Kod ParameterParser
Klasa ParameterParser zawiera ponad tuzin metod, które zwracają parametry żądania — dwie dla każdego rdzennego typu Javy. Posiada również dwie metody getStringParameter() w przypadku, gdyby konieczne było pobranie parametru w jego surowym formacie String. Kod ParameterParser jest przedstawiony w przykładzie 19.1. Wyjątek ParameterNotFoundExceptoion znajduje się w przykładzie 19.2.
Przykład 19.1.
Klasa ParameterParser
package com.oreilly.servlet;
import java.io.*;
import java.util.*;
import javax.servlet.*;
public class ParameterParser {
private ServletRequest req;
private String encoding;
public ParameterParser(ServletRequest req) {
this.req = req;
}
public void setCharacterEncoding(String encoding)
throws UnsupportedEncodingException {
// Sprawdzenie prawidłowości kodowania
new String("".getBytes("8859_1"), encoding);
// Jeżeli tutaj, to prawidłowe, więc jego ustawienie
this.encoding = encoding;
}
public String getStringParameter(String name)
throws ParameterNotFoundException {
String[] values = req.getParameterValues(name);
if (values == null) {
throw new ParameterNotFoundException(name + " not found");
}
else if (values[0].length() == 0) {
throw new ParameterNotFoundException(name + " was empty");
}
else {
if (encoding == null) {
return values[0];
}
else {
try {
return new String(values[0].getBytes("8859_1"), encoding);
}
catch (UnsupportedEncodingException e) {
return values[0]; // should never happen
}
}
}
}
public String getStringParameter(String name, String def) {
try { return getStringParameter(name); }
catch (Exception e) { return def; }
}
public boolean getBooleanParameter(String name)
throws ParameterNotFoundException, NumberFormatException {
String value = getStringParameter(name).toLowerCase();
if ((value.equalsIgnoreCase("true")) ||
(value.equalsIgnoreCase("on")) ||
(value.equalsIgnoreCase("yes"))) {
return true;
}
else if ((value.equalsIgnoreCase("false")) ||
(value.equalsIgnoreCase("off")) ||
(value.equalsIgnoreCase("no"))) {
return false;
}
else {
throw new NumberFormatException("Parameter " + name + " value " + value +
" is not a boolean");
}
}
public boolean getBooleanParameter(String name, boolean def) {
try { return getBooleanParameter(name); }
catch (Exception e) { return def; }
}
public byte getByteParameter(String name)
throws ParameterNotFoundException, NumberFormatException {
return Byte.parseByte(getStringParameter(name));
}
public byte getByteParameter(String name, byte def) {
try { return getByteParameter(name); }
catch (Exception e) { return def; }
}
public char getCharParameter(String name)
throws ParameterNotFoundException {
String param = getStringParameter(name);
if (param.length() == 0)
throw new ParameterNotFoundException(name + " is empty string");
else
return (param.charAt(0));
}
public char getCharParameter(String name, char def) {
try { return getCharParameter(name); }
catch (Exception e) { return def; }
}
public double getDoubleParameter(String name)
throws ParameterNotFoundException, NumberFormatException {
return new Double(getStringParameter(name)).doubleValue();
}
public double getDoubleParameter(String name, double def) {
try { return getDoubleParameter(name); }
catch (Exception e) { return def; }
}
public float getFloatParameter(String name)
throws ParameterNotFoundException, NumberFormatException {
return new Float(getStringParameter(name)).floatValue();
}
public float getFloatParameter(String name, float def) {
try { return getFloatParameter(name); }
catch (Exception e) { return def; }
}
public int getIntParameter(String name)
throws ParameterNotFoundException, NumberFormatException {
return Integer.parseInt(getStringParameter(name));
}
public int getIntParameter(String name, int def) {
try { return getIntParameter(name); }
catch (Exception e) { return def; }
}
public long getLongParameter(String name)
throws ParameterNotFoundException, NumberFormatException {
return Long.parseLong(getStringParameter(name));
}
public long getLongParameter(String name, long def) {
try { return getLongParameter(name); }
catch (Exception e) { return def; }
}
public short getShortParameter(String name)
throws ParameterNotFoundException, NumberFormatException {
return Short.parseShort(getStringParameter(name));
}
public short getShortParameter(String name, short def) {
try { return getShortParameter(name); }
catch (Exception e) { return def; }
}
public String[] getMissingParameters(String[] required) {
Vector missing = new Vector();
for (int i = 0; i < required.length; i++) {
String val = getStringParameter(required[i], null);
if (val == null) {
missing.addElement(required[i]);
}
}
if (missing.size() == 0) {
return null;
}
else {
String[] ret = new String[missing.size()];
missing.copyInto(ret);
return ret;
}
}
}
Przykład 19.2.
Klasa ParameterNotFoundException
package com.oreilly.servlet;
public class ParameterNotFoundException extends Exception {
public ParameterNotFoundException() {
super();
}
public ParameterNotFoundException(String s) {
super(s);
}
}
Wysyłanie poczty elektronicznej
Czasami wysłanie przez serwlet wiadomości jest konieczne, a czasami jest to po prostu ułatwienie. Na przykład, proszę sobie wyobrazić serwlet otrzymujący dane z formularza użytkownika, który umieszcza tam swoje komentarze. Serwlet ten może chcieć przesłać dane formularza na listę dystrybucyjną zainteresowanych stron. Albo proszę sobie wyobrazić serwlet, który napotyka niespodziewany problem i może wysłać administratorowi pocztą stronę prosząc o pomoc.
Serwlet może wykorzystać do wysłania poczty elektronicznej jedną z czterech metod:
Może sam zarządzać szczegółami — nawiązać połączenie przez zwykły port z serwerem poczty i wysyłając wiadomość poprzez protokół pocztowy niskiego poziomu, zazwyczaj tak zwany Simple Mail Transfer Protocol (Prosty Protokół Transferu Poczty — SMTP).
Może uruchomić z wiersza poleceń zewnętrzny program pocztowy, jeżeli system serwera posiada taki program.
Może wykorzystać interfejs JavaMail, zaprojektowany do pomocy w skomplikowanej obsłudze, tworzeniu i przetwarzaniu poczty (proszę zobaczyć http://java.sun.com/products/javamail).
Może wykorzystać jedną z wielu dostępnych bezpłatnie klas pocztowych, które zmieniają szczegóły wysyłania poczty na proste i łatwe do wywołania metody.
W przypadku nieskomplikowanego wysyłania poczty poleca się ostatni sposób ze względu na jego prostotę. Dla bardziej skomplikowanych zastosowań rekomendowany jest JavaMail — zwłaszcza w przypadku serwletów działających pod kontrolą serwera J2EE, w którym na pewno dostępne są dwa pliki JAR wymagane przez JavaMail.
Stosowanie klasy MailMessage
Dla celów tego przykładu zademonstrowany zostanie serwlet wykorzystujący klasę com.oreilly.servlet.MailMessage, która nie zostanie tu przedstawiona, ale jest dostępna pod adresem http://www.servlets.com. Została ona utworzona na podstawie klasy sun.net.smtp.SmtpClient zawartej w JDK firmy Sun, ale unika politycznego problemu wykorzystywania „niewspieranej i będącej celem zmian” klasy sun.*. Posiada ona również kilka przyjemnych usprawnień funkcjonalności. Jej stosowanie jest proste:
Wywołanie MailMessage wiad = new MailMessage(). Opcjonalnie przekazanie konstruktorowi nazwy komputera, który należy wykorzystać jako serwer poczty i który zastępuje domyślny localhost. Większość komputerów pracujących pod Uniksem może działać jako serwer poczty SMTP.
Wywołanie wiad.from(adresOd), określające adres nadawcy. Adres nie musi być prawidłowy.
Wywołanie wiad.to(adresDo), określające adres odbiorcy. Metoda ta może zostać wywołana kilka razy, jeżeli istnieją dodatkowi odbiorcy. Występują również metody cc() i bcc().
Wywołanie wiad.setSubject(temat) w celu określenia tematu. Technicznie nie jest to wymagane, ale umieszczenie tematu zawsze jest dobrym pomysłem.
Wywołanie wiad.setHeader(nazwa, wartosc) jeżeli powinny zostać dołączone dodatkowe nagłówki. Nagłówki To: (Do), CC: (DW) i Subject: (Temat) nie muszą być ustawiane, ponieważ są one automatycznie określane przez metody to(), cc() i setSubject(). (Metoda bcc() oczywiście nie wysyła żadnych nagłówków.) Metodę setHeader() można wykorzystać do nadpisania wartości domyślnych. Nazwy i wartości nagłówków powinny być podporządkowane zasadom podanym w dokumencie RFC 822, dostępnym pod adresem http://www.ietf.org/rfc/rfc0822.txt.
Wywołanie PrintStream wiad = wiad.getPrintStream() w celu pobrania potoku wynikowego wiadomości.
Wysłanie zawartości wiadomości pocztowej do PrintStream.
Wywołanie wiad.sendAndClose() w celu wysłania wiadomości i zamknięcia połączenia z serwerem.
Wysyłanie pocztą danych formularza
Przykład 19.3 przedstawia serwlet wysyłający dane, które otrzymuje, pocztą na listę dystrybucyjną. Proszę zauważyć bardzo szerokie wykorzystanie klasy ParametrParser.
Przykład 19.3.
Wysyłanie wiadomości z serwletu.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.oreilly.servlet.MailMessage;
import com.oreilly.servlet.ParameterParser;
import com.oreilly.servlet.ServletUtils;
public class SerwletPoczta extends HttpServlet {
static final String FROM = "SerwletPoczta";
static final String TO = "komentarze@wazna-firma.com";
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();
ParameterParser analiza = new ParameterParser(zadanie);
String from = analiza.getStringParameter("from", FROM);
String to = analiza.getStringParameter("to", TO);
try {
MailMessage wiad = new MailMessage(); // przyjęcie, że localhost
wiad.from(from);
wiad.to(to);
wiad.setSubject("Komentarz klienta");
PrintStream glowny = wiad.getPrintStream();
Enumeration enum = zad.getParameterNames();
while (enum.hasMoreElements()) {
String name = (String)enum.nextElement();
if (nazwa.equals("to") || nazwa.equals("from")) continue; //Opuszczenie góra/dół
String wartosc = analizator.getStringParameter(nazwa, null);
glowny.println(nazwa + " = " + wartosc);
}
glowny glowny.tytul2();
glowny.println("---");
body.println("Wysłany przez " + HttpUtils.getRequestURL(zad));
wiad.sendAndClose();
wyj.println("Dziękujemy za komentarz...");
}
catch (IOException w) {
wyj.println("Wystąpił problem z obsługą komentarza...");
log("Wystąpił problem z wysyłaniem wiadomości", e);
}
}
}
Serwlet po pierwsze określa adresy „Od” i „Do” wiadomości. Domyślne wartości są ustawione w zmiennych FROM i TO, chociaż wysyłany formularz może zawierać (najprawdopodobniej ukryte) pola określające alternatywy adresów Od i Do. Serwlet następnie rozpoczyna tworzenie wiadomości pocztowej SMTP. Łączy się z lokalnym komputerem i adresuje wiadomość. Następnie ustawia temat i wstawia dane formularza do zawartości, ignorując zmienne FROM i TO. Na końcu wysyła wiadomość i dziękuje użytkownikowi za komentarz. Jeżeli wystąpi problem, informuje o tym użytkownika i zapisuje wyjątek w dzienniku zdarzeń.
Klasa MailMessage aktualnie nie obsługuje załączników, ale ich obsługa może zostać w prosty sposób dodana. W przypadku zastosowań bardziej zaawansowanych, dobrą alternatywą jest JavaMail.
Stosowanie wyrażeń regularnych
Ten podrozdział przeznaczony jest szczególnie dla osób znających oparte na CGI skrypty Perla i przyzwyczajonych do możliwości wyrażeń regularnych w Perlu. W tym rozdziale zostaną przedstawione zasady korzystania z wyrażeń regularnych w Javie. Osobom nie znającym tematu należy się wyjaśnienie, że wyrażenia regularne są mechanizmem umożliwiającym bardzo zaawansowaną manipulację łańcuchami przy minimalnej ilości kodu. Cała moc wyrażeń regularnych jest doskonale przedstawiona w książce „Mastering Reular Expressions” autorstwa Jefreya E. F. Friedla (O'Reilly).
Pomimo dużej ilości klas i możliwości dodawanych przez firmę Sun do JDK przez wiele lat, ciągle brakuje w nim wyrażeń regularnych. Nie należy się tym jednak martwić. Podobnie jak w przypadku większości funkcji Javy, jeżeli nie dostarcza ich Sun, można je uzyskać za rozsądną cenę od innego niezależnego producenta, a jeżeli jest to coś tak przydatnego, jak wyrażenia regularne, istnieje duża szansa, że dostępna jest odpowiednia biblioteka Open Source. Tak jest również w tym przypadku — istnieje mechanizm wyrażeń regularnych Open Source dostępny jako część Apache Jakarta Project, stworzony początkowo przez Jonathana Locke'a i dostępny w licencji Apache. Licencja Apache jest dużo bardziej liberalna niż Ogólna Licencja Publiczna GNU (GPL), ponieważ pozwala programistom na wykorzystanie mechanizmu wyrażeń regularnych przy tworzeniu nowych produktów bez konieczności udostępniania tych produktów jako Open Source.
Odnajdywanie łącz przy pomocy wyrażeń regularnych
W celu zademonstrowania zastosowań wyrażeń regularnych, wykorzystany zostanie mechanizm wyrażeń regularnych Apache w celu utworzenia serwletu, który pobiera i wyświetla listę wszystkich łącz <A HREF> HTML odnalezionych na stronie WWW. Jego kod jest przedstawiony w przykładzie 19.4.
Przykład 19.4.
Wyszukiwanie wszystkich łącz
import java.io.*;
import java.net.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.oreilly.servlet.*;
import org.apache.regexp.*;
public class Lacza extends HttpServlet {
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();
// URL do przetworzenia pobierany jest jako dodatkowa część ścieżki
// np. http://localhost:8080/servlet/Lacza/http://www.servlets.com/
String url = zad.getPathInfo();
if (url == null || url.length() == 0) {
odp.sendError(odp.SC_BAD_REQUEST,
"Proszę przekazać URL do odczytania jako dodatkową część ścieżki ");
return;
}
url = url.substring(1); // odcięcie wiodącego '/'
String strona = null;
try {
// Żądanie strony
HttpMessage wiad = new HttpMessage(new URL(url));
BufferedReader in =
new BufferedReader(new InputStreamReader(wiad.sendGetMessage()));
// Przekształcenie całej odpowiedzi do String
StringBuffer buf = new StringBuffer(10240);
char[] znaki = new char[10240];
int znakiOdczyt = 0;
while ((znakiOdczyt = in.read(znaki, 0, znaki.length)) != -1) {
buf.append(znaki, 0, znakiOdczyt);
}
strona = buf.toString();
}
catch (IOException w) {
odp.sendError(odp.SC_NOT_FOUND),
"Niemożliwe odczytanie " + url + ":<BR>" +
ServletUtils.getStackTraceAsString(w));
return;
}
wyj.println("<HTML><HEAD><TITLE>Pobieranie łączy</TITLE>");
try {
// Konieczne określenie <BASE> żeby względne łącza działały właściwie
// Jeżeli strona już go posiada, można go użyć
RE wr = new RE("<base[^>]*>", RE.MATCH_CASEINDEPENDENT);
boolean maBase = wr.match(strona);
if (maBase) {
// Wykorzystanie istniejącego <BASE>
wyj.println(wr.getParen(0));
}
else {
// Obliczenie BASE z URL-a, wykorzystanie wszystkiego do ostatniego '/'
wr = new RE("http://.*/", RE.MATCH_CASEINDEPENDENT);
boolean odczytBase = wr.match(url);
if (odczytBase) {
// Sukces, wyświetlenie odczytanego BASE
wyj.println("<BASE HREF=\"" + wr.getParen(0) + "\">");
}
else {
// Brak wiodącego ukośnika, dodanie go
wyj.println("<BASE HREF=\"" + url + "/" + "\">");
}
}
wyj.println("</HEAD><BODY>");
wyj.println("Łącza na <A HREF=\"" + url + "\">" + url + "</A>" +
" są następujące: <BR>");
wyj.println("<UL>");
String szukaj = "<a\\s+[^<]*</a\\s*>";
wr = new RE(szukaj, RE.MATCH_CASEINDEPENDENT);
int indeks = 0;
while (wr.match(strona, indeks)) {
String pasuje = wr.getParen(0);
indeks = wr.getParenEnd(0);
wyj.println("<LI>" + pasuje + "<BR>");
}
wyj.println("</UL>");
wyj.println("</BODY></HTML>");
}
catch (RESyntaxException w) {
// Nie powinien wystąpić, bo łańcuchy poszukiwań są sztywne
w.printStackTrace(wyj);
}
}
}
Przykładowy wynik uruchomienia powyższego przykładu jest przedstawiony na rysunku 19.1.
Rysunek 19.1.
Przeglądanie przy minimalnej szybkości transferu
Teraz powyższy kod zostanie opisany dokładniej. Po pierwsze, serwlet określa URL, którego łącza powinny zostać pobrane poprzez odszukanie dodatkowych informacji ścieżki. Oznacza to, że powyższy serwlet powinien zostać wywoływany podobnie do http://localhost:8080/servlet/Lacza/http://www.servlets.com. Następnie serwlet odczytuje zawartość tego URL-a przy pomocy klasy HttpMessage i przechowuje stronę jako String. Dla bardzo dużych stron podejście to nie jest wydajne, ale może służyć jako dobry przykład.
Następnym krokiem jest upewnienie się, że wyświetlana strona posiada właściwy znacznik <BASE> tak, aby łącza względne na stronie były właściwie interpretowane przez przeglądarkę. Jeżeli na pobieranej stronie istnieje już znacznik <BASE>, to można go wykorzystać, tak więc następuje jego poszukiwanie przy pomocy wyrażenia regularnego <base[^>]*>. Łańcuch ten zostaje przekazany do konstruktora org.apache.regexp.RE razem ze znacznikiem wskazującym na niezależność od wielkości liter. Ta składnia wyrażenia regularnego jest standardowa i identyczna jak w Perlu. Nakazuje ona wyszukanie tekstu <base>, po którym następuje dowolna ilość znaków nie będących >, po których następuje tekst >. Jeżeli właściwy tekst zostanie odnaleziony, jest on pobierany przy pomocy wr.getParen(0). Metoda ta pobiera najbardziej zewnętrzne łańcuch (pasujące łańcuchy mogą być zagnieżdżane przy pomocy nawiasów).
Jeżeli na stronie nie występuje znacznik <BASE>, należy go skonstruować. <BASE> powinien zawierać cały źródłowy URL razem z ostatnim ukośnikiem. Informacja ta może zostać pobrana przy pomocy prostego wyrażenia regularnego http://.*/. Nakazuje ono dopasowanie do tekstu http://, po którym następuje dowolna ilość znaków kończąca się /. Wzór .* odczytuje maksymalną możliwą ilość znaków, która nie koliduje z innymi warunkami wyrażenia regularnego (terminologia wyrażeń regularnych nazywa to działanie zachłannym), tak więc wyrażenie to zwraca wszystko łącznie z wiodącym ukośnikiem. Jeżeli ukośnik taki nie występuje, zostaje on po prostu dodany.
Na końcu ze strony pobrane zostają znaczniki <A HREF>, przy pomocy stosunkowo skomplikowanego wyrażenia regularnego <a\s+[^<]*</a\s*>. (Można dostrzec, że w kodzie znaki \ zostały wyłączone przy pomocy dodatkowego znaku \.) Nakazuje to poszukiwanie tekstu <a, po którym następuje dowolna liczba znaków, które nie są <, następnie tekst </a, dalej dowolna ilość pustego miejsca, potem >. Jeżeli złoży się te informacje, okaże się, że następuje pobranie znaczników <A HREF> od początkowego <A do końcowego </A>, niezależnie od wielkości liter i ilości pustego miejsca, zabezpieczone przed omyłkowym dopasowaniem znaczników takich jak <APPLET>. Po odnalezieniu każdego pasującego tekstu jest on wyświetlany na liście, a poszukiwanie jest kontynuowane przy pomocy indeksu zapisującego następny punkt początkowy.
Większa ilość informacji na temat działań możliwych do wykonania przy pomocy wyrażeń regularnych jest dostępna w dokumentacji dołączonej do biblioteki.
Uruchamianie programów
Czasami serwlet musi wykonać program zewnętrzny. Jest to generalnie działanie ważne w sytuacjach, w których zewnętrzny program oferuje funkcjonalność niedostępną przy pomocy samej Javy. Na przykład, serwlet może wywołać zewnętrzny program w celu manipulowania obrazkiem lub sprawdzenia stanu serwera. Uruchamianie programów zewnętrznych utrudnia przenośność i zachowanie bezpieczeństwa. Jest to działanie, które powinno być podejmowane tylko wtedy, gdy jest absolutnie konieczne i jedynie przez serwlety, pracujące pod kontrolą stosunkowo liberalnego mechanizmu bezpieczeństwa — konkretnie mechanizmu zezwalającego serwletom na wywoływanie metody exec() java.lang.Runtime.
Finger
Program finger zapytuje (przypuszczalnie zdalny) komputer o listę aktualnie zalogowanych użytkowników. Jest on dostępny w praktycznie wszystkich systemach Uniksowych i niektórych komputerach z Windows NT posiadających zdolność obsługi sieci. Program finger działa przez połączenie z demonem finger (zazwyczaj noszącym nazwę fingerd), który nasłuchuje na porcie 79. finger wykonuje swoje żądanie do fingerd przy pomocy własnego protokołu „finger”, a fingerd odpowiada właściwą informacją. Większość systemów Uniksowych posiada fingerd, ale duża część skoncentrowanych na bezpieczeństwie administratorów wyłącza go w celu ograniczenia ilości informacji, które mogłyby zostać wykorzystane podczas prób włamania. Ciągle ciężko jest odnaleźć fingerd w systemach Windows. Uruchomiony bez żadnych argumentów, finger zgłasza wszystkich użytkowników na lokalnym komputerze. Na przykład:
% finger
Login Name TTY Idle When Office
jkowalski Jan Kowalski q0 3:13 Thu 12:13
anowak Anna Nowak q1 Thu 12:18
Uruchomiony z nazwą użytkownika jako argumentem, finger zwraca informacje na temat tego użytkownika:
% finger jkowalski
Login name: jkowalski In real life: Jan Kowalski
Directory: /usr/people/jkowalski Shell: /bin/tcsh
On since Jan 1 12:13:28 on ttyq0 from :0.0
3 hours 13 minutes Idle Time
On since Jan 1 12:13:28 on ttyq2 from :0.0
Uruchomiony z nazwą komputera jako argumentem, finger zwraca informacje o wszystkich użytkownikach określonego komputera. Zdalny komputer musi posiadać działający fingerd:
% finger @fikcja
Login Name TTY Idle When Office
ppawlak Piotr Pawlak q0 17d Mon 10:45
I, oczywiście, jeżeli uruchomiony z nazwą użytkownika i nazwą komputera, finger zwraca informacje na temat określonego użytkownika na określonym komputerze:
% finger ppawlak@fikcja
[fikcja.calkowita.com]
Login name: ppawlak In real life: Piotr Pawlak
Directory: /usr/people/ppawlak Shell: /bin/tcsh
On since Dec 15 10:45:22 on ttyq0 from :0.0
17 days Idle Time
Uruchamianie polecenia finger
Zakładając, że serwlet pragnie dostępu do informacji odczytanych przez finger, posiada on dwie możliwości — może ustanowić połączenie przez port z fingerd i wykonać żądanie informacji tak, jak dowolny inny klient finger, lub może uruchomić z wiersza poleceń program finger, który wykonuje połączenie na własną rękę i odczytać informacje zwracane przez finger. Tu zostanie przedstawiona druga technika.
Przykład 19.5 przedstawia sposób wykonania przez serwlet polecenia finger w celu sprawdzenia, którzy użytkownicy są zalogowani na lokalnym komputerze. Odczytuje on wynik polecenia i wyświetla go w standardowym urządzeniu.
Przykład 19.5.
Wykonywanie polecenia finger przy pomocy serwletu
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.oreilly.servlet.ServletUtils;
public class Finger extends HttpServlet {
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();
String polecenie = "finger";
Runtime czas = Runtime.getRuntime();
Process proces = null;
try {
proces = czas.exec(polecenie);
DataInputStream in = new DataInputStream(proces.getInputStream());
// Odczytanie i wyświetlenie wyników
String linia = null;
while ((linia = in.readLine()) != null) {
wyj.println(linia);
}
}
catch (Exception w) {
wyj.println("Problem z finger: " +
ServletUtils.getStackTraceAsString(w));
}
}
}
Serwlet wykorzystuje polecenie exec() tak, jak każda inna klasa Javy. Uruchamia polecenie finger, po czym odczytuje i wyświetla wynik. Jeżeli wystąpi problem, serwlet wyłapuje wyjątek i wyświetla użytkownikowi ścieżkę stosu. Powyższy serwlet zakłada, że polecenie finger znajduje się w domyślnej ścieżce programów. Jeżeli nie jest to prawda, należy zmienić łańcuch polecenia tak, by określał on ścieżkę do finger.
Należy wskazać, że chociaż Java wykonuje rdzenny kod podczas uruchamiania programu finger, sama nie naraża się na ryzyko normalnie związane z wykonywaniem rdzennego kodu. Dzieje się tak, ponieważ program finger jest wykonywany jako oddzielny proces. Może on załamać się lub zostać zabity bez wpływu na serwer, na którym działa serwlet.
Uruchamianie finger z argumentami
Zakładając, że polecenie finger powinno zostać wywołane z argumentem, jego wywołanie powinno wyglądać nieco inaczej. Metoda exec() pobiera pojedynczy łańcuch określający polecenie, lub tablicę łańcuchów, określającą polecenie i argumenty, które należy mu przekazać. Kod uruchamiający finger jkowalski przedstawiony jest w przykładzie 19.6.
Przykład 19.6.
Dodawanie parametru do uruchamianego polecenia
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.oreilly.servlet.ServletUtils;
public class Finger extends HttpServlet {
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();
String[] polecenie = { "finger", "jkowalski" }; // jedyna zmiana
Runtime czas = Runtime.getRuntime();
Process proces = null;
try {
proces = czas.exec(polecenie);
DataInputStream in = new DataInputStream(proces.getInputStream());
// Odczytanie i wyświetlenie wyników
String linia = null;
while ((linia = in.readLine()) != null) {
wyj.println(linia);
}
}
catch (Exception w) {
wyj.println("Problem z finger: " +
ServletUtils.getStackTraceAsString(w));
}
}
}
Zmienna polecenie jest teraz tablicą łańcuchów {"finger", "jkowalski"}. Polecenie nie działałoby jako pojedynczy łańcuch "finger jkowalski".
Uruchamianie finger z przekierowywanym wynikiem
Ostatnim przykładem będzie opis przekierowywanego wyniku polecenia finger. Wynik może zostać przekierowany do pliku w celu późniejszego wykorzystania przy pomocy wywołania finger jkowalski > /tmp/jkowalski. Można również przekierować wynik do programu grep w celu usunięcia odwołań do pewnego użytkownika, na przykład finger|grep -v jkowalski.
Zadanie to jest trudniejsze, niż może się wydawać. Jeżeli zmienna polecenia jest ustawiona jako łańcuch finger|grep -v jkowalski, Java traktuje ten łańcuch jako nazwę pojedynczego programu — którego najprawdopodobniej nie odnajdzie. Jeżeli zmienna polecenia jest ustawiona jako tablica łańcuchów {"finger", "|", "grep", "-v", "jkowalski"}, Java wykonuje polecenie finger i przekazuje mu następne cztery łańcuchy jako parametry, co bez wątpienia zmyli program finger.
Rozwiązanie tego problemu wymaga zrozumienia, że przekazanie jest funkcją powłoki. Powłoka to program, w którym zazwyczaj wpisuje się polecenia. W Uniksie najpopularniejszymi powłokami są csh, tsh, bash i sh. W Windows 95/98 powłoka to zazwyczaj command.com. W Windows NT i Windows 2000, powłoką jest command.com lub cmd.exe.
Zamiast bezpośrednio uruchamiać finger, uruchomiona zostaje powłoka i przekazuje się jej łańcuch polecenia, które powinno zostać wykonane. Taki łańcuch może zawierać polecenie finger i dowolny rodzaj przekierowania. Powłoka może zanalizować polecenie, prawidłowo rozpoznać i wykonać przekierowanie. Dokładne polecenie musi uruchomić powłokę, tak więc program zależy od powłoki, czyli od systemu operacyjnego. Technika ta w związku z tym ogranicza niezależność serwletów od platformy. W systemie Uniksowym poniższa zmienna polecenia nakazuje csh wykonanie polecenia finger|grep -v jkowalski:
String[] polecenie = { "/bin/csh", "-c", "finger | grep -v jkowalski" };
Program, który wykonuje Java to /bin/csh. csh zostają przekazane dwa argumenty: -c, które nakazuje powłoce wykonanie następnego parametru, oraz finger|grep -v jkowalski, który jest parametrem wykonywanym przez powłokę.
W systemie Windows zmienna polecenia powinna wyglądać następująco:
String[] polecenie = { "command.com", "/c", "finger | grep -v jkowalski" };
Argument /c command.com działa w ten sam sposób, co -c csh. Przyrostek .com jest konieczny. Użytkownicy Windows NT powinni zapamiętać, że wykorzystanie cmd.exe może sprawiać problemy, ponieważ przekierowuje ono wynik do nowego okna zamiast do konsoli Javy, która je uruchomiła. Właściwie nawet uruchomienie serwera z powłoki cmd.exe może spowodować błąd w poleceniu command.com.
Stosowanie rdzennych metod
Pomimo nacisków firmy Sun na stosowanie czystego kodu Javy, rdzenny kod wciąż posiada swoje zastosowania. Rdzenny kod jest konieczny przy wykonywaniu zadań, których Java (i zewnętrzne problemy przez nią uruchamiane) nie może wykonać — są to blokowanie plików, uzyskiwanie dostępu do identyfikatorów użytkowników i współdzielonej pamięci, wysyłanie faksów i tak dalej. Rdzenny kod jest przydatny również podczas uzyskiwania dostępu do starych danych poprzez bramki nie przystosowane do Javy. Także w sytuacjach, w których ważny jest każdy element wydajności, biblioteki rdzennego kodu mogą spowodować poważne przyśpieszenie serwletu.
Jednak rdzenny kod nie powinien być wykorzystywany poza sytuacjami, w których jest absolutnie konieczny, ponieważ jeśli rdzenny kod uruchomiony przez serwlet spowoduje błąd, cały serwer może się załamać! Zabezpieczenia Javy nie mogą chronić serwera przed załamaniami rdzennego kodu. Z tego powodu nie jest rozsądnym wykorzystywanie rdzennego mostu JDBC-ODBC przez serwlet, ponieważ wiele sterowników ODBC może mieć problemy z dostępem wielowątkowym. Rdzenny kod ogranicza również niezależność serwletu od platformy. Chociaż nie jest to ważne w przypadku własnych serwletów przywiązanych do konkretnego serwera, należy o tym pamiętać.
Sposób uzyskiwania przez serwlet dostępu do rdzennych metod zależy od serwera WWW i JVM, na których jest uruchomiony. Można podjąć ryzyko i powiedzieć ogólnie, że można z dużym prawdopodobieństwem spodziewać się, że serwer WWW i wirtualna maszyna Javy obsługują standardowy Java Native Interface (Rdzenny Interfejs Javy — JNI). Wykorzystywanie JNI jest stosunkowo skomplikowane i nawet podstawowe wprowadzenie do niego wykracza poza zakres tego rozdziału.
Podczas stosowania JNI z serwletami należy pamiętać o następujących sprawach:
Jedynie najbardziej liberalne mechanizmy bezpieczeństwa serwerów pozwalają serwletem na wykonywanie rdzennego kodu.
W JDK 1.1.x występuje błąd, który nie pozwala na wykorzystywanie rdzennego kodu przez klasę pobraną przy pomocy własnego mechanizmu ładowania klas (takiego jak mechanizm pobierający serwlety z domyślnego katalogu serwletów). Serwlety wykorzystujące rdzenny kod muszą w związku z tym być umieszczone w ścieżce klas serwera (takim jak katalog_macierzysty/classes).
Katalog, w którym umieszczona jest współdzielona biblioteka (lub biblioteki dołączane dynamicznie, czyli DLL) zawierająca rdzenny kod zależy od serwera WWW i JVM. Niektóre serwery posiadają konkretne miejsca przeznaczone dla bibliotek współdzielonych. Jeżeli serwer nie określa konkretnego katalogu bibliotek współdzielonych, proszę spróbować umieścić bibliotekę w miejscu zależnym od JVM, takim jak katalog_JDK/bin lub katalog_JDK/lib (gdzie katalog_JDK jest katalogiem macierzystym instalacji JDK), lub miejscu zależnym od systemu, takim jak katalog_windows/system32 lub /usr/lib.
Występowanie jako klient RMI
W rozdziale 10, „Komunikacja aplet-serwlet” opisany został sposób, w jaki serwlet występować może jako serwer RMI. Tutaj role zostaną odwrócone i przedstawiony zostanie serwlet działający jako klient RMI. Poprzez podjęcie się roli klienta RMI, serwlet może wykorzystać usługi innych serwerów w celu wypełnienia swojego zadania, skoordynować swoje wysiłki z innymi serwerami lub serwletami na tych serwerach i/lub działać jako proxy dla apletów nie mogących samodzielnie komunikować się z serwerami RMI.
Przykład 19.7 przedstawia SerwletKlientGodziny, serwlet, który pobiera aktualną godzinę dnia z serwera RMI SerwletGodziny przedstawionego w rozdziale 10.
Przykład 19.7.
Serwlet jako klient RMI
import java.io.*;
import java.rmi.*;
import java.rmi.registry.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SerwletKlientGodziny extends HttpServlet {
SerwerGodziny godzina;
// Zwraca odwołanie do SerwerGodziny, lub null jeżeli wystąpi problem.
protected SerwerGodziny pobierzSerwerGodziny() {
// Jeżeli wykorzysta się funkcję dynamicznego ładowania kodu RMI.
// trzeba ustawić mechanizm bezpieczeństwa taki jak RMISecurityManager
// if (System.getSecurityManager() == null) {
// System.setSecurityManager(new RMISecurityManager());
// }
try {
Registry rejestr =
LocateRegistry.getRegistry(pobierzRejestrKomp(), pobierzRejestrPort());
return (SerwerGodziny)registry.lookup(pobierzRejestrNazwa());
}
catch (Exception w) {
getServletContext().log(w, "Problem z pobraniem odwołania do SerwerGodziny");
return null;
}
}
private String pobierzRejestrNazwa() {
String nazwa = getInitParameter("registryName");
return (nazwa == null ? "SerwletGodziny" : nazwa);
}
private String pobierzRejestrKomp() {
// Zwrócenie nazwy komputera podanej przez "registryHost"
// lub w przeciwnym wypadku null nakazujący wybranie localhost
return getInitParameter("registryHost");
}
private int pobierzRejestrPort() {
try { return Integer.parseInt(getInitParameter("registryPort")); }
catch (NumberFormatException w) { return Registry.REGISTRY_PORT; }
}
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();
// pobranie obiektu godzina jeżeli nie zostało to zrobione wcześniej
if (godzina == null) {
godzina = pobierzSerwerGodziny();
if (godzina == null) {
// Pobranie niemożliwe, więc zgłoszenie niedostępności.
throw new UnavailableException(this, "Zlokalizowanie godzina niemożliwe");
}
}
// Pobranie i wyświetlenie aktualnego czasu na (zazwyczaj zdalnym) komputerze
// godzina
wyj.println(godzina.getDate().toString());
}
}
Powyższy serwlet powinien wydawać się podobny do apletu opisanego w rozdziale 10. Zarówno serwlety jak i aplety muszą wykonać te same podstawowe działania w celu uzyskania dostępu do serwera RMI. Oba lokalizują rejestr przy pomocy nazwy komputera i numeru portu, następnie wykorzystują ten rejestr do odszukania odwołania do obiektu zdalnego. Jedyna możliwa różnica jest taka, że serwlet, jeżeli korzysta z funkcji dynamicznego ładowania kodu RMI w celu automatycznego pobrania klasy-końcówki z innego komputera, musi najpierw upewnić się, że działa pod strażą mechanizmu bezpieczeństwa w celu uchronienia siebie od potencjalnie niebezpiecznej pobranej zdalnie klasy-końcówki. Aplet zawsze działa pod kontrolą mechanizmu bezpieczeństwa apletów, tak więc krok ten nie jest konieczny. Serwlet może jednak działać bez domyślnego mechanizmu bezpieczeństwa, tak więc kiedy działa jako klient RMI, należy mu go przypisać.
Usuwanie błędów
Faza testowania/usuwania błędów może być jednym z najtrudniejszych aspektów programowania serwletów. Serwlety mogą wykorzystywać w dużym stopiniu interakcję klient/serwer, co ułatwia powstawanie błędów, ale utrudnia ich reprodukcję. Ciężkie może również okazać się wyśledzenie nieoczywistego błędu, ponieważ serwlety nie współpracują zbyt dobrze ze standardowymi programami uruchomieniowymi, ponieważ działają na mocno wielowątkowych i ogólnie złożonych serwerach WWW. Poniżej umieszczono kilka rad i sztuczek pomocnych przy usuwaniu błędów.
Sprawdzenie dzienników zdarzeń
Kiedy podejrzewa się istnienie błędu, należy po pierwsze sprawdzić dzienniki zdarzeń. Większość serwerów tworzy dziennik błędów, w którym można odnaleźć listę wszystkich błędów zaobserwowanych przez serwer oraz dziennik zdarzeń, który zawiera listę interesujących zdarzeń serwletów. Dziennik zdarzeń może również przechowywać wiadomości zapisane przez serwlety przy pomocy metody log(), ale nie zawsze jest to możliwe.
Proszę zauważyć, że wiele serwerów buforuje wyświetlane informacje w dziennikach w celu poprawienia wydajności. Podczas odkrywania problemu można spróbować zatrzymania buforowania (zazwyczaj przez zmniejszenie wielkości buforu serwera do zera bajtów), co pozwala na dostrzeżenie problemu w momencie jego wystąpienia. Należy pamiętać o przywróceniu później rozsądnej wielkości bufora.
Wyświetlenie dodatkowych informacji
Jeżeli w dzienniku zdarzeń serwera nie można odnaleźć w dziennikach serwera, można spróbować zapisania w dzienniku dodatkowych informacji przy pomocy metody log(). Jak przedstawiono w przykładach w innych miejscach niniejszej książki, zazwyczaj w dziennikach zapisywane są ścieżki stosów i inne sytuacje błędów. Podczas usuwania błędów można dodać kilka tymczasowych poleceń log() działających jako prosty program uruchomieniowy w celu poznania ogólnego sposobu wykonywania kodu i wartości zmiennych serwletu. Czasami dobrze jest umieścić polecenia log() w serwlecie otoczone klauzulami if tak, aby uruchamiały się jedynie wtedy, gdy konkretny parametr inicjacji uruchamiania błędów posiada wartość true.
Pobieranie dodatkowych informacji z dzienników serwera może czasami okazać się nieporęczne. W celu ułatwienia odnajdywania tymczasowych informacji błędów można nakazać serwletowi wyświetlanie klientowi informacji o błędach (poprzez PrintWriter) lub do konsoli serwera (poprzez System.out). Nie wszystkie konsole serwerów są połączone z System.out serwletu; niektóre przekierowują wyświetlane informacje do pliku.
Stosowanie standardowego programu uruchomieniowego
Możliwe jest zastosowanie standardowego programu uruchomieniowego w celu wyśledzenia problemów serwletów, chociaż nie musi to być intuicyjnie oczywiste. Nie można wykonać tego działania bezpośrednio na serwlecie, ponieważ serwlety nie są samodzielnymi programami. Serwlety są rozszerzeniami serwera i jako takie muszą działać wewnątrz serwera.
Na szczęście Tomcat jest serwerem napisanym wyłącznie w Javie i w związku z tym jest idealny do usuwania błędów z serwletów. Jedyną sztuczką, o której należy pamiętać jest konieczność uruchomienia Tomcata wewnątrz programu uruchomieniowego. Dokładne procedury mogą różnić się w zależności od wersji Tomcata (lub innego opartego na Javie serwera WWW), idea jednak pozostaje ta sama:
Ustawienie ścieżki klas programu uruchomieniowego tak, aby mogła odnaleźć klasy i pliki JAR konieczne do uruchomienia Tomcata. Można sprawdzić informacje wyświetlane podczas startu oraz skrypty startowe serwera (tomcat.sh i tomcat.bat), aby uzyskać pomoc w określeniu ścieżki klas.
Ustawienie ścieżki klas programu uruchomieniowego tak, aby mógł on również odnaleźć serwlety i klasy wspierające, co oznacza zazwyczaj katalog WEB-INF/classes i pliki w katalogu WEB-INF/lib. Zazwyczaj te katalogi i pliki JAR nie powinny znajdować się w ścieżce klas, ponieważ uniemożliwia to przeładowywanie serwletów. To dołączenie jednak jest przydatne przy usuwaniu błędów. Umożliwia ono programowi uruchomieniowemu ustawienie punktów kontrolnych w serwlecie zanim mechanizm ładujący serwlety odpowiedzialny za pobieranie klas z WEB-INF załaduje serwlet.
Po właściwym ustawieniu ścieżki klas należy rozpocząć usuwanie błędów poprzez uruchomienie klasy serwera zawierającej podstawową metodę main(). W przypadku Tomcata 3.2 klasa ta to org.apache.tomcat.startup.Tomcat. Inne serwery oparte na Javie i przyszłe wersje Tomcata mogą wykorzystywać inne klasy. Informacje na temat nazwy podstawowej klasy powinny znajdować się w skrypcie startowym.
Tomcat może nakazywać ustawienie konkretnych właściwości systemu lub zmiennych środowiskowych. Na przykład, Tomcat 3.2 poszukuje własności systemu tomcat.home lub zmiennej środowiskowej TOMCAT_HOME w celu określenia swojego podstawowego katalogu. Proszę ustawić je, jeżeli okaże się to konieczne.
Ustawienie punktów kontrolnych w dowolnym serwlecie, w którym powinny zostać usunięte błędy, a następnie wykorzystanie przeglądarki WWW w celu wykonania żądania do HttpServer o dany serwlet (http://localhost:8080/servlet/SerwletDoSprawdzenia). Wykonywanie powinno być przerywane w punktach kontrolnych.
Duża część graficznych programów uruchomieniowych ukrywa te szczegóły i umożliwia zintegrowane sprawdzanie serwletów wykorzystując wbudowany serwer do uruchamiania serwletów. Często dobrze jest jednak znać ręczną procedurę, ponieważ serwery dołączane do graficznych programów uruchomieniowych są zazwyczaj nieco przestarzałe.
Niektóre programy dołączane do kontenerów serwletów (które normalnie działają jedynie w połączeniu z serwerem WWW nie utworzonym w Javie) posiadają samodzielne wersje utworzone w czystej Javie zaprojektowane specjalnie do wykorzystania podczas usuwania błędów, jak opisano powyżej. Zaletą wykorzystania tych serwerów, kiedy są one dostępne, jest możliwość przesuwania dowolnych własnych plików konfiguracyjnych z serwera zewnętrznego do środowiska testowego i z powrotem.
Analiza żądania klienta
Czasami kiedy serwlet nie zachowuje się w spodziewany sposób, warto jest przyjrzeć się surowemu żądaniu HTTP, na które serwlet ten odpowiada. Osoby znające strukturę HTTP mogą odczytać żądanie i zobaczyć dokładnie, w którym miejscu serwlet mógł się pomylić. Jednym ze sposobów dojrzenia surowego żądania jest zamiana procesu serwera WWW na własną aplikację, która wyświetla wszystkie otrzymane informacje. Przykład 19.8 przedstawia taki serwer.
Przykład 19.8.
Przechwytywanie żądania klienta
import java.io.*;
import java.net.*;
import java.util.*;
public class PatrzPort {
private static void printUsage() {
System.out.println("zastosowanie: java PatrzPort port");
}
public static void main(String[] arg) {
if (arg.length < 1) {
printUsage();
return;
}
// Pierwszym argumentem jest port na którym należy nasłuchiwać
int port;
try {
port = Integer.parseInt(arg[0]);
}
catch (NumberFormatException w) {
printUsage();
return;
}
try {
// Ustanowienie portu serwera przyjmującego połączenia klienta
// Każde połączenie przekazywane do wątku obsługującego
ServerSocket ss = new ServerSocket(port);
while (true) {
Socket zadanie = ss.accept();
new WatekObslugi(zadanie).start();
}
}
catch (Exception w) {
w.printStackTrace();
}
}
}
class WatekObslugi extends Thread {
Socket s;
public WatekObslugi(Socket s) {
this.s = s;
}
public void run() {
try {
// Wyświetlenie każdego bajtu wychodzącego z portu
InputStream in = s.getInputStream();
byte[] bajty = new byte[1];
while ((in.read(bajty)) != -1) {
System.out.print((char)bajty[0]);
}
}
catch (Exception w) {
w.printStackTrace();
}
}
}
Nasłuchiwanie na porcie 8080 można rozpocząć przy pomocy następującego polecenia powłoki:
java PatrzPort 8080
Proszę zauważyć, że na jednym porcie nie mogą nasłuchiwać równocześnie dwie aplikacje, tak więc należy upewnić się, że na wybranym porcie nie nasłuchuje żaden inny serwer. Po uruchomieniu serwera można do niego przekierować żądania HTTP tak, jakby był on zwykłym serwerem WWW. Na przykład, można wykorzystać przeglądarkę WWW do obejrzenia strony http://localhost:8080. Kiedy PatrzPort otrzymuje żądanie HTTP przeglądarki, wysyła żądanie do standardowego urządzenia wyświetlającego, aby można je było obejrzeć, Przeglądarka może być zajęta czekaniem na odpowiedź, która nigdy nie nadejdzie, Można to zakończyć przez naciśnięcie przycisku Stop.
Poniżej przedstawiono przykładowy wynik uruchomienia PatrzPort, który pokazuje szczegóły żądania GET na http://localhost:8080:
GET / HTTP 1.0
Connection: Keep-Alive
User-Agent: Mozilla/4.7 [pl] (X11; U; IRIX 6.2 IP22)
Pragma: no-cache
Host: localhost:8080
Accept: image.gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Cookie: JSESSIONIN=To1010mC10934500694587412At
Utworzenie własnego żądania klienta
Oprócz przechwytywania i przeglądania żądań HTTP klienta, przydatne może okazać się utworzenie własnego żądania HTTP. Można to wykonać poprzez połączenie się z portem serwera, na którym serwer nasłuchuje, a następnie wprowadzenie właściwie uformowanego żądania HTTP. W celu ustanowienia połączenia można wykorzystać program telnet, dostępny we wszystkich komputerach Uniksowych i większości komputerów Windows posiadających funkcje sieciowe. Program telnet przyjmuje jako argumenty nazwę komputera i numer portu, z którym powinien się połączyć. Po połączeniu można wykonać żądanie, które będzie wyglądać podobnie do przedstawionego w poprzednim podrozdziale. Na szczęście własne żądanie może być o wiele prostsze — należy określić jedynie pierwszą linię, określającą cel żądania oraz ostatnią, która musi być pustą linią wskazującą na koniec żądania. Na przykład:
% telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost
Escape character is '^]'.
GET /servlet/PrzeglParametr?nazwa=wartosc HTTP/1.0
http/1.1 200 OK.
Server: Tomcat Web Server/3,2
Content-Type: text/plain
Connection: close
Date: Sun, 25 Jun 2000 20:29:06 GMT
Wuery String:
Nazwa=wartosc
Request Parameters
Nazwa (0): wartosc
Connection closed by foreign host.
Jak często się to zdarza, Windows zachowuje się nieco inaczej, niż jest to pożądane. Domyślny program telnet.exe Windows 95/98/NT nadaje złą formę wielu odpowiedziom serwerów WWW, ponieważ nie rozumie, że w sieci WWW znak końca linii powinien być traktowany jako koniec linii i powrót karetki. Zamiast telnet.exe, programiści Windows mogą wykorzystać zachowujący się bardziej odpowiednio program Javy przedstawiony w przykładzie 19.9.
Przykład 19.9.
Inny sposób łączenia się z serwerem WWW
import java.io.*;
import java.net.*;
import java.util.*;
public class KlientHttp {
private static void printUsage() {
System.out.println("usage: java KlientHttp komputer port");
}
public static void main(String[] arg) {
if (arg.length < 2) {
printUsage();
return;
}
// Komputer to pierwszy parametr, port to drugi
String komp = arg[0];
int port;
try {
port = Integer.parseInt(arg[1]);
}
catch (NumberFormatException w) {
printUsage();
return;
}
try {
// Otwarcie portu do serwera
Socket s = new Socket(host, port);
// Rozpoczęcie wątku wysyłającego wpisy z klawiatury do serwera
new ZarzadWpisKlaw(System.in, s).start();
// Teraz wyświetlenie wszystkich informacji otrzymanych z serwera
BufferedReader in =
new BufferedReader(new InputStreamReader(s.getInputStream()));
String linia;
while ((linia = in.readLine()) != null) {
System.out.println(linia);
}
}
catch (Exception w) {
w.printStackTrace();
}
}
}
class ZarzadWpisKlaw extends Thread {
InputStream in;
Socket s;
public ZarzadWpisKlaw(InputStream in, Socket s) {
this.in = in;
this.s = s;
setPriority(MIN_PRIORITY); // Odczyty z portu powinny posiadać wyższy priorytet
// Niestety nie można użyć select() !
setDaemon(true); // Pozwolenie na śmierć aplikacji nawet jeżeli ten wątek pracuje
}
public void run() {
try {
BufferedReader klaw = new BufferedReader(new InputStreamReader(in));
PrintWriter serwer = new PrintWriter(s.getOutputStream());
String linia;
System.out.println("Połączono… Proszę wprowadzić żądanie HTTP");
System.out.println("------------------------------------------");
while ((linia = klaw.readLine()) != null) {
server.print(linia);
server.print("\r\n"); // linia HTTP kończy się \r\n
server.flush();
}
}
catch (Exception w) {
w.printStackTrace();
}
}
}
Powyższy program KlientHttp działa podobnie do telnet:
% java KlientHttp localhost 8080
Połączono… Proszę wprowadzić żądanie HTTP
------------------------------------------
GET /index.html HTTP/1.0
HTTP /1.0 200 OK.
Content-Type: text/html
Content-Length: 2582
Last-Modified: Fri, 15 Sep 2000 22:20:15 GMT
Servlet-Engine: Tomcat Web Server/3.2 (JSP 1.1; Servlet 2.2; Java 1.2.2;
Windows NT 4.0 x86; java.vendor=Sun Microsystems Inc.)
<!doctype html public "-//w3c//dtd html 4.0 transitional//en">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html"; charset=iso-8859-1">
<title>Tomcat v3.2</title>
</head>
...
Wykorzystanie niezależnego narzędzia
Niezależne narzędzia dają nowe możliwości i łatwość wykorzystania zadaniu usuwania błędów. IBM AlphaWorks produkuje program o nazwie Distributed Application Tester (DAT), który przegląda żądania HTTP i HTTPS, umożliwiając przeglądanie i zapisywanie obu stron ruchu klient-serwer. DAT zawiera możliwość wykonywania testów funkcjonalności wydajności aplikacji WWW poprzez automatyczne generowanie żądań i przeglądanie odpowiedzi. Program jest utworzony całkowicie w Javie, ale jest udostępniany z programem instalacyjnym działającym jedynie pod Windows. Jego jedyną licencją jest bezpłatny 90-dniowy czas testowania, ponieważ oprogramowanie to występuje w wersji „alpha”, a co dziwne w wersji tej jest od stycznia 199. DAT jest dostępny pod adresem http://www.alphaworks.ibm.com.
Firma Allaire, twórca popularnej „wtyczki” serwletów Jrun (po wykupieniu Live Software), posiada mało znane narzędzie służące do usuwania błędów z serwletów o nazwie ServletDebugger. Narzędzie to jest zaprojektowane programowego wspomagania testów i usuwania błędów z serwletów. ServletDebugger nie wymaga wykorzystania serwera WWW lub przeglądarki do wykonania żądania. Zamiast tego, wykorzystuje się zbiór klas do stworzenia niewielkiej klasy-końcówki, która przygotowuje i wykonuje żądanie serwletu. Końcówka określa wszystko — parametry inicjacji serwletu, nagłówki HTTP żądania oraz parametry żądania. ServletDebugger jest stosunkowo prosty i dobrze przystosowany do zautomatyzowanego testowania. Jego największą wadą jest konieczność wykonania sporej ilości pracy w celu właściwego przygotowania realistycznego żądania. ServletDebugger można odnaleźć w cenniku Allaire pod adresem http://www.allaire.com.
Ostatnie wskazówki
Jeżeli wszystkie powyższe porady nie pomogły w odnalezieniu i usunięciu błędu, proszę obejrzeć poniższe ostateczne wskazówki dotyczące suwania błędów z serwletów:
Proszę wykorzystać System.getProperty("java.class.path") przy pomocy serwletu w celu uzyskania pomocy w rozwiązywaniu problemów związanych ze ścieżką klas. Ponieważ serwlety są często uruchamiane na serwerach WWW z osadzonymi wirtualnymi maszynami Javy, trudne może być dokładne określenie ścieżki klas poszukiwanej przez JVM. Określić to może właściwość java.class.path.
Proszę pamiętać, że klasy odnalezione w bezpośredniej ścieżce klas serwera (katalog_macierzysty/classes) przypuszczalnie nie są przeładowywane podobnie jak, w większości serwerów niezwiązanych z serwletami, klasy wspierające w katalogu klas aplikacji WWW (WEB-INF/classes). Zazwyczaj przeładowywane są jedynie klasy serwletów w katalogu klas aplikacji WWW.
Należy zażądać od przeglądarki pokazania surowej zawartości wyświetlanej strony. Może to pomóc w zidentyfikowaniu problemów z formatowaniem. Zazwyczaj jest to opcja w menu Widok.
Należy upewnić się, że przeglądarka nie przechowuje w pamięci podręcznej wyników poprzedniego żądania poprzez wymuszenie pełnego przeładowania strony. W przeglądarce Netscape Navigator, należy zastosować Shift-Reload; W Internet Explorer należy zastosować Shift-Refresh.
Jeżeli pomija się wersję init(), która pobiera ServletConfig należy upewnić się, że pomijająca metoda od razu wywołuje super.init(config).
Poprawa wydajności
Serwlety poprawiające wydajność wymagają nieco innego podejścia niż aplikacje lub aplety Javy wykonujące to samo działanie. Powodem tego jest fakt, że JVM uruchamiająca serwlety zazwyczaj obsługuje kilkadziesiąt, jeżeli nie kilkaset wątków, z których każdy uruchamia serwlet. Te współistniejące serwlety muszą dzielić się zasobami JVM w sposób inny, niż zwykłe aplikacje. Tradycyjne sztuczki poprawiające wydajność oczywiście działają, ale posiadają mniejszy wpływ, kiedy zostają wykorzystane w systemie wielowątkowym. Poniżej przedstawiono niektóre sztuczki najczęściej wykorzystywane przez programistów serwletów.
Tworzyć, ale nie przesadzać
Należy unikać niepotrzebnego tworzenia obiektów. To zawsze była dobra rada — tworzenie niepotrzebnych obiektów marnuje pamięć i dużą ilość czasu. W przypadku serwletów to jest rada jeszcze lepsza. Tradycyjnie wiele JVM wykorzystywało globalną stertę obiektów, która musi zostać przypisana do każdej nowej alokacji pamięci. Kiedy serwlet tworzy nowy obiekt lub alokuje dodatkową pamięć, działania tego nie może wykonać żaden inny serwlet.
Nie łączyć
Należy unikać łączenia kilku łańcuchów. Zamiast StringBuffer poleca się zastosowanie metody append(). To również zawsze była dobra rada, ale w przypadku serwletów szczególnie pociągające jest napisanie kodu przygotowującego łańcuch do późniejszego wyświetlania w następujący sposób:
String wyswietl;
wyswietl += "<TITLE>";
wyswietl += "Witaj, " + uzyt;
wyswietl += "</TITLE>";
Chociaż powyższy kod wygląda miło i sprawnie, w trakcie uruchomienia działa, jak gdyby wyglądał mnie więcej tak jak poniżej, z nowymi StringBuffer i String tworzonymi w każdej linii:
String wyswietl;
wyswietl = new StringBuffer().append("<TITLE>").toString();;
wyswietl = new StringBuffer(wyswietl).append("Witaj, ").toString();
wyswietl = new StringBuffer(wyswietl).append(uzyt).toString();
wyswietl = new StringBuffer(wyswietl).append("</TITLE>").toString();
Kiedy wydajność jest ważną kwestią, należy przepisać oryginalny kod tak, aby wyglądał jak poniższy, tak aby utworzone zostały tylko pojedyncze StringBuffer i String:
StringBuffer buf = new StrngBuffer();
buf.append("<TITLE>");
buf.append("Witaj, "").append(uzyt);
buf.append("</TITLE>");
wyswietl = buf.toString();
Proszę zauważyć, że jeszcze bardziej wydajne jest zastosowanie tablicy bajtów.
Ograniczać synchronizację
Należy synchronizować bloki, kiedy jest to konieczne, ale nic poza tym. Każdy zsynchronizowany blok w serwlecie wydłuża czas odpowiedzi serwletu. Ponieważ ten sam egzemplarz serwletu może obsługiwać wiele współbieżnych żądań, musi, oczywiście, zająć się ochroną swojej klasy i zmiennych egzemplarza przy pomocy zsynchronizowanych bloków. Jednak przez cały czas jeden wątek żądania znajduje się w zsynchronizowanym bloku i żaden inny wątek nie może zostać do bloku wprowadzony. W związku z tym najlepiej jest uczynić te bloki jak najmniejszymi.
Należy także przyjrzeć się najgorszemu z możliwych wyników sporu między wątkami. Jeżeli najgorszy przypadek jest znośny (jak w przykładzie licznika w rozdziale 3, „Okres trwałości serwletów”), można rozważyc całkowite usuniecie bloków synchronizacji. Można również rozważyć zastosowanie interfejsu znaczników SingleThreadModel, w którym serwer zarządza pulą egzemplarzy serwletów, aby zagwarantować, że każdy egzemplarz jest wykorzystywany przez co najwyżej jeden wątek w jednym czasie. Serwlety będące implementacją SingleThreadModel nie muszą synchronizować dostępu do swoich zmiennych egzemplarza.
W końcu należy również pamiętać, że java.util.Vector i java.util.Hashtable są zawsze wewnętrznie zsynchronizowane, podczas gdy równoważne im java.util.ArrayList i java.util.HashMap, wprowadzone w JDK 1.2, nie są zsynchronizowane, jeżeli nie nastąpi odpowiednie żądanie. Tak wiec jeżeli dany Vector lub Hashtable nie potrzebuje synchronizacji, można go zastąpić ArrayList lub HashMap.
Buforować dane wprowadzane i wyświetlane
Należy buforować dane wprowadzane i wyświetlane, wszystkie pliki magazynowe, wszystkie potoki pobrane z bazy danych itd. To prawie zawsze podnosi wydajność, ale poprawa ta może być szczególnie widoczna w przypadku serwletów. Jej powód jest taki, że odczyt i zapis jednego elementu za jednym razem może spowolnić cały serwer w związku z koniecznością dokonywania częstych przełączeń kontekstu. Na szczęście ogólnie buforowanie podczas zapisu do PrintWriter lub ServletOutputStream lub podczas odczytywania z BufferedReader lub ServletInputStream nie jest konieczne. Większość implementacji serwerów sama buforuje te potoki.
Spróbować wykorzystania OutputStream
W przypadku stron WWW wykorzystujących kodowanie znaków Latin-1, technicznie możliwe jest wykorzystanie PrintWriter lub ServletOutputStream. Rekomendowanym podejściem jest wykorzystanie PrintWriter, ponieważ obsługuje on internacjonalizację, ale na niektórych serwerach wykorzystanie ServletOutputStream powoduje zauważalny wzrost wydajności, a ServletOutputStream posiada także ułatwiające pracę metody print() i println() będące dziedzictwem Servlet API 1.0, w którym nie występowała opcja PrintWriter. Proszę jednak uważać. W przypadku wielu serwerów zależność jest odwrotna i to PrintWriter powoduje wyższą wydajność. Osoby, które nie są pewne swojej platformy programistycznej i nie przeprowadzały porównywalnych prób czasowych powinny trzymać się PrintWriter.
Wykorzystać narzędzie profilujące
Dostępnych jest kilka narzędzi profilujących Javy które mogą wspomóc w odnalezieniu wąskich gardeł w kodzie. Większość problemów z wydajnością w Javie działającej po stronie serwera jest powodowana nie przez język czy JVM, ale kilka wąskich gardeł. Sztuką jest odnalezienie tych wąskich gardeł. Narzędzia analizujące pracują w tle, obserwując obsługę żądań przez serwer i zwracając dokładne podsumowanie spędzonego czasu, a także opis alokacji pamięci. Dwa popularne narzędzia to OptimizeIT! Firmy Intuitive Systems (http://www.optimizeit.com) i Jprobe formy Sitraka, dawniej KL Group (http://sitraka.com/jprobe). Duża ilość JVM może przyjmować również znaczniki z linii poleceń (-prof w JDK 1.1 i Xrunhproof() w JDK 1.2). Aby uruchomić obciążony serwer, można wykorzystać narzędzie takie jak Apache JMeter (http://java.apache.org).
Bez dodatkowej logiki nie przedstawionej w tym miejscu (między innymi dokononywania wstępnego połączenia z serwerem), metoda ta może zawieść w przypadku URL-i takich, jak http://www.jdom.org/news, które przekierowuje na http://www.jdom.org/news/. Aby się zabezpieczyć, należy uwidocznić każdy wiodący ukośnik w UTL-u przekazanym serwletowi.
Oczywiście jeżeli sprawdzenia dokonuje osoba nie znająca struktury HTTP, to ona może się pomylić, W tym przypadku warto jest przeczytać wprowadzenie do HTTP w rozdziale 2, „Podstawy serwletów HTTP” oraz książkę „HTTP Pocket Reference” autorstwa Clintona Wonga (O'Reilly).
Nie byłoby zaskakujące, jeżeli w najbliższym czasie firma Allaire zrzuciłaby obsługę ServletDebugger. Jeżeli się to zdarzy, a nawet jeżeli się nie wydarzy, można poszukać wersji Open Source.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\0-praca\Java Servlet - programowanie. Wyd. 2\r19-t.doc