Java 7

Хабибуллин Ильдар Шаукатович

Сервлеты

 

Первоначально перед HTTP-серверами стояла простая задача: найти и отправить клиенту файл, указанный в полученном от клиента запросе. Запрос составлялся тоже очень просто по правилам протокола HTTP в специально придуманной форме URL.

Потом понадобилось сделать на сервере какую-либо небольшую предварительную обработку отправляемого файла. Появились включения на стороне сервера SSI (Server Side Include) и различные приемы динамической генерации страниц HTML. HTTP-сервер усложнился и стал называться Web-сервером.

Затем возникла необходимость выполнять на сервере процедуры. В запрос URL вставили возможность вызова процедур, а на сервере реализовали технологию CGI (Common Gateway Interface), о которой мы говорили в предыдущих главах. Теперь в запросе URL указывается процедура, которую надо выполнить на сервере, и записываются аргументы этой процедуры в виде пар "имя — значение", например:

Для составления таких запросов в язык HTML введен тег

.

Web-сервер, получив запрос, загружает и запускает CGI-процедуру (в предыдущем примере это процедура mycgiprog.pl), расположенную на сервере в каталоге cgi-bin, и передает ей значения "Ivanov" и "27" аргументов name и age. Процедура оформляет свой ответ в виде страницы HTML, которую Web-сервер отправляет клиенту.

Процедуру CGI можно написать на любом языке, лишь бы он воспринимал стандартный ввод и мог направить результат работы процедуры в стандартный вывод. Неожиданную популярность получил язык Perl. Оказалось, что на нем удобно писать CGI-программы. Возникли специальные языки: PHP, ASP, серверный вариант JavaScript.

Технология Java не могла пройти мимо такой насущной потребности и отозвалась на нее созданием сервлетов и языком JSP (JavaServer Pages).

Сервлеты (servlets) выполняются под управлением Web-сервера подобно тому, как апплеты выполняются под управлением браузера, откуда и произошло их название. Для слежения за работой сервлетов и управления ими создается специальный программный модуль, называемый контейнером сервлетов (servlet container). Слово "контейнер" в русском языке означает пассивную емкость стандартных размеров, но контейнер сервлетов активен, он загружает сервлеты, инициализирует их, передает им запросы клиентов, принимает ответы. Сервлеты не могут работать без контейнера, как апплеты не могут работать без браузера. Жаргонное выражение "сервлетный движок", происходящее от английского "servlet engine", лучше выражает суть дела, чем выражение "контейнер сервлетов".

Web-сервер, снабженный контейнером сервлетов и другими контейнерами, стал называться сервером приложений (application server, AS).

Чтобы сервлет мог работать, он должен быть зарегистрирован в контейнере, по терминологии спецификации "Java Servlet Specification" установлен (deploy) в него. Установка (deployment) сервлета в контейнер включает получение уникального имени и определение начальных параметров сервлета, запись их в конфигурационные файлы, создание каталогов для хранения всех файлов сервлета и другие операции. Процесс установки сильно зависит от контейнера. Одному контейнеру достаточно скопировать сервлет в определенный каталог, например autodeploy/ или webapps/, другому надо после этого перезапустить контейнер, для третьего надо воспользоваться утилитой установки. В стандартном контейнере Java EE SDK такая утилита называется deploytool.

Один контейнер может управлять работой нескольких установленных в него сервлетов. При этом один контейнер способен в одно и то же время работать в нескольких виртуальных машинах Java, образуя распределенное Web-приложение. Сами же виртуальные машины Java могут работать на одном компьютере или на разных компьютерах.

Контейнеры сервлетов создаются как часть Web-сервера или как встраиваемый в него модуль. Большую популярность получили встраиваемые контейнеры Tomcat, разработанные сообществом Apache Software Foundition в рамках проекта Jakarta, Resin фирмы Caucho, JRun фирмы Macromedia. Точное распределение обязанностей между Web-сервером и контейнером сервлетов выпадает на долю их производителей.

Сервер Tomcat можно скопировать со страницы и установить отдельно. Удобнее скопировать его дистрибутив в виде zip-файла, его надо просто развернуть в какой-либо каталог. Для выполнения всех примеров этой и следующей главы понадобится Tomcat версии не меньше 7, "умеющий" выполнять Servlet 3.0 и JSP 2.2.

Web-приложение

