Класс GenericServlet

Абстрактный класс GenericServlet реализует сразу интерфейсы Servlet и ServletConfig. Кроме реализации методов обоих интерфейсов в него введен пустой метод init () без аргументов. Этот метод выполняется автоматически после метода init(ServletConfig), точнее говоря, последний метод реализован так:

public void init(ServletConfig config) throws ServletException{ this.config = config; log("init"); this.init();

}

Поэтому удобно всю инициализацию записывать в метод init () без аргументов, не заботясь о вызове super. init (config).

Класс GenericServlet оставляет нереализованным только метод service ( ). Удобно создавать сервлеты, расширяя этот класс и переопределяя только метод service ().

Так и сделано в листинге 26.2. Можно записать его проще, не определяя метод init(), а прямо используя реализацию методов интерфейса ServletConfig, сделанную в классе GenericServlet. Этот вариант приведен в листинге 26.3. В него добавлено еще получение контекста сервлета.

Листинг 26.3. Упрощенное чтение начальных параметров сервлета

package myservlets; import java.io.*;

import java.util.*;import javax.servlet.*; public class InfoServlet extends GenericServlet{

@Override

public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException{

ServletContext cont = getServletContext();

resp.setContentType("text/html; charset=windows-1251");

PrintWriter pw = resp.getWriter();

pw.println("<html><head>");

pw.println("<title>Параметры сервлета</^^е>"); pw.println(,,</head><body><h2>Сведения о сервлете<^2>"); pw.println("HiMq сервлета — " + getServletName() + "<br>");

pw.println(,,Параметры и контекст сервлета: <br>");

Enumeration names = getInitParameterNames();

while (names.hasMoreElements()){

String name = (String)names.nextElement(); pw.print(name + ": ");

pw.println(getInitParameter(name) + "<br>");

}

pw.println("Сервер: " + cont.getServerInfo() +"<br>");

pw.println("</body></html>") ; pw.flush(); pw.close();

}

}

Работа по протоколу HTTP

Большинство запросов к сервлетам происходит по протоколу HTTP, который в настоящее время реализуется по рекомендации RFC 2616. Для удобства работы с этим протоколом интерфейсы ServletRequest и ServletResponse расширены интерфейсами HttpServletRequest и HttpServletResponse соответственно. При расширении интерфейсов в них добавлены методы, характерные для протокола HTTP.

Интерфейс HttpServletRequest

Несколько методов интерфейса HttpServletRequest позволяют разобрать HTTP-запрос.

Первая строка запроса, выполненного по протоколу HTTP, состоит из метода передачи данных, адреса URI и версии протокола. Строка завершается символами CRLF. Элементы строки разделяются пробелами, поэтому внутри каждого элемента первой строки запроса пробелов быть не должно.

Первая строка запроса выглядит примерно так:

GET http://some.firm.com/MyWebAppl/servlet/MyServlet?age=27 HTTP/1.1

Запрос HTTP начинается с одного из слов get, post, head или другого слова, обозначающего метод передачи данных. Узнать HTTP-метод передачи позволяет метод интерфейса

public String getMethod();

Далее в запросе, после пробела, идет адрес URI, который разбирается несколькими методами:

? public string getRequestURL() — возвращает адрес URL от названия схемы http до вопросительного знака;

? public string getservletPath() — возвращает часть этого адреса, показывающую путь к сервлету.

Часть пути, определяющая контекст сервлета, возвращается методом

public String getContextPath();

Часть URI после вопросительного знака возвращается методом

public String getQueryString();

После имени сервлета может идти дополнительный путь к какому-нибудь файлу, который можно получить методом

public String getPathInfo();

Этот же путь, дополненный до абсолютного пути к каталогу документов сервера, можно получить методом

public String getPathTranslated();

После первой строки запроса могут идти заголовки запроса: Accept, Accept-Charset, Accept-Language, User-Agent и прочие заголовки, описанные в рекомендации RFC 2616. Узнать заголовки запроса и их значения можно методами

public Enumeration getHeaderNames(); public Enumeration getHeaders(String name); public String getHeader(String name); public int getIntHeader(String name); public long getDateHeader(String name);

Наконец, можно получить cookies, хранящиеся в браузере клиента, в виде массива объектов класса Cookie методом

public Cookie[] getCookies();

В пакете javax.servlet.http есть прямая реализация интерфейса HttpServletRequest — класс HttpServletRequestWrapper, расширяющий класс ServletRequestWrapper. Объект этого класса создается конструктором

public HttpServletRequestWrapper(HttpServletRequest req);

и обладает всеми методами интерфейса HttpServletRequest. Разработчики, желающие расширить возможности объекта, содержащего HTTP-запрос, например для написания фильтра, могут расширить класс HttpServletRequestWrapper.

Интерфейс HttpServletResponse

При составлении ответа по протоколу HTTP можно использовать дополнительные методы, включенные в интерфейс HttpServletResponse.

Метод

public void setHeader(String name, String value);

устанавливает заголовок ответа с именем name и значением value. Старое значение, если оно существовало, при этом стирается.

Если надо дать несколько значений заголовку с именем name, то следует воспользоваться методом

public void addHeader(String name, String value);

Для заголовков с целочисленными значениями то же самое делается методами

public void setIntHeader(String name, int value); public void addIntHeader(String name, int value);

Заголовок с датой записывается методами

public void setDateHeader(String name, long date); public void addIntHeader(String name, int value);

Код ответа (status code) устанавливается методом

public void setStatus(int sc);

Как и все заголовки, этот метод записывается перед получением потока класса PrintWriter. Аргумент метода sc — это одна из множества констант, например константа sc_ok, соответствующая коду ответа 200 — успешная обработка запроса, sc_bad_request — код ответа 400 и т. д. Около сорока таких статических констант приведено в документации к интерфейсу HttpServletRequest. Метод setstatus () применяется для сообщений об успешной обработке с кодами 200—299.

Сообщения об ошибке посылаются методом

public void sendError(int sc, String message);

Обычно код ответа заносится в сообщение об ошибке message. Если надо послать стандартное сообщение об ошибке, например "404 Not Found", то применяется второй метод:

public void sendError(int sc);

Этот метод использован в листинге 26.6.

Сообщение методом sendError () посылается вместо результатов обработки запроса. Если попытаться послать после него ответ, то система выбросит исключение класса

IllegalStateException.

Наконец, к запросу можно добавить cookie методом

public void addCookie(Cookie cookie);

В пакете javax.servlet.http есть прямая реализация интерфейса HttpServletResponse — класс HttpServletResponseWrapper, расширяющий класс ServletResponseWrapper. Объект этого класса создается конструктором

public HttpServletResponseWrapper(HttpServletResponse resp);

и обладает всеми методами интерфейса HttpServletResponse. Разработчики, желающие расширить возможности объекта, содержащего запрос, могут расширить класс

HttpServletResponseWrapper.

Класс HttpServlet

Для использования особенностей протокола HTTP класс GenericServlet расширен абстрактным классом HttpServlet. Главная особенность этого класса заключается в том, что, расширяя его, не надо переопределять метод service (). Он уже определен, причем реализован так, что служит диспетчером, вызывающим методы doGet(), doPost() и другие методы, обрабатывающие HTTP-запросы с конкретными методами передачи данных GET, POST и др.

Вначале метод

public void service(ServletRequest req, ServletResponse resp);

анализирует типы аргументов req и resp. Эти типы должны быть на самом деле HttpServletRequest и HttpServletResponse. Если это не так, то метод выбрасывает исключение класса servletException и завершается.

Если аргументы req и resp подходящего типа, то методом getMethod () определяется HTTP-метод передачи данных и вызывается метод, соответствующий этому HTTP-методу, а именно один из методов

protected void doXxx(HttpServletRequest req, HttpServletResponse resp);

где Xxx означает Get, Post, Head, Delete, Options, Put или Trace.

Вот эти-то методы и надо переопределять, расширяя класс HttpServlet. Методы doHead (), doOptions () и doTrace () уже реализованы в соответствии с рекомендацией RFC 2616, их редко приходится переопределять. Остальные методы "реализованы" таким образом, что просто посылают сообщение о том, что они не реализованы. Чаще всего приходится переопределять методы doGet ( ) и doPost ( ).

Аннотации сервлета

Всю конфигурацию сервлета типа HttpServlet, которая была описана в предыдущих пунктах и которая записывается обычно в конфигурационный файл web.xml, тожно сделать с помощью аннотаций прямо в коде сервлета. В таком случае конфигурационный файл web.xml становится необязательным. Если же файл web.xml присутствует, то записанные в нем значения будут перекрывать значения, указанные в аннотации. Это удобно в тех случаях, когда надо изменить конфигурацию сервлета без его перекомпиляции.

Аннотации находятся в пакете javax.servlet.annotation, который надо указать в операторе import. Вот как они выглядят:

import javax.servlet.*;

import j avax.servlet.annotation.*;

@WebServlet(name="informer", urlPatterns={"/InfoServlet"}, initParams={

@WebInitParam(name="unit", value="1"), @WebInitParam(name="invoke", value="yes")

})

public class InfoServlet extends HttpServlet{

// Код сервлета .. .

}

Аннотация @WebServlet соответствует элементу <servlet> конфигурационного файла web.xml. Если в аннотации нет параметра name, то имя сервлета будет совпадать с полным именем класса сервлета, включая его пакет. Параметр urlPatterns можно заменить параметром value. Нельзя записывать оба эти параметра в одной аннотации, хотя один из них обязательно должен присутствовать.

Аннотация @WebInitParam соответствует элементу <init-param>. Она содержит имя и значение начального параметра. Совокупность начальных параметров записывается в аннотации @WebServlet параметром initParams, в фигурных скобках.

Пример сервлета класса HttpServlet

Приведем пример сервлета, осуществляющего регистрацию клиента Web-приложения- некоторой системы дистанционного обучения (СДО). Сервлет RegPrepServlet

принимает запрос от HTML-формы, соединяется с базой данных, заносит в нее полученную информацию и отправляет клиенту подтверждение регистрации в виде страницы HTML, содержащей форму для выбора учебного курса. В листинге 26.4 приведена HTML-форма регистрации клиента. Ее вид показан на рис. 26.2.

Листинг 26.4. Форма регистрации клиента СДО

<html><head>

<^^е>Регистрация</title>

<META http-equiv=Content-Type

content="text/html; charset=windows-1251">

</head>

<body><h2 align="center">Дистанционная система обучения Qn,0</h2> <р>Для регистрации занесите сведения о себе в следующие поля:</р>

<br>

<form method="POST" action=

"http://some.firm.com:8000/WebAppl/servlet/RegPrepServlet">

<pre>

Фамилия: <input type="text" size="40" name="surname">

Имя: <input type="text" size="40" name="name">

Отчество: <input type="text" size="40" name="secname">

E-mail: <input type="text" si ze="4 0" name="addr">

<input type="submit" value="Зарегистрировать">

</pre>

</form>

</body>

</html>

Рис. 26.2. Страница регистрации

Рис. 26.3. Страница подтверждения регистрации

Форма посылает сервлету RegPrepServlet четыре параметра: surname, name, secname и addr по HTTP-методу post. Сервлет должен принять их, обработать и послать клиенту результаты запроса или замечания по регистрации. Код сервлета приведен в листинге 26.5. Страница подтверждения регистрации показана на рис. 26.3.

Листинг 26.5. Сервлет регистрации клиента СДО

package myservlets;

import java.io.*; import java.sql.*;

import java.util.*;import javax.servlet.http.*; import j avax.servlet.annotation.*;

@WebServlet()public class RegPrepServlet extends HttpServlet{

private String driver = "oracle.jdbc.driver.OracleDriver", url = "jdbc:oracle:thin:@homexp:1521:SDO",

user = "sdoadmin", password = "sdoadmin";

private Connection con; private PreparedStatement pst;

@Override

public void init(){

try{

Class.forName(driver); con = DriverManager.getConnection( url, user, password); pst = con.prepareStatement(

"INSERT INTO students (id, name, address) " + "VALUES(reg_seq.NEXTVAL, ?, ?)"); }catch(Exception e){

System.err.println("From init(): " + e);

}

}

@Override

public void doGet(HttpServletRequest req, HttpServletResponse resp){ doPost(req, resp); }

@Override

public void doPost(HttpServletRequest req, HttpServletResponse resp){ try{

req.setCharacterEncoding("Cp1251");

String surname = req.getParameter("surname");

String name = req.getParameter("name");

String secname = req.getParameter("secname");

String addr = req.getParameter("addr");

resp.setContentType("text/html; charset=windows-1251");

PrintWriter pw = resp.getWriter();

if (surname.length() * name.length() * secname.length() * addr.length() == 0){

pw.println("<html><head>");

pw.println("<title>Продолжение регистрации</^^е>"); pw.println("</head><body><h2 align=center>" +

"Дистанционная система обучения СДО<^2>"); pw.println ("^3>Замечание:</h3>" );

pw.println("Заполните, пожалуйста, все поля.<Ьг>");

pw.println("</body></html>");

pw.flush();

pw.close();

return;

}

String fullname = surname.trim() + " " + name.trim() + " " + secname.trim(); pst.setString(1, fullname); pst.setString(2, addr); int count = pst.executeUpdate();

Statement st = con.createStatement();

ResultSet rs = st.executeQuery(

"SELECT id, name FROM students ORDER BY id DESC");

rs.next();

int id = rs.getInt(1); fullname = rs.getString(2);

rs.close();

StringTokenizer sttok = new StringTokenizer(fullname); sttok.nextToken(); name = sttok.nextToken(); secname = sttok.nextToken();

pw.println("<html><head>");

pw.println ("<Ь^1е>Регистрация</title>" );

pw.println("</head><body><h2 align=center>" +

"Дистанционная система обучения СДО<^2>" );

pw.println('^o6po пожаловать, " + name + " " + secname + "!<br>");

pw.println("Bbi зарегистрированы в СДО.<Ьг>"); pw.println("Ваш регистрационный номер " + id + "<br>"); pw.println("Выбeритe учебный курс:<Ьг> ");

rs = st.executeQuery("SELECT course name FROM courses");

pw.println("<form method=post action=" +

""http://homexp:8000/InfoAppl/servlet/CoursesServlet">");

pw.println("<select size=5 name=courses>"); while (rs.next())

pw.println("<option>" + rs.getString(1));

pw.println("</option></form></body></html>"); pw.flush(); pw.close();

rs.close(); }catch(Exception e){

System.err.println(e);

}

}

@Override

public void destroy(){ pst.close(); con.close();

}

}

В листинге 26.5 все запросы пользуются одним и тем же соединением с базой данных. При большом количестве одновременных запросов это может снизить производительность системы и даже превысить допустимое число соединений. В таком случае при инициализации сервлета надо в методе init () создать пул соединений с тем, чтобы за-

просы брали соединения из этого пула и возвращали соединение в пул при своем завершении. Еще лучше создать этот пул средствами сервера приложений, а в методе init () только обращаться к этому пулу.

Следует заметить, что у системы управления базой данных Oracle, с которой соединяется сервлет RegPrepServlet, есть свой сервер приложений Oracle Application Server (OAS) с контейнером сервлетов Apache/JServ или Tomcat. Можно установить сервлет прямо в Oracle AS и использовать для соединения с базой серверный драйвер JDBC с именем kprb. Это резко повысит производительность сервлета.

Разумеется, сервлет может отправлять клиенту не только страницы HTML, но и изображения, тексты в разных форматах, например PDF, звуковые файлы, короче говоря, данные любых MIME-типов. В листинге 26.6 приведен пример сервлета, позволяющего клиенту просматривать изображения типа GIF и JPEG в каталоге, заданном начальным параметром сервлета.

Листинг 26.6. Сервлет, отправляющий изображения клиенту

package myservlets;

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

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

@WebServlet()

public class ImageServlet extends HttpServlet{

Vector imFiles = new Vector(); int curIndex;

@Override

public void init() throws ServletException{

File imDir = null;

String imDirName = getInitParameter("imagedir");

if (imDirName != null) imDir = new File(imDirName);

if ((imDir != null) && imDir.exists() && imDir.isDirectory()){ String[] files = imDir.list();

for (int i = 0; i < files.length; i++) if (files[i].endsWith(".jpg") || files[i].endsWith(".gif")){

File curFile = new File(imDir, files[i]); imFiles.addElement(curFile);

}

}else log("Cannot find image dir: " + imDirName);

@Override

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ int len = imFiles.size();

if (len > 0){

File curFile = (File)imFiles.elementAt(curIndex);

String fileName = curFile.getName();

ServletContext ctxt = getServletConfig().getServletContext();

String ctype = ctxt.getMimeType(fileName);

if (ctype == null) ctype = fileName.endsWith(".jpg") ?

"image/jpeg" : "image/gif";

resp.setContentType(ctype);

try{

BufferedInputStream bis = new BufferedInputStream( new FileInputStream(curFile));

Outputstream os = resp.getOutputStream();

int cur = 0;

while ((cur = bis.read()) != -1) os.write(cur);

os.close(); bis.close();

}catch(FileNotFoundException e){

resp.sendError(HttpServletResponse.SC NOT FOUND);

}catch(Exception e){

resp.sendError(HttpServletResponse.SC SERVICE UNAVAILABLE);

}

curIndex= (curIndex + 1) % len;

}else resp.sendError(HttpServletResponse.SC SERVICE UNAVAILABLE); } public long getLastModified(){

return System.currentTimeMillis();

}

}

Сеанс связи с сервлетом

Сервер HTTP не сохраняет информацию о клиенте, связавшемся с ним. Хотя TCP-соединение может сохраняться вплоть до его явного закрытия (persistent HTTP connection) и за это время можно передать несколько запросов и ответов, протокол HTTP не предполагает средств сохранения информации о клиенте.

Многие задачи, решаемые Web-приложениями, требуют знания сведений о клиенте. Например, клиент электронного магазина может сделать несколько заказов в течение дня или даже нескольких дней. Сервер должен знать, в чью "корзину" складывать заказанные покупки. Еще пример. В программе листинга 26.5 завязывается диалог. Клиент регистри-

руется и посылает сведения о себе на сервер СДО. Сервер предлагает клиенту подобрать себе учебный курс. Клиент выбирает курс и посылает его название серверу. Сервер должен занести имя курса в учетную карточку клиента, а для этого ему надо знать регистрационный номер клиента.

Для получения сведений о клиенте, приславшем запрос, приходится применять искусственные средства, не входящие в протокол HTTP. Наиболее часто используются три средства: cookie, параметр в строке URL и скрытое поле в HTML-форме.

Первое средство — cookie — действует так. Получив первый запрос, сервер составляет заголовок ответа Set-Cookie, в который заносит пару "имя = значение", обычно это идентификатор клиента, а также диапазон URL, для которого действует cookie, срок хранения этих сведений и другую информацию. Браузер, получив ответ с таким заголовком, создает небольшой, размером не более 4 Кбайт, cookie-файл с этими сведениями и сохраняет его у себя в каталоге. Посылая следующие запросы, браузер отыскивает у себя соответствующий cookie-файл и заносит в заголовок Cookie запроса пару "имя = значение". Сервер по этому заголовку "узнает" клиента.

В пакете javax.servlet.http есть класс Cookie, методы которого обеспечивают работу с cookie. Объект этого класса создается конструктором

public Cookie(String name, String value);

Методы setXxx () позволяют добавить в объект остальные сведения, а методы getXxx () прочитать их.

После создания и формирования объекта cookie этот объект устанавливается в заголовок ответа методом

public void addCookie(Cookie cookie); интерфейса HttpServletResponse.

Вот обычная последовательность действий по созданию cookie и отправке его клиенту:

String value = "" + id;

Cookie ck = new Cookie("studentid", value);

ck.setMaxAge(60*60*24*183); // Cookie будет существовать полгода

resp.addCookie(ck);

Прочитать cookie из запроса клиента можно методом

public Cookie[] getCookies(); интерфейса HttpServletRequest.

Вот обычная последовательность действий по чтению cookie:

int id = 0;

Cookie[] cks = req.getCookies();

if (cks != null)

for (int i = 0; i < cks.length; i++)

if (cks[i].getName().equals("studentid")){ id = Integer.parseInt(cks[i].getValue()); break;

Использовать cookie удобно, но беда в том, что многие клиенты запрещают запись cookie-файлов, справедливо полагая, что нельзя записывать что-либо на диск без ведома хозяина.

Второе средство — параметр в строке URL (rewriting URL) — просто записывает в первую строку запроса после имени ресурса идентификатор клиента, например:

GET /some.com/InfoAppl/index.html?jsessionid=12345678 HTTP/1.1

Это тоже хороший способ, но клиент может сам формировать строку запроса, например, просто набирая ее в поле адреса браузера и забывая об идентификаторе.

Третье средство — скрытое поле HTML-формы — это поле

<input type="hidden" name="studentid" value="12345678">

в которое можно записать идентификатор клиента. Для применения данного средства надо на каждой странице HTML создавать форму.

Итак, какого-то одного универсального средства, для того чтобы создать сеанс связи с Web-сервером, нет. В пакет javax.servlet.http внесены интерфейсы и классы для облегчения распознавания клиента. Они автоматически переключаются с одного средства на другое. Если запрещены cookies, то формируется идентификатор клиента в строке URL и т. д.

Основу средств создания сеанса связи с клиентом составляет интерфейс HttpSession. Объект типа HttpSession формируется контейнером сервлета при получении запроса, а получить или создать его можно методом

public HttpSession getSession(boolean create);

описанным в интерфейсе HttpServletRequest. Метод возвращает объект типа HttpSession, если он существует. Если же такой объект отсутствует, то поведение метода зависит от значения аргумента create — если он равен false, то метод возвращает null, если true — создает новый объект.

Второй метод того же интерфейса

public HttpSession getSession(); эквивалентен getSession(true).

Логическим методом

public boolean isNew();

интерфейса HttpSession можно узнать, новый ли это, только что созданный сеанс (true) или продолжающийся, уже запрошенный клиентом (false).

В сеансе отмечается время его создания и время последнего запроса, которые можно получить методами

public long getCreationTime(); public long getLastAccessedTime();

Они возвращают время в миллисекундах, прошедших с полуночи 1 января 1970 года (дата рождения UNIX).

Создавая сеанс, контейнер дает ему уникальный идентификатор. Метод

public String getId();

возвращает идентификатор сеанса в виде строки.

У сеанса могут быть атрибуты, в качестве которых способны выступать любые объекты Java. Атрибут задается методом

public void setAttribute(String name, Object value);

Получить имена и значения атрибутов можно методами

public Enumeration getAttributeNames(); public Object getAttribute(String name);

Атрибуты — удобное средство хранения объектов, которые должны существовать на протяжении сеанса.

Атрибут удаляется методом

public void removeAttribute(String name);

Контейнер следит за событиями, происходящими во время сеанса. Создание или удаление атрибута, изменение его значения- события класса HttpSessionBindingEvent. Этот

класс является подклассом класса HttpSessionEvent, экземпляр которого создается при всяком изменении в активных сеансах Web-приложения — создании сеанса, прекращении сеанса, истечении срока ожидания запроса.

Сеанс завершается методом invalidate () или по истечении времени ожидания очередного запроса. Это время, в секундах, задается методом

public void setMaxInactiveInterval(int secs);

Сервлет может узнать назначенное время ожидания методом

public int getMaxInactiveInterval();

Фильтры

Суть работы сервлета заключается в обработке полученного из объекта типа ServletRequest запроса и формировании ответа в виде объекта типа ServletResponse. Попутно сервлет может проделать массу работы, создавая объекты, обращаясь к их методам, загружая файлы, соединяясь с базами данных. Эти действия усложняют изначально простую и четкую структуру сервлета. Чтобы придать стройность и упорядоченность сервлету, можно организовать цепочку фильтров — объектов, последовательно пропускающих через себя информацию, идущую от запроса к ответу, и преобразующих ее.

Удобство фильтров заключается еще и в том, что один фильтр может использоваться несколькими сервлетами и даже всеми ресурсами Web-приложения. Разработчик может подготовить набор фильтров на все случаи жизни и применять их в своих сервлетах.

При работе с фильтрами сразу создается цепочка фильтров, даже если в нее входит всего один фильтр. Вся работа с цепочками фильтров описывается интерфейсами Filter, FilterChain и FilterConfig. Интерфейс Filter реализуется разработчиком приложения, остальные интерфейсы должен реализовать контейнер сервлетов.

Каждый фильтр в цепочке — это объект типа Filter. Структура этого объекта напоминает структуру сервлета. Интерфейс Filter описывает три метода:

public void init(FilterConfig cong);

public void doFilter(ServletRequest req, ServletResponse resp,

FilterChain chain); public void destroy();

Как видно из этих описаний, фильтр может выполнить начальные действия методом init (), причем ему передается созданный контейнером объект типа FilterConfig, который очень похож на объект типа ServletConfig. Он также содержит начальные параметры, которые можно получить методами

public Enumeration getInitParameterNames(); public String getInitParameter(String name);

У него тоже есть имя, которое дается ему при установке фильтра в контейнер и записывается в конфигурационный файл web.xml в элемент <filter-name>. Имя класса-фильтра можно получить методом

public String getFilterName();

Наконец, он тоже возвращает ссылку на контекст методом

public ServletContext getServletContext(String name);

Вся фильтрация выполняется методом doFilter( ), который получает объекты req и resp, изменяет их и передает управление следующему фильтру в цепочке с помощью аргумента chain.

Организация цепочки достается на долю контейнера, интерфейс FilterChain описывает только один метод

public void doFilter(ServletRequest req, ServletResponse resp);

Чтобы передать управление фильтру, следующему в цепочке, фильтр должен просто обратиться к этому методу, передав ему измененные объекты req и resp.

Приведем пример фильтра. Русскоязычному программисту постоянно приходится думать о правильной кодировке кириллицы. Параметры запроса идут от браузера чаще всего в MIME-типе application/x-www-form-urlencoded, использующем байтовую кодировку, принятую по умолчанию на машине клиента. Эта кодировка должна указываться в заголовке Content-Type, например:

Content-Type: application/x-www-form-urlencoded;charset=windows-1251

Но, как правило, браузер не посылает этот заголовок Web-серверу. В таком случае встает задача определить кодировку параметров запроса и заслать ее в метод setCharacterEncoding(String), чтобы метод getParameter(String) правильно перевел значение параметра в Unicode. Эту задачу должен решать метод getCharacterEncoding(), но он чаще всего реализован так, что просто берет кодировку из заголовка Content-Type. Листинг 26.7 показывает схему такой реализации в одной из прежних версий контейнера сервлетов Tomcat. Оцените качество кодирования и посмотрите, почему, используя эту версию Tomcat, между словом "charset" и знаком равенства нельзя оставлять пробелы.

Кстати говоря, некоторые контейнеры сервлетов не воспринимают кодировку, если оставлены пробелы между точкой с запятой и словом "charset".

Листинг 26.7. Фильтр определения кодировки параметров запроса

import java.io.*;import javax.servlet.*; import j avax.servlet.annotation.*;

@WebFilter(

urlPatterns={"/*"}, servletNames={""}

initParams={ @WebInitParam(name="simpleParam", value="paramValue") }

)

public class SetCharEncFilter implements Filter{ protected String enc; protected FilterConfig fc;

@Override

public void init(FilterConfig conf) throws ServletException{ fc = conf;

enc = conf.getInitParameter("encoding");

}

@Override

public void doFilter(ServletRequest req,

ServletResponse resp,

FilterChain chain)

throws IOException, ServletException{

String encoding = selectEncoding(req); if (encoding != null) req.setCharacterEncoding(encoding);

chain.doFilter(req, resp);

}

protected String selectEncoding(ServletRequest req){ String charEncoding =

getCharsetFromContentType(req.getContentType()); return (charEncoding == null) ? enc : charEncoding;

}

// From org.apache.tomcat.util.RequestUtil.java

public static String getCharsetFromContentType(String type){ if (type == null) { return null;

}

int semi = type.indexOf(";");

if (semi == -1) { return null;

String afterSemi = type.substring(semi + 1); int charsetLocation = afterSemi.indexOf("charset="); if (charsetLocation == -1) { return null;

}

String afterCharset = afterSemi.substring(charsetLocation + 8);

String encoding = afterCharset.trim(); return encoding; }

@Override

public void destroy(){ enc = null; fc = null;

}

}

Фильтр класса SetCharEncFilter, описанный в листинге 26.7, очень прост. Он извлекает из запроса req заголовок Content-Type методом getContentType (). Если такой заголовок есть, то он пытается извлечь из него кодировку. Если это не удается, то берет кодировку из своего начального параметра "encoding". Затем он заносит кодировку в ответ resp методом setCharacterEncoding (String) и передает управление следующему фильтру.

Всякий разработчик, желающий улучшить определение кодировки, может переопределить метод selectEncoding (), извлекая информацию из других заголовков, например:

User-Agent, Accept-Language, Accept-Charset, Content-Language.

В более сложных случаях понадобится расширить объекты req и resp. Вот тут-то и пригодятся классы HttpServletRequestWrapper и HttpServletResponseWrapper. Дополнительные свойства запроса и ответа можно занести в расширения этих классов-оболочек и использовать их в фильтре по такой схеме.

public class MyRequestHandler extends HttpServletRequestWrapper{

public MyRequestHandler(HttpServletRequest req){ super(req);

// ...

}

// ...

}

public class MyResponseHandler extends HttpServletResponseWrapper{

public MyResponseHandler(HttpServletResponse resp){ super(resp);

// ...

}

// ...

}@WebFilter(urlPatterns={"/*"})

public class MyFilter implements Filter{

private MyRequestHandler mreq; private MyResponseHandler mresp;

@Override

public void init(FilterConfig conf){

// ...

}

@Override

public void doFilter(ServletRequest req,

ServletResponse resp,

FilterChain chain){

mreq = new MyRequestHandler((HttpServletRequest)req); mresp = new MyResponseHandler((HttpServletResponse)resp);

// Действия до перехода к следующему фильтру.

chain.doFilter(mreq, mresp);

// Действия после возврата из сервлета и фильтров.

}

@Override

public void destroy(){ mreq = null; mresp = null;

}

}

После того как класс-фильтр написан и скомпилирован, его надо установить в контейнер и приписать (map) к одному или нескольким сервлетам. Это выполняется утилитой установки или средствами IDE, в которых указывается имя фильтра, его начальные параметры и сервлет, к которому приписывается фильтр. Утилита установки заносит сведения о фильтре в конфигурационный файл web.xml в элемент <filter>. Это можно сделать и вручную. Приписка фильтра к сервлету отмечается внутри элемента <filtermapping> парой вложенных элементов <filter-name> и <servlet-name>. Например:

<filter-mapping>

<filter-name>MyFilter</filter-name>

<servlet-name>RegServlet</servlet-name>

</filter-mapping>

Фильтр можно приписать не только сервлетам, но и другим ресурсам. Для этого записывается элемент <url-pattern>, например после

<filter-mapping>

<filter-name>MyFilter</filter-name>

<url-pattern>*.html</url-pattern>

</filter-mapping>

фильтр будет применен ко всем вызовам документов HTML.

Порядок фильтров в цепочке соответствует порядку элементов <filter-mapping> в конфигурационном файле web.xml. При обращении клиента к сервлету контейнер сначала отыскивает фильтры и последовательно выполняет их, а уж потом запускает сервлет. После работы сервлета его ответ проходит цепочку фильтров в обратном порядке.

Обращение к другим ресурсам

В некоторых случаях недостаточно вставить в сервлет фильтр или даже цепочку фильтров, а надо обратиться к другому сервлету, странице JSP, документу HTML, XML или иному ресурсу. Если требуемый ресурс находится в том же контексте, что и сервлет, который его вызывает, то для получения ресурса следует обратиться к методу

public RequestDispatcher getRequestDispatcher(String path);

описанному в интерфейсе ServletRequest. Здесь path — это путь к ресурсу относительно контекста. Например:

RequestDispatcher rd = req.getRequestDispatcher("CourseServlet");

Если же ресурс находится в другом контексте, то нужно сначала получить контекст методом

public ServletContext getContext(String uripath);

интерфейса ServletContext, а потом воспользоваться методом

public RequestDispatcher getRequestDispatcher(String path);

интерфейса ServletContext. Здесь путь path должен быть абсолютным, т. е. начинаться с наклонной черты /. Например:

RequestDispatcher rd = conf.getServletContext(). getContext("/product").

getRequestDispatcher("/product/servlet/CourseServlet");

Если требуемый ресурс — сервлет, помещенный в контекст под своим именем, то для его получения можно обратиться к методу

public RequestDispatcher getNamedDispatcher(String name);

интерфейса ServletContext.

Все три метода возвращают null, если ресурс недоступен или сервер не реализует интерфейс RequestDispatcher.

Как видно из описания методов, к ресурсу можно обратиться только через объект типа RequestDispatcher. Этот объект предлагает два метода обращения к ресурсу.

Первый метод,

public void forward(ServletRequest req, ServletResponse resp);

просто передает управление другому ресурсу, предоставив ему свои аргументы req и resp. Вызывающий сервлет выполняет предварительную обработку объектов req и resp и передает их вызванному сервлету или другому ресурсу, который окончательно формирует ответ resp и отправляет его клиенту или опять-таки вызывает другой ресурс. Например:

if (rd != null) rd.forward(req, resp);

else resp.sendError(HttpServletResponse.SC NO CONTENT);

Вызывающий сервлет не должен выполнять какую-либо отправку клиенту до обращения к методу forward ( ), иначе будет выброшено исключение класса IllegalStateException.

Если же вызывающий сервлет уже что-то отправлял клиенту, то следует обратиться ко второму методу,

public void include(ServletRequest req, ServletResponse resp);

Этот метод вызывает ресурс, который на основании объекта req может изменить тело объекта resp. Но вызванный ресурс не может изменить заголовки и код ответа объекта resp. Это естественное ограничение, поскольку вызывающий сервлет мог уже отправить заголовки клиенту. Попытка вызванного ресурса изменить заголовок будет просто проигнорирована контейнером. Можно сказать, что метод include () выполняет такую же работу, как вставки на стороне сервера SSI (Server Side Include).

После выполнения метода include () управление возвращается в сервлет.

Асинхронное выполнение запросов

Если несколько запросов приходят одновременно, контейнер сервлетов создает подпроцессы, выполняющие метод service () для каждого запроса. Количество подпроцессов может оказаться слишком велико, а время их работы может затянуться, особенно если сервлет ожидает получение данных из базы данных или из файла или окончания длительной обработки информации. В этом случае необходимо обеспечить быстроту работы сервлета. Этого можно достигнуть, выполняя запросы асинхронно: сервлет принимает запрос, передает его обработку асинхронному подпроцессу и завершает работу, не дожидаясь ответа от этого подпроцесса. Асинхронный подпроцесс сам формирует ответ или передает управление другому сервлету в итоге выполнения своей работы.

Такая возможность предоставлена сервлетам и фильтрам, начиная с версии 3.0.

Сервлет или фильтр, выполняющий асинхронную работу, отмечается в конфигурационном файле web.xml элементом <async-supported> со значением true или в аннотациях

@WebServlet, @WebFilter параметром asyncSupported, например,

@WebServlet(value="/MyAsyncServlet", asyncSupported="true")

Возможность выполнения асинхронных действий можно проверить методом

isAsyncSupported().

Асинхронная обработка запроса начинается обращением к методу startAsync () объекта типа ServletRequest, в который передаются ссылки на запрос ServletRequest и ответ ServletResponse. Этот метод возвращает ссылку на объект типа AsyncContext, методы которого позволяют проследить за работой асинхронного метода. Проверить, что запрос обрабатывается асинхронно, можно логическим методом isAsyncStarted ( ).

После выполнения метода startAsync() обработка запроса будет завершена не по окончании работы метода service(), как обычно, а методом complete() объекта AsyncContext или по прошествии времени, заданного предварительно методом setTimeout (). Кроме того, можно создать объект типа AsyncListener и его методами отследить этапы асинхронной обработки.

Интерфейс AsyncListener описывает четыре метода: onStartAsync( ), onError(), onComplete () и onTimeout (). Реализовав эти методы, можно отреагировать на наступление соответствующих четырех событий.

Асинхронную работу естественно выполнять в отдельном подпроцессе.

В листинге 26.8 приведен типичный пример асинхронного сервлета. Обратиться к нему можно примерно так:

http://localhost:8080/MyAsyncApp/AsyncServlet?id=1

После набора этого адреса в адресной строке браузера, подождите 10 секунд, и вы увидите ответ сервлета.

Листинг 26.8. Асинхронный сервлет

package mypack;

import java.io.*;

import java.util.concurrent.*;

import javax.servlet.*;

import javax.servlet.annotation.*;

import javax.servlet.http.*;

import java.util.Date;

@WebServlet(urlPatterns = "/AsyncServlet", asyncSupported=true) public class AsyncServlet extends HttpServlet {

@Override

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (!request.isAsyncSupported()){ response.getWriter().println(

"Asynchronous processing is not supported"); return;

}

AsyncContext asyncCtx = request.startAsync(); asyncCtx.addListener(new MyAsyncListener());

asyncCtx.setTimeout(20000);

Executor executor = new ThreadPoolExecutor(10, 10, 50000L,

TimeUnit. MI LLI SECONDS,

new LinkedBlockingQueue<Runnable>(100)); executor.execute(new AsyncProcessor(asyncCtx));

}

}

class AsyncProcessor implements Runnable { private AsyncContext asyncContext;

public AsyncProcessor(AsyncContext asyncContext) { this.asyncContext = asyncContext;

@Override

public void run() {

String reqId = asyncContext.getRequest().getParameter("id"); if (null == reqId || reqId.length() == 0) reqId = "unknown"; Date before = new Date();

String result = longStandingProcess(reqId);

String resp = "Request id: " + reqId +

"<br/> Started at: " + before.toString() +

".<br/> Completed at: " + result + ".<br/>"; asyncContext.getResponse().setContentType("text/html");

try {

PrintWriter out = asyncContext.getResponse().getWriter(); out.println(resp);

} catch(Exception e) {

System.out.println(e.getMessage() + ": " + e);

}

asyncContext.complete();

}

public String longStandingProcess(String reqId) { try {

Thread.sleep(10000);

} catch (InterruptedException ie) {

System.out.println("Request: " + reqId +

", " + ie.getMessage() + ": " + ie);

}

return new Date().toString();

}

}

@WebListener

public class MyAsyncListener implements AsyncListener { public MyAsyncListener() { }

public void onComplete(AsyncEvent ae) {

System.out.println("AsyncListener: onComplete for request: " + ae . getAsyncContext () . getRequest () . getParameter ( "id" ) ) ;

}

public void onTimeout(AsyncEvent ae) {

System.out.println("AsyncListener: onTimeout for request: " + ae.getAsyncContext() .getRequest() .getParameter("id") ) ;

}

public void onError(AsyncEvent ae) {

System.out.println("AsyncListener: onError for request: " + ae.getAsyncContext().getRequest().getParameter("id"));

public void onStartAsync(AsyncEvent ae) {

System.out.println("AsyncListener: onStartAsync");

}

}

В этом простейшем примере длительный процесс — это просто задержка на 10 секунд в методе longStandingProcess (). Метод longStandingProcess () вызывается в рамках подпроцесса, выполняющего метод run() класса AsyncProcessor. После выполнения метода longStandingProcess () формируется ответ клиенту — строка resp — и отправляется в выходной поток, после чего асинхронное выполнение завершается методом complete (). Роль метода doGet () заключается только в запуске нового подпроцесса методом execute (), после чего метод doGet () завершается, не формируя никакого ответа.

В более сложной ситуации, когда запросы идут один за другим, создается очередь запросов, выполняемых асинхронно. Очередь освобождается по мере выполнения запросов. Такой пример, названный AsyncRequest, приведен в стандартной поставке Java EE 6 SDK.

Вопросы для самопроверки

1. Какая разница между HTTP-сервером и Web-сервером?

2. Что такое сервер приложений?

3. Что такое сервлет?

4. Что такое контейнер сервлетов?

5. Что означает процедура установки сервлета?

6. Может ли сервлет отправить клиенту не страницу HTML, а другой документ?

7. Может ли сервлет обрабатывать параллельно несколько запросов?

8. Могут ли сервлеты, установленные в один контейнер, обмениваться информацией?

9. Может ли сервлет установить сеанс связи с клиентом?

ГЛАВА 27