Обзоры Обзоры 15.07.2003 в 21:00 comment

Конвейер на Web-узле

author avatar
https://secure.gravatar.com/avatar/2f8d57cddfeb455ba418faa11ee01bb0?s=96&r=g&d=https://itc.ua/wp-content/uploads/2023/06/no-avatar.png *** https://secure.gravatar.com/avatar/2f8d57cddfeb455ba418faa11ee01bb0?s=96&r=g&d=https://itc.ua/wp-content/uploads/2023/06/no-avatar.png *** https://itc.ua/wp-content/themes/ITC_6.0/images/no-avatar.svg

ITC.UA

автор

С тех пор прошло более тридцати лет. Оконные приложения практически вытеснили программы, запускаемые из командной строки. А с переносом исполняемого кода на Web-сервер для конвейерной обработки информации, казалось, и вовсе не останется места. Действительно, при этом назначение стандартных потоков ввода/вывода становится несколько иным, и для передачи результатов работы одной Web-программы на вход другой, выполняющейся на стороне сервера, необходимо затрачивать дополнительные усилия. Однако с появлением сервлетов ситуация существенно изменилась.

Сервлет… с фильтром

Вначале сервлеты немногим отличались от CGI-сценариев. Несмотря на то что запускались они как потоки и один сервлет мог обслуживать несколько запросов, принцип обработки данных оставался таким же, как и в "классических" CGI-программах. Сервлет получал параметры, заданные пользователем, обрабатывал их и возвращал ответ серверу, который, в свою очередь, передавал информацию клиенту.

Вскоре стало ясно, что для создания полноценного Web-приложения, решающего нетривиальные задачи, одного сервлета явно недостаточно — дополнительно необходимы HTML-файлы и другие статические ресурсы. Появившиеся вскоре JSP-документы также нашли достойное место в составе Web-приложений, а для согласования действий различных компонентов были разработаны дескрипторы доставки.

В многокомпонентных приложениях нельзя обойтись без средств, позволяющих эффективно перенаправлять запрос и включать в текущий документ результаты работы других ресурсов. Для решения этой задачи обычно используются методы forward() и include(), объявленные в составе интерфейса RequestDispatcher. Например, два сервлета, совместно обслуживающие один и тот же запрос, могут взаимодействовать таким образом. Первый выполняет предварительную обработку данных, полученных от клиента, а затем создает объект RequestDispatcher, описывающий следующий сервлет. В результате он получает в свое распоряжение вышеупомянутые методы, позволяющие передать запрос на дальнейшую обработку.

Однако в спецификации Servlet 2.3 разработчиков Web-приложений ожидал приятный сюрприз. Оказалось, что RequestDispatcher — не единственное средство перенаправления запроса. В новой спецификации были определены компоненты, называемые фильтрами, которые сделали возможным создание полноценных цепочек Web-программ, предназначенных для поэтапной обработки информации. Контейнер, реализующий средства, описанные в Servlet 2.3, позволяет связать с любым сервлетом цепочку из одного или нескольких фильтров (при этом последним звеном цепочки окажется сам сервлет). Направленный ему запрос перехватывается первым фильтром, который выполняет требуемые действия и передает управление следующему. Таким образом запрос движется по цепочке до тех пор, пока не достигнет целевого сервлета.

Психологічний профорієнтаційний тест для IT-фахівців від Ithillel.
Пройдіть психологічний профорієнтаційний тест для IT-фахівців щоб дізнатися ваші сильні сторони, вподобання і інтереси і з'ясувати, яка IT-спеціальність вам підходить.
Пройти тест

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

Чтобы Java-класс мог называться фильтром и перехватывать обращения к сервлету, он должен реализовывать интерфейс Filter, в составе которого присутствуют три метода: init() — для инициализации фильтра, destroy() — для завершения его работы и doFilter() — для выполнения реальной обработки запроса. Вызовы этих трех методов и составляют жизненный цикл фильтра. Каждый, кто имеет хотя бы минимальный опыт создания Web-программ на языке Java, не может не заметить сходства с жизненным циклом обычного сервлета. От doGet, doPost и других подобных методов, объявленных в интерфейсе HttpServlet, doFilter отличается лишь тем, что, помимо объектов ServletRequest и ServletResponse, ему передается третий параметр — объект FilterChain, представляющий цепочку фильтров. Вызов метода doFilter класса FilterChain приводит к передаче управления следующему фильтру в цепочке или последнему ее звену — целевому сервлету.