Как правило, сервлет не выполняется один. Он работает в составе Web-приложения. Web-приложение (web application) составляют все ресурсы, написанные для обслуживания запросов клиента: сервлеты, JSP, страницы HTML, документы XML, другие документы, изображения и чертежи, музыкальные и видеофайлы. Спецификация "Java Servlet Specification" описывает структуру каталогов, содержащих все эти ресурсы. Она изображена на рис. 26.1.

Как видно из рисунка, все, что относится к данному Web-приложению, содержится в одном каталоге, имя которого будет именем Web-приложения. В примере это каталог InfoAppl. В этом каталоге обязательно должен быть каталог WEB-INF и необязательно другие каталоги и файлы. Все, что находится в каталоге WEB-INF и его подкаталогах, недоступно клиенту. Это "внутренняя кухня" Web-приложения. То, что расположено в приложении вне каталога WEB-INF, доступно клиенту.

В каталоге WEB-INF должен быть конфигурационный XML-файл с именем web.xml, в котором описано Web-приложение: его ресурсы, их адреса и связи между ресурсами.

Это минимальный состав Web-приложения — каталог с его именем, в нем подкаталог WEB-INF, а в нем файл web.xml. Впрочем, при использовании аннотаций даже файл web.xml становится необязательным.

Скомпилированные сервлеты обычно располагаются в подкаталогах каталога WEB-INF/classes в соответствии со своей структурой пакетов и подпакетов.

InfoAppI

—WEB-INF-г

web.xml

classes

myservlets

— index.html index, htm index.jsp

sdotags—

lib-pjstl.jar

-standard .jar

—sdotaglib.tld — tags-info.tag

InfoServlet.class

RegPrepServlet.class

HeadTag.class QueryTag.class

'—images

logo.gif

header.jpg

Рис. 26.1. Структура каталогов Web-приложения

Все Web-приложение целиком часто упаковывается в один файл по технологии JAR. Такой файл обычно получает расширение war (Web ARchive). Этот файл можно переносить с одного Web-сервера на другой, при этом многие контейнеры сервлетов могут запускать Web-приложение прямо из архива, не распаковывая его. Серверу Tomcat достаточно занести WAR-архив или всю структуру каталогов приложения в каталог webapps. Сервер его "увидит" и немедленно, без перезапуска, вовлечет в работу. Необходимость распаковки архива или работа прямо с архивом указывается при настройке Tomcat в его конфигурационном файле server.xml.

Интерфейс Servlet

Как водится в технологии Java, понятие "сервлет" описывается интерфейсом Servlet. Рассмотрим его подробнее.

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

ServletConfig:

public void init(ServletConfig conf);

Объект, описанный интерфейсом ServletConfig, создается Web-приложением и передается контейнеру для инициализации сервлета. Информация, необходимая для создания объекта, содержится в конфигурационном файле Web-приложения.

Конфигурационный файл

Конфигурационный файл (deployment descriptor) описывает ресурсы, составляющие Web-приложение: сервлеты, их фильтры и слушатели, страницы JSP, документы HTML и XML, изображения и документы других типов. Он формируется при создании Web-приложения и заполняется при установке сервлета и других ресурсов в контейнер. Конфигурационный файл записывается на языке XML и называется web.xml. Он располагается в каталоге WEB-INF, одном из каталогов Web-приложения, и создается вручную, утилитой установки сервлета в контейнер или с помощью IDE, вроде NetBeans или Eclipse. Каждая фирма-производитель контейнера сервлетов предоставляет свою утилиту установки или make-файл, содержащий команды установки. Надо заметить, что вместо построителя make в технологии Java используются другие построители, написанные на языке Java: построитель ant, разработанный Apache Software Foundation в рамках проекта Jakarta, построитель Maven — еще одна разработка Apache Software Foundation, или Ivy — опять-таки разработка Apache.

Утилита установки контейнера Tomcat запускается из браузера, она расположена на его странице /manager/html. Для того чтобы запустить утилиту, в браузере надо набрать примерно такую строку: .

В листинге 26.1 показан фрагмент конфигурационного файла web.xml, созданного для контейнера Tomcat. С языком XML мы познакомимся в главе 28, а пока смотрите разъяснения элементов XML в комментариях.

Листинг 26.1. Конфигурационный файл web.xml

PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" " 2 3.dtd">

Если в запросе не указан ресурс, то вызывается сервлет по умолчанию. Ниже записано его имя "default" и полное имя класса сервлета.

