Сервлеты

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

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

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

http://some.firm.com/cgi-bin/mycgiprog.pl?name=Ivanov&age=27

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

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 можно скопировать со страницы http://tomcat.apache.org/ и установить отдельно. Удобнее скопировать его дистрибутив в виде 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. Для того чтобы запустить утилиту, в браузере надо набрать примерно такую строку: http://localhost:8080/manager/.

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

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

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app

PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/j2ee/dtds/web-app 2 3.dtd">

<web-app>

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

-->

<servlet>

<servlet-name>default</servlet-name>

<servlet-class>

org.apache.tomcat.servlets.DefaultServlet

</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

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

-->

<servlet>

<servlet-name>invoker</servlet-name> <servlet-class>

org.apache.tomcat.servlets.InvokerServlet

</servlet-class>

<init-param>

<param-name>debug</param-name>

<param-value>0</param-value>

</init-param>

<load-on-startup>2</load-on-startup>

</servlet>

<!--

Все запросы к страницам JSP вначале обрабатываются сервлетом JspServlet. Ему дано имя "jsp".

-->

<servlet>

<servlet-name>j sp</servlet-name>

<servlet-class>

org.apache.j asper.runtime.JspServlet

</servlet-class>

<!— uncomment the following to use Jikes for JSP compilation Уберите комментарий, если вы используете компилятор jikes. <init-param>

<param-name>j spCompilerPlugin</param-name>

<pa ram-value>

org.apache.j asper.compiler.JikesJavaCompiler

</param-value>

</init-param>

-->

<load-on-startup>

-2147483646

</load-on-startup>

</servlet>

<!--

Некоторым путям-псевдонимам сопоставляется сервлет, который вызывается при указании в строке URL этого пути. Путь отсчитывается относительно корневого каталога контейнера, чаще всего это public html или webapps.

Если в адресе URL указан каталог servlet, то вызывается сервлет с именем "invoker", т. е. InvokerServlet.

-->

<servlet-mapping>

<servlet-name>invoker</servlet-name> <url-pattern>/servlet/*</url-pattern>

</servlet-mapping>

<!--

Если в адресе указан путь к странице JSP, то вначале вызывается сервлет с именем "jsp", т. е. сервлет JspServlet.

-->

<servlet-mapping>

<servlet-name>j sp</servlet-name>

<url-pattern>*.j sp</url-pattern>

</servlet-mapping>

<session-config>

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

<!--

Ниже идет длинный список соответствий расширений имен файлов и MIME-типов содержимого этих файлов.

-->

<mime-mapping>

<extension>txt</extension>

<mime-type>text/plain</mime-type>

</mime-mapp i ng>

<mime-mapping>

<extension>html</extension>

<mime-type>text/html</mime-type>

</mime-mapp i ng>

<mime-mapping>

<extension>htm</extension>

<mime-type>text/html</mime-type>

</mime-mapp i ng>

<mime-mapping>

<extension>gif</extension>

<mime-type>image/gif</mime-type>

</mime-mapp i ng>

<!--

И так далее.

Наконец, идет список файлов, которые посылаются клиенту при обращении только к каталогу без указания ресурса.

-->

<welcome-file-list>

<welcome-file>index.jsp </welcome-file> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> </welcome-file-list>

</web-app>

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

Интерфейс ServletConfig

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

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

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

Начальные параметры записываются в конфигурационный файл web.xml во время установки сервлета вручную или с помощью утилиты установки. Механизм задания и чтения начальных параметров сервлета очень похож на механизм определения параметров апплета, записываемых в теги <param> и читаемых методами 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=windows-1251");

PrintWriter pw = resp.getWriter();

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

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

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

Enumeration names = sc.getInitParameterNames();

while (names.hasMoreElements()){

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

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

}

pw.println("</body></html>"); 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://<servlethost>:8000/InfoAppl/servlet/InfoServlet

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

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

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

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

http://<servlethost>:8000/InfoAppl/InfoServlet

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

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

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

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

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

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 и продолжается до достижения символа перевода строки ' ' или до достижения количества прочитанных символов 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) для тех же типов, добавляющие к выводимым данным символы " ". Еще один метод println() без аргументов просто заносит в выходной поток символы " ".

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

public PrintWriter getWriter();

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

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

public ServletResponseWrapper(ServletResponse resp);

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

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

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

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

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

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

init();

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