Запрос — структура гибкая

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

При обращении к Web-ресурсу информация передается посредством полей заголовка и параметров, содержащихся в составе запроса. Несмотря на то что в классе HttpServletRequest отсутствуют методы типа setParameter или setHeader, с помощью которых можно было бы изменить параметры запроса или поля заголовка, организовать передачу по цепочке необходимых данных относительно несложно. Для этого имеется специальный класс HttpServletRequestWrapper, позволяющий сформировать своеобразную "оболочку" вокруг существующего объекта HttpServletRequest. Определив подкласс HttpServletRequestWrapper, можно переопределить getHeader, getHeaderNames, getParameter, getParameterValues и другие методы, посредством которых сервлеты и фильтры получают информацию о запросе. Если же программист по каким-то причинам предпочитает работать на более низком уровне и передавать данные о запросах с помощью объектов ServletRequest, ему вместо HttpServletRequestWrapper следует использовать класс ServletRequestWrapper.

Психологічний профорієнтаційний тест для IT-фахівців від Ithillel.
Пройдіть психологічний профорієнтаційний тест для IT-фахівців щоб дізнатися ваші сильні сторони, вподобання і інтереси і з'ясувати, яка IT-спеціальність вам підходить.
Пройти тест


Фильтрация выходных данных

Предположим, что в распоряжении разработчика имеется сервлет, который блестяще выполняет почти все, что требуется от Web-приложения. Он успешно анализирует параметры и заголовки запроса, обрабатывает их и генерирует выходные данные. Дополнив этот сервлет рядом элементарных функций (а не "изобретая велосипед" заново), можно было бы считать задачу по созданию приложения выполненной. Однако это оказывается не так просто: ведь сервлет передает клиенту готовый HTML-файл, содержащий открывающий и закрывающий дескрипторы <html>, заголовок документа и т. д. Если же исходный код чудо-сервлета по тем или иным причинам недоступен, придется дополнительно организовывать "перехват" и обработку выходных данных. Но на самом деле данную задачу можно достаточно просто решить, воспользовавшись все теми же фильтрами.

В примерах вызов метода doFilter() объекта FilterChain обычно является последней командой в теле метода doFilter() самого фильтра. Но давайте отступим от этой традиции и включим после данного вызова другие операторы, каким-то образом сообщающие о своем присутствии. Результат будет несколько неожиданным. Оказывается после выполнения всех звеньев цепочки, включая целевой сервлет, текущий фильтр продолжит свою работу. Другими словами, при обращении к методу doFilter() объекта FilterChain управление не теряется навсегда, а возвращается в точку, следующую за вызовом. Таким образом, действия, реализованные в фильтре, могут выполняться не только перед вызовом целевого сервлета, но и по завершении его работы. Но как получить данные, сгенерированные сервлетом? Ведь тот записывает их в выходной поток, содержимое которого контейнер передает клиенту.

Чтобы ответить на этот вопрос, надо вспомнить о существовании класса CharArrayWriter, реализующего специальный поток, связанный с символьным массивом. Создав подкласс HttpServletResponseWrapper и переопределив в нем метод getWriter(), можно "подставить" сервлету "фальшивый" поток, в который и будут записаны результаты его работы. Таким образом, вывод не отправится клиенту, а окажется доступным в теле метода doFilter().

Дальше остается лишь представить содержимое массива в виде строки (метод toString()), выполнить необходимую обработку и снова записать, на этот раз уже в настоящий поток, данные из которого будут переданы серверу, а затем и Web-клиенту.

В этом случае сравнение с конвейерной обработкой уже становится не совсем уместным, так как в "классическом" варианте предшествующая программа не способна влиять на результаты работы последующих. Но так как аналогию с обработкой данных в системе Unix хочется все же продолжить, то перехват потока вывода фильтром можно сравнить с порождением дочерних процессов.


Фильтры и RequestDispatcher