-->

default

org.apache.tomcat.servlets.DefaultServlet

1

Если пришел запрос к сервлету, не описанному в данном файле, то вызывается сервлет с именем "invoker".

-->

invoker

org.apache.tomcat.servlets.InvokerServlet

debug

0

2

j sp

org.apache.j asper.runtime.JspServlet

j spCompilerPlugin

org.apache.j asper.compiler.JikesJavaCompiler

-->

-2147483646

invoker /servlet/*

j sp

*.j sp

30

txt

text/plain

html

text/html

htm

text/html

gif

image/gif

index.jsp index.html index.htm

Как видите, конфигурационный файл web.xml весьма объемен. В большом Web-приложении он становится сложным и трудно читаемым. Начиная с версии Servlet 3.0, его можно составить из нескольких файлов, содержащих отдельные фрагменты с описаниями отдельных сервлетов и других ресурсов. Каждый фрагмент, в отличие от основного файла web.xml, обрамляется XML-элементом , а не элементом . Файл с фрагментом должен называться web-fragmentxml и располагаться в каталоге META-INF. Порядок подключения фрагментов указывается элементами XML в каждом фрагменте или в файле web.xml.

Интерфейс ServletConfig

Каждый объект типа ServletConfig содержит имя сервлета, извлеченное из элемента конфигурационного файла, набор начальных параметров, взятых из элементов , и контекст сервлета в виде объекта типа ServletContext. Эти конфигурационные параметры сервлет может получить методами

public String getServletName(); public Enumeration getInitParameterNames(); public String getInitParameter(String name); public ServletContext getServletContext();

описанными в интерфейсе ServletConfig.

Начальные параметры записываются в конфигурационный файл web.xml во время установки сервлета вручную или с помощью утилиты установки. Механизм задания и чтения начальных параметров сервлета очень похож на механизм определения параметров апплета, записываемых в теги и читаемых методами getParameter() апплета.

В листинге 26.2 приведен простейший сервлет, отправляющий клиенту свое имя и начальные параметры.

Листинг 26.2. Начальные параметры сервлета

package myservlets; import java.io.*;

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

@Override

public void init(ServletConfig conf) throws ServletException{ super.init(conf); sc = conf; }

@Override

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

resp.setContentType("text/html; charset=utf-8");

PrintWriter pw = resp.getWriter();

pw.println("");

pw.println("^^^^араметры сервлета"); pw.println(,,

Сведения о сервлете<^2>"); pw.println(,,Имя сервлета — " + sc.getServletName() + "
");

pw.println(,,Параметры сервлета:
");

Enumeration names = sc.getInitParameterNames();

while (names.hasMoreElements()){

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

pw.println(sc.getInitParameter(name) + "
");

}

pw.println(""); pw.flush(); pw.close();

}

public void destroy(){ sc = null;

}

}

Полностью код листинга 26.2 будет подробно разъяснен позднее, а пока запомните два правила:

□ переопределяя метод init(ServletConfig), вызывайте метод init (ServletConfig) суперкласса;

□ кодировку ответа устанавливайте методом setContentType () перед получением ссылки на выходной поток методом getWriter (). Это относится и к другим заголовкам ответа.

Код листинга 26.2 компилируется обычным образом, а затем полученный файл с сервлетом InfoServlet. class устанавливается в контейнер. Процедура установки выполняется соответствующей утилитой, входящей в состав контейнера сервлетов или сервера приложений, make-файлом или ant-файлом. Часто достаточно поместить приложение в каталог, называемый webapps, autodeploy, или как-нибудь еще в зависимости от сервера приложений. По окончании процедуры установки сервлет можно вызвать из браузера, набрав в нем строку адреса вида:

http://:8000/InfoAppl/servlet/InfoServlet

Браузер покажет страничку HTML, сформированную сервлетом. Для других контейнеров и серверов приложений номер порта и путь к сервлету будет, конечно, иным.

Замечание по отладке

Для многих контейнеров и серверов приложений недостаточно просто перекомпилировать измененный код сервлета, чтобы контейнер воспринял обновленный сервлет. Надо еще сервлет переустановить.

Многие серверы приложений, в их числе и последние версии Tomcat, не запускают сервлеты по отдельности, а только в составе Web-приложения. Обычно утилиты установки создают необходимую для Web-приложения структуру каталогов, но это можно сделать и вручную. Обращение к сервлету в составе Web-приложения выглядит проще:

http://:8000/InfoAppl/InfoServlet

Как видно из сигнатуры метода getServletContext (), контейнер получает доступ к еще одному объекту — объекту типа ServletContext, содержащему контекст сервлета.

Контекст сервлета

Для всех сервлетов, работающих в рамках одного Web-приложения, создается один контекст. Контекст (context) сервлетов составляют каталоги и файлы, описывающие Web-приложение. Они содержат, в частности, код сервлета, изображения, чертежи, конфигурационный файл web.xml и его фрагменты, короче говоря, все относящееся к сервлету. При инициализации сервлета некоторые сведения о его контексте заносятся в объект типа ServletContext. Методы этого интерфейса позволяют сервлету получить сведения, содержащиеся в контексте.

Метод getServerInfo () позволяет получить имя и версию Java EE SDK, методы getMaj orVersion () и getMinorVersion () возвращают номер версии и модификации Servlet API.

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

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

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

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

Установить и удалить атрибуты можно методами

public void setAttribute(String name, Object value); public void removeAttribute(String name);

Атрибуты — это удобный способ сохранять объекты, общие для всего Web-приложения, разделяемые всеми сервлетами, входящими в Web-приложение, и независимые от отдельных запросов.

Метод Service

Основная работа сервлета заключена в методе

public void service(ServletRequest req, ServletResponse resp);

К этому методу контейнер обращается автоматически после завершения метода init () и передает ему объект req типа ServletRequest, содержащий всю информацию, находящуюся в запросе клиента. Созданием и заполнением объекта req тоже занимается контейнер сервлетов. Кроме того, контейнер создает и передает методу service () ссылку на пустой объект resp типа ServletResponse.

Метод service () обрабатывает сведения, содержащиеся в объекте req, и заносит результаты обработки в объект resp. Заполненный объект resp передается контейнеру, который через Web-сервер отправляет ответ клиенту. Все эти действия выполняются методами, описанными в интерфейсах ServletRequest и ServletResponse.

Интерфейс ServletRequest

В интерфейсе ServletRequest, который должен реализовать каждый контейнер сервлетов, описана масса методов getxxx(), возвращающих параметры запроса или null, если параметр неизвестен.

Методы getRemoteAddr(), getRemoteHost() и getRemotePort() возвращают IP-адрес, полное DNS-имя отправителя запроса или proxy-сервера и его номер порта, а методы getServerName () и getServerPort () возвращают имя и номер порта сервера, принявшего запрос.

Методы getLocalAddr (), getLocalName() и getLocalPort() возвращают IP-адрес, полное DNS-имя сетевого интерфейса, с которого получен запрос, и его номер порта.

Метод getScheme ( ) возвращает схему запроса: http:, https:, ftp: и т. д., а метод getProtocol () — имя протокола в виде строки, например "HTTP/1.1".

Методы getContentType () и getCharacterEncoding() возвращают MIME-тип и кодировку запроса, если они указаны в заголовке запроса, а метод getContentLength ( ) — длину тела запроса в байтах, если она известна, или -1, если длина неизвестна.

Метод setCharacterEncoding (String) устанавливает кодировку, если она не определяется методом getCharacterEncoding (), или переписывает кодировку, указанную в запросе. К этому методу часто приходится обращаться для правильного преобразования параметров запроса, пришедших в байтовой кодировке, в строку типа String. Пример такого обращения приведен в листинге 26.5. Этот метод следует применять до разбора параметров запроса.

Имена и значения параметров, пришедших с запросом, можно получить методами

public Enumeration getParameterNames(); public String getParameter(String name); public String[] getParameterValues(String name); public Map getParameterMap();

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

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

Наконец, интерфейс описывает два входных потока для получения тела запроса: байтовый и символьный. Байтовый поток реализуется специально разработанным классом

ServletInputStream.

Класс ServletInputStream — это абстрактный класс, расширяющий класс InputStream. Он добавляет к методам своего суперкласса только один метод

public int readLine(byte[] buf, int offset, int length);

читающий строку тела запроса в заранее определенный буфер buf. Чтение начинается с байта с номером offset и продолжается до достижения символа перевода строки '\n' или до достижения количества прочитанных символов length. Метод возвращает число прочитанных байтов или -1, если входной поток уже исчерпан.

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

public ServletInputStream getInputStream();

Второй поток — символьный — это поток класса BufferedReader, который мы рассмотрели в главе 23. Получить его можно методом

public BufferedReader getReader();

В пакете javax.servlet есть прямая реализация интерфейса ServletRequest — класс ServletRequestWrapper. Объект этого класса создается конструктором

public ServletRequestWrapper(ServletRequest req);

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

Интерфейс ServletResponse

Результаты своей работы метод service ( ) заносит в объект типа ServletResponse, ссылка на который предоставлена вторым аргументом метода service ().

Методы setContentType (String) и setLocale(Locale) устанавливают в заголовок ответа MIME-тип и локаль тела ответа, а метод setContentLength(int) записывает длину тела ответа. Если надо установить только кодировку символов в ответе, то можно воспользоваться методом setCharacterEncoding(String).

Тело ответа передается контейнеру через байтовый или символьный выходной поток. Байтовый поток специально разработанного класса ServletOutputStream возвращает метод

public ServletOutputStream getOutputStream();

Абстрактный класс ServletOutputStream расширяет класс OutputStream, добавляя к нему методы print (xxx) для вывода типов boolean, char, int, long, float, double, String и методы println(xxx) для тех же типов, добавляющие к выводимым данным символы "\r\n". Еще один метод println() без аргументов просто заносит в выходной поток символы "\r\n".

Символьный поток можно получить методом

public PrintWriter getWriter();

Именно он использован в листинге 26.2.

В пакете javax.servlet есть прямая реализация интерфейса ServletResponse — класс ServletResponseWrapper. Объект этого класса создается конструктором

public ServletResponseWrapper(ServletResponse resp);

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

Цикл работы сервлета

Сервлет загружается контейнером, как правило, при первом запросе к нему или во время запуска контейнера. После выполнения запроса сервлет может быть оставлен в спящем состоянии, ожидая следующего запроса, или выгружен, предварительно выполнив метод destroy (). Это зависит от реализации контейнера сервлетов.

Работа сервлета начинается с метода init(), затем выполняется метод service(), который может создавать объекты, обращаться к их методам, связываться с базами данных и удаленными объектами, выполняя обычную работу обычного класса Java. При этом надо учитывать, что сервлету может быть направлено сразу несколько запросов. Число одновременных запросов в промышленных системах достигает сотен и тысяч. Для обработки каждого запроса контейнер создает новый подпроцесс (thread), выполняющий метод service (). Поэтому выполняйте следующие правила:

□ разрабатывая метод service(), постоянно имейте в виду, что он будет параллельно выполняться несколькими подпроцессами, и принимайте меры к синхронизации их работы;

□ выносите создание объектов, общих для сервлета, определение параметров, пула соединений с базами данных и удаленными объектами, в поля класса и в метод

init();

□ завершающие действия, такие как закрытие потоков, запись результатов на диск, закрытие соединений, выносите в метод destroy(), выполняющийся при закрытии сервлета.

 

Класс 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=utf-8");

PrintWriter pw = resp.getWriter();

pw.println("");

pw.println("Параметры сервлета</^^е>"); pw.println(,,</head><body><h2>Сведения о сервлете<^2>"); pw.println("HiMq сервлета — " + getServletName() + "<br>"); </p> <p class="paragraph"> pw.println(,,Параметры и контекст сервлета: <br>"); </p> <p class="paragraph"> Enumeration names = getInitParameterNames(); </p> <p class="paragraph"> while (names.hasMoreElements()){ </p> <p class="paragraph"> String name = (String)names.nextElement(); pw.print(name + ": "); </p> <p class="paragraph"> pw.println(getInitParameter(name) + "<br>"); </p> <p class="paragraph"> } </p> <p class="paragraph"> pw.println("Сервер: " + cont.getServerInfo() +"<br>"); </p> <p class="paragraph"> pw.println("</body></html>") ; pw.flush(); pw.close(); </p> <p class="paragraph"> } </p> <p class="paragraph"> } </p> <p class="paragraph"> Работа по протоколу HTTP </p> <p class="paragraph"> Большинство запросов к сервлетам происходит по протоколу HTTP, который в настоящее время реализуется по рекомендации RFC 2616. Для удобства работы с этим протоколом интерфейсы ServletRequest и ServletResponse расширены интерфейсами HttpServletRequest и HttpServletResponse соответственно. При расширении интерфейсов в них добавлены методы, характерные для протокола HTTP. </p> <p class="paragraph"> Интерфейс HttpServletRequest </p> <p class="paragraph"> Несколько методов интерфейса HttpServletRequest позволяют разобрать HTTP-запрос. </p> <p class="paragraph"> Первая строка запроса, выполненного по протоколу HTTP, состоит из метода передачи данных, адреса URI и версии протокола. Строка завершается символами CRLF. Элементы строки разделяются пробелами, поэтому внутри каждого элемента первой строки запроса пробелов быть не должно. </p> <p class="paragraph"> Первая строка запроса выглядит примерно так: </p> <p class="paragraph"> GET HTTP/1.1 </p> <p class="paragraph"> Запрос HTTP начинается с одного из слов get, post, head или другого слова, обозначающего метод передачи данных. Узнать HTTP-метод передачи позволяет метод интерфейса </p> <p class="paragraph"> public String getMethod(); </p> <p class="paragraph"> Далее в запросе, после пробела, идет адрес URI, который разбирается несколькими методами: </p> <p class="paragraph"> □ public string getRequestURL() — возвращает адрес URL от названия схемы http до вопросительного знака; </p> <p class="paragraph"> □ public string getservletPath() — возвращает часть этого адреса, показывающую путь к сервлету. </p> <p class="paragraph"> Часть пути, определяющая контекст сервлета, возвращается методом </p> <p class="paragraph"> public String getContextPath(); </p> <p class="paragraph"> Часть URI после вопросительного знака возвращается методом </p> <p class="paragraph"> public String getQueryString(); </p> <p class="paragraph"> После имени сервлета может идти дополнительный путь к какому-нибудь файлу, который можно получить методом </p> <p class="paragraph"> public String getPathInfo(); </p> <p class="paragraph"> Этот же путь, дополненный до абсолютного пути к каталогу документов сервера, можно получить методом </p> <p class="paragraph"> public String getPathTranslated(); </p> <p class="paragraph"> После первой строки запроса могут идти заголовки запроса: Accept, Accept-Charset, Accept-Language, User-Agent и прочие заголовки, описанные в рекомендации RFC 2616. Узнать заголовки запроса и их значения можно методами </p> <p class="paragraph"> public Enumeration getHeaderNames(); public Enumeration getHeaders(String name); public String getHeader(String name); public int getIntHeader(String name); public long getDateHeader(String name); </p> <p class="paragraph"> Наконец, можно получить cookies, хранящиеся в браузере клиента, в виде массива объектов класса Cookie методом </p> <p class="paragraph"> public Cookie[] getCookies(); </p> <p class="paragraph"> В пакете javax.servlet.http есть прямая реализация интерфейса HttpServletRequest — класс HttpServletRequestWrapper, расширяющий класс ServletRequestWrapper. Объект этого класса создается конструктором </p> <p class="paragraph"> public HttpServletRequestWrapper(HttpServletRequest req); </p> <p class="paragraph"> и обладает всеми методами интерфейса HttpServletRequest. Разработчики, желающие расширить возможности объекта, содержащего HTTP-запрос, например для написания фильтра, могут расширить класс HttpServletRequestWrapper. </p> <p class="paragraph"> Интерфейс HttpServletResponse </p> <p class="paragraph"> При составлении ответа по протоколу HTTP можно использовать дополнительные методы, включенные в интерфейс HttpServletResponse. </p> <p class="paragraph"> Метод </p> <p class="paragraph"> public void setHeader(String name, String value); </p> <p class="paragraph"> устанавливает заголовок ответа с именем name и значением value. Старое значение, если оно существовало, при этом стирается. </p> <p class="paragraph"> Если надо дать несколько значений заголовку с именем name, то следует воспользоваться методом </p> <p class="paragraph"> public void addHeader(String name, String value); </p> <p class="paragraph"> Для заголовков с целочисленными значениями то же самое делается методами </p> <p class="paragraph"> public void setIntHeader(String name, int value); public void addIntHeader(String name, int value); </p> <p class="paragraph"> Заголовок с датой записывается методами </p> <p class="paragraph"> public void setDateHeader(String name, long date); public void addIntHeader(String name, int value); </p> <p class="paragraph"> Код ответа (status code) устанавливается методом </p> <p class="paragraph"> public void setStatus(int sc); </p> <p class="paragraph"> Как и все заголовки, этот метод записывается перед получением потока класса PrintWriter. Аргумент метода sc — это одна из множества констант, например константа sc_ok, соответствующая коду ответа 200 — успешная обработка запроса, sc_bad_request — код ответа 400 и т. д. Около сорока таких статических констант приведено в документации к интерфейсу HttpServletRequest. Метод setstatus () применяется для сообщений об успешной обработке с кодами 200—299. </p> <p class="paragraph"> Сообщения об ошибке посылаются методом </p> <p class="paragraph"> public void sendError(int sc, String message); </p> <p class="paragraph"> Обычно код ответа заносится в сообщение об ошибке message. Если надо послать стандартное сообщение об ошибке, например "404 Not Found", то применяется второй метод: </p> <p class="paragraph"> public void sendError(int sc); </p> <p class="paragraph"> Этот метод использован в листинге 26.6. </p> <p class="paragraph"> Сообщение методом sendError () посылается вместо результатов обработки запроса. Если попытаться послать после него ответ, то система выбросит исключение класса </p> <p class="paragraph"> IllegalStateException. </p> <p class="paragraph"> Наконец, к запросу можно добавить cookie методом </p> <p class="paragraph"> public void addCookie(Cookie cookie); </p> <p class="paragraph"> В пакете javax.servlet.http есть прямая реализация интерфейса HttpServletResponse — класс HttpServletResponseWrapper, расширяющий класс ServletResponseWrapper. Объект этого класса создается конструктором </p> <p class="paragraph"> public HttpServletResponseWrapper(HttpServletResponse resp); </p> <p class="paragraph"> и обладает всеми методами интерфейса HttpServletResponse. Разработчики, желающие расширить возможности объекта, содержащего запрос, могут расширить класс </p> <p class="paragraph"> HttpServletResponseWrapper. </p> <p class="paragraph"> Класс HttpServlet </p> <p class="paragraph"> Для использования особенностей протокола HTTP класс GenericServlet расширен абстрактным классом HttpServlet. Главная особенность этого класса заключается в том, что, расширяя его, не надо переопределять метод service (). Он уже определен, причем реализован так, что служит диспетчером, вызывающим методы doGet(), doPost() и другие методы, обрабатывающие HTTP-запросы с конкретными методами передачи данных GET, POST и др. </p> <p class="paragraph"> Вначале метод </p> <p class="paragraph"> public void service(ServletRequest req, ServletResponse resp); </p> <p class="paragraph"> анализирует типы аргументов req и resp. Эти типы должны быть на самом деле HttpServletRequest и HttpServletResponse. Если это не так, то метод выбрасывает исключение класса servletException и завершается. </p> <p class="paragraph"> Если аргументы req и resp подходящего типа, то методом getMethod () определяется HTTP-метод передачи данных и вызывается метод, соответствующий этому HTTP-методу, а именно один из методов </p> <p class="paragraph"> protected void doXxx(HttpServletRequest req, HttpServletResponse resp); </p> <p class="paragraph"> где Xxx означает Get, Post, Head, Delete, Options, Put или Trace. </p> <p class="paragraph"> Вот эти-то методы и надо переопределять, расширяя класс HttpServlet. Методы doHead (), doOptions () и doTrace () уже реализованы в соответствии с рекомендацией RFC 2616, их редко приходится переопределять. Остальные методы "реализованы" таким образом, что просто посылают сообщение о том, что они не реализованы. Чаще всего приходится переопределять методы doGet ( ) и doPost ( ). </p> <p> Аннотации сервлета </p> <p class="paragraph"> Всю конфигурацию сервлета типа HttpServlet, которая была описана в предыдущих пунктах и которая записывается обычно в конфигурационный файл web.xml, тожно сделать с помощью аннотаций прямо в коде сервлета. В таком случае конфигурационный файл web.xml становится необязательным. Если же файл web.xml присутствует, то записанные в нем значения будут перекрывать значения, указанные в аннотации. Это удобно в тех случаях, когда надо изменить конфигурацию сервлета без его перекомпиляции. </p> <p class="paragraph"> Аннотации находятся в пакете javax.servlet.annotation, который надо указать в операторе import. Вот как они выглядят: </p> <p class="paragraph"> import javax.servlet.*; </p> <p class="paragraph"> import j avax.servlet.annotation.*; </p> <p class="paragraph"> @WebServlet(name="informer", urlPatterns={"/InfoServlet"}, initParams={ </p> <p class="paragraph"> @WebInitParam(name="unit", value="1"), @WebInitParam(name="invoke", value="yes") </p> <p class="paragraph"> }) </p> <p class="paragraph"> public class InfoServlet extends HttpServlet{ </p> <p class="paragraph"> // Код сервлета .. . </p> <p class="paragraph"> } </p> <p class="paragraph"> Аннотация @WebServlet соответствует элементу <servlet> конфигурационного файла web.xml. Если в аннотации нет параметра name, то имя сервлета будет совпадать с полным именем класса сервлета, включая его пакет. Параметр urlPatterns можно заменить параметром value. Нельзя записывать оба эти параметра в одной аннотации, хотя один из них обязательно должен присутствовать. </p> <p class="paragraph"> Аннотация @WebInitParam соответствует элементу <init-param>. Она содержит имя и значение начального параметра. Совокупность начальных параметров записывается в аннотации @WebServlet параметром initParams, в фигурных скобках. </p> <p> Пример сервлета класса HttpServlet </p> <p class="paragraph"> Приведем пример сервлета, осуществляющего регистрацию клиента Web-приложения- некоторой системы дистанционного обучения (СДО). Сервлет RegPrepServlet </p> <p class="paragraph"> принимает запрос от HTML-формы, соединяется с базой данных, заносит в нее полученную информацию и отправляет клиенту подтверждение регистрации в виде страницы HTML, содержащей форму для выбора учебного курса. В листинге 26.4 приведена HTML-форма регистрации клиента. Ее вид показан на рис. 26.2. </p> <p> Листинг 26.4. Форма регистрации клиента СДО </p> <p class="paragraph"> <html><head> </p> <p class="paragraph"> <^^е>Регистрация

content="text/html; charset=utf-8">

Дистанционная система обучения Qn,0

<р>Для регистрации занесите сведения о себе в следующие поля:


"">

                             

Фамилия:

Имя:

Отчество:

E-mail:

Рис. 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=utf-8");

PrintWriter pw = resp.getWriter();

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

pw.println("");

pw.println("Продолжение регистрации</^^е>"); pw.println("</head><body><h2 align=center>" + </p> <p class="paragraph"> "Дистанционная система обучения СДО<^2>"); pw.println ("^3>Замечание:</h3>" ); </p> <p class="paragraph"> pw.println("Заполните, пожалуйста, все поля.<Ьг>"); </p> <p class="paragraph"> pw.println("</body></html>"); </p> <p class="paragraph"> pw.flush(); </p> <p class="paragraph"> pw.close(); </p> <p class="paragraph"> return; </p> <p class="paragraph"> } </p> <p class="paragraph"> String fullname = surname.trim() + " " + name.trim() + " " + secname.trim(); pst.setString(1, fullname); pst.setString(2, addr); int count = pst.executeUpdate(); </p> <p class="paragraph"> Statement st = con.createStatement(); </p> <p class="paragraph"> ResultSet rs = st.executeQuery( </p> <p class="paragraph"> "SELECT id, name FROM students ORDER BY id DESC"); </p> <p class="paragraph"> rs.next(); </p> <p class="paragraph"> int id = rs.getInt(1); fullname = rs.getString(2); </p> <p class="paragraph"> rs.close(); </p> <p class="paragraph"> StringTokenizer sttok = new StringTokenizer(fullname); sttok.nextToken(); name = sttok.nextToken(); secname = sttok.nextToken(); </p> <p class="paragraph"> pw.println("<html><head>"); </p> <p class="paragraph"> pw.println ("<Ь^1е>Регистрация" );

pw.println("

" +

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

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

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

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

pw.println("

"\"">");

pw.println("

в которое можно записать идентификатор клиента. Для применения данного средства надо на каждой странице 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 в элемент . Имя класса-фильтра можно получить методом

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=utf-8

Но, как правило, браузер не посылает этот заголовок 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 в элемент . Это можно сделать и вручную. Приписка фильтра к сервлету отмечается внутри элемента парой вложенных элементов и . Например:

MyFilter

RegServlet

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

MyFilter

*.html

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

Порядок фильтров в цепочке соответствует порядку элементов в конфигурационном файле 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 элементом со значением 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 приведен типичный пример асинхронного сервлета. Обратиться к нему можно примерно так:

После набора этого адреса в адресной строке браузера, подождите 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(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 +

"
Started at: " + before.toString() +

".
Completed at: " + result + ".
"; 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