После первого знакомства с фильтрами у начинающего разработчика возникает вопрос: зачем они нужны, если тот же результат можно получить, используя метод forward()? Действительно, сервлет способен предварительно обработать данные и с помощью объекта RequestDispatcher отправить их следующему сервлету, тот — выполнить свои действия и обратиться далее, и так до тех пор, пока запрос не будет полностью обслужен. При вызове метода forward() ему передаются объекты HttpServletRequest и HttpServletResponse. Как и при работе с фильтрами, разработчик может создавать на их базе объекты HttpServletRequestWrapper и HttpServletResponseWrapper, переопределяя при необходимости их методы. Чем же принципиально отличается механизм фильтров от перенаправления запросов?

Различия между этими средствами, конечно же, не только в названиях. При создании объекта RequestDispatcher необходимо явно (в коде!) указывать ресурс, которому будет передан запрос, лишь после этого можно вызывать метод forward(). Другими словами, сервлет должен знать о том, какой ресурс продолжает обработку данных. Совсем по-другому обстоит дело с фильтрами. Здесь поддержкой цепочки занимается объект FilterChain, предоставляемый контейнером. Фильтр, вызывающий метод doFilter() объекта FilterChain, не имеет никаких сведений о других звеньях цепочки, поэтому работает полностью независимо от них. Соответственно и разработчики могут трудиться над фильтрами абсолютно автономно.

Состав же цепочки определяется в дескрипторе доставки приложения. Ниже приведен фрагмент файла web.xml:

...
<filter>
<filter-name>firstfilter
</filter-name>
<filter-class>
filters.FirstFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>firstfilter
</filter-name>
<servlet-name>mainservlet
</servlet-name>
</filter-mapping>
...

В данном примере элемент <filter> описывает фильтр с именем firstfilter, а элемент <filter-mapping> задает отображение, т. е. указывает, что при обращении к сервлету с именем mainservlet необходимо предварительно передать управление именно firstfilter. Порядок следования отображений в дескрипторе доставки приложения определяет положение фильтров в цепочке, а следовательно, и порядок их вызова при обработке запросов. Так, например, если после приведенных выше элементов включить отображение фильтра secondfilter, то он займет вторую позицию в цепочке.

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

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

А как же JSP?

Как видите, фильтры прекрасно "сотрудничают" с сервлетами, дополняя их возможности. Но в последнее время все чаще используются JSP-документы. Это вполне оправданно, ведь JSP позволяют объединять в одном файле статические данные и средства генерации динамической информации. Но при работе с JSP-документами также часто возникают задачи, решать которые удобнее, используя фильтры. Можно ли организовать конвейерную обработку запросов, адресованных JSP?

Оказывается можно, и очень просто. Достаточно вместо элемента <servlet-name> включить в состав <filter-mapping> элемент <url-pattern> и указать в нем расположение JSP-документа. Полученный в результате дескриптор доставки предоставляет контейнеру информацию о том, что запрос, направленный указанному JSP-документу (или любому другому ресурсу, заданному в качестве значения <url-pattern>), надо предварительно передать для обработки фильтру.


Работа продолжается

С момента появления спецификации Servlet 2.3 механизм фильтров успел хорошо себя зарекомендовать в реальных разработках. Однако над ним довлеет одно существенное ограничение: фильтры могут перехватывать запросы, непосредственно переданные клиентами, но оказываются бессильны перед запросами, перенаправленными с помощью методов forward() и include() объекта RequestDispatcher. Попытка устранить этот недостаток предпринята в проекте спецификации Servlet 2.4. Разработчики этого документа предусмотрели в составе дескриптора доставки приложения новый элемент <dispatcher>. С его помощью можно указать, каким образом фильтр должен получать управление: непосредственно от клиента (значение REQUEST) либо в результате перенаправления с использованием методов forward() (значение FORWARD) или include() (значение INCLUDE) класса RequestDispatcher. Это нововведение значительно расширяет возможности для создания Web-приложений с применением фильтров.

Хорошей идее суждена долгая жизнь — данный пример (фильтры, перехватывающие обращения к сервлетам) еще раз подтверждает это. Удачные решения нередко успешно применяются даже в тех областях, где для них, казалось бы, вовсе нет места. В настоящее время Web-приложения бурно развиваются, и хочется верить, что в них найдут воплощение и многие другие принципы Unix, заложенные более тридцати лет назад Томпсоном и Ричи.


Loading comments...

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: