О сервлетах

Введение

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

В современной разработке количество инструментов достигло такого уровня, что их освоение компенсируется за счёт абстрагирования базовых технологий. Однако использование ‘черного ящика’ может сделать счастливым только клиента, но никак не разработчика, который, скорее всего, столкнётся с ‘головной болью’ из-за недальновидности принятых решений.

Цель данной статьи — подвести итоги моего знакомства с сервлетами и систематизировать, а также формализировать полученные знания.

История

Ранние дни и мотивация создания

В середине 90-х годов, когда интернет начинал набирать популярность, веб-приложения обычно создавались с использованием CGI (Common Gateway Interface). Однако CGI-скрипты имели значительные недостатки, такие как высокая накладная стоимость создания нового процесса для каждого запроса и ограниченные возможности многопоточности. Эти недостатки побудили разработчиков искать более эффективные решения.

<<<<<<<<<<ВСТАВИТЬ ПРИМЕР CGI скрипта

Имя “сервлет” было впервые использовано компанией Sun Microsystems в 1997 году. Джеймс Дэвидсон, разработчик из Sun Microsystems, начал работать над созданием более эффективной альтернативы CGI-скриптам, а также стандартизации API для обработки HTTP-запросов и предоставления динамического контента.

Мы, люди, любим все раскладывать по полочкам(smile), и усилия разработчиков Sun Microsystems привели к созданию спецификации Servlet API. По сути, это формализация контрактов в виде классов и интерфейсов между сервлетом и средой выполнения, которая представляет собой контейнер сервлетов (например, веб-сервер Tomcat). А сам сервлет мы можем назвать продуктом или “главным героем” этой стандартизации.

По факту же, сервлет – это маленькая java программа, которая работает внутри веб-сервера. Эта программа работает с веб-клиентом, посредством запросов и ответов, обычно, через HTTP-протокол. Помимо улучшения производительности и поддержки многопоточности, в сравнение с CGI-скриптами, сервлеты также обладают аттрибутами Java классов – гарантия выполнение на любых устройствах где может быть развернута JVM и легкая интерграция с Java EE.

Что такое сервлет? (семантика)

Слово “servlet” состоит из двух частей “serve” и “let“.
Префис “Serve” – мы можем перевести как “обслуживать”, а суффикс “let” – указывает на что-то маленькое или подчиненное. Я интерпретировал это для себя как “обслуживания запроса”.

Назначение сервлета

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

Эту потребность и закрывают сервлеты. Цель сервлета заключается в динамической обработке HTTP-запросов.
Рассмотрим основные этапы работы сервлета:

  1. Получение запроса: Сервлеты получают HTTP-запросы от клиента через веб-сервер, который действует как посредник между клиентом и сервлетом.
  2. Обработка запроса: Сервлеты анализируют полученные данные и выполняют необходимую бизнес-логику. Этот этап может включать в себя взаимодействие с базами данных, выполнение вычислений или интеграцию с другими веб-сервисами.
  3. Формирование HTTP-ответа: После обработки запроса сервлеты формируют ответ, который может включать HTML-страницы, JSON-данные, XML и другие форматы, и отправляют его обратно клиенту.

Инфраструктура

После знакомства с сервлетами уже стало явным, то что они не работают в вакууме, существует некая среда выполнения – контейнер-сервлетов. В этой статье мы рассмотрим Tomcat, так как он является самым популярным контейнером-сервлетов, который исполняет контракт Servlet-API. Но прежде чем разговаривать о Tomcat, пару слов о том, что такое HTTP- протокол, так как именно благодаря ему клиент и сервер понимают друг друга.

Хотя Servlet API не ограничивает сервлеты конкретным протоколом общения, все же сервлеты преимущественно используются с HTTP-протоколом.

HTTP – протокол

Объяснить машине, что 2 + 2 = 4 это только пол дела. Нужно еще научить ее различать 2,+ и =.

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

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

Структура HTTP-запроса
POST /myservlet HTTP/1.1                 // Стартовая строка
Host: localhost:8080                     // Заголовок
Content-Type: application/x-www-form-urlencoded  // Заголовок
Content-Length: 27                       // Заголовок

username=user&password=pass              // Тело запроса
  1. Стартовая строка (Request Line):
    • POST – указывает на тип операции, которую хочет выполнить клиент (например, GET, POST, PUT, DELETE). Основываясь на следующей структуре браузер определит тип запрашиваемой операции. Может показаться, что браузеру, в отличии от сервера не нужно это знать, но основываясь на типе запроса браузер проводит дополнительную оптимизацию, формирует структуру запроса, например, для GET запросов не будет сформировано тело запроса, так как мы не передаем информацию, а запрашиваем.
    • /myservlet – путь, который указывает на целевой ресурс на сервере. Сервер предоставляет множество сервисов, мы должны явно указать путь до ресурса, что нас интересует.
    • HTTP/1.1 – указывает на версию протокола HTTP, используемого в запросе. Более новые протоколы имею большие возможности, так как время не стоит на месте. Но соответственно требуют своей уникальной структуры.
  2. Заголовки (Headers) – метаданные, которые содержат дополнительное описание запроса:
    • Host: localhost:8080 – указывает имя хоста и порт сервера, к которому отправлен запрос.
    • Content-Type: application/x-www-form-urlencoded – указывает тип содержимого тела запроса.
    • Content-Length: 27 – указывает длину тела запроса в байтах.
  3. Пустая строка:
    • Разделяет заголовки и тело запроса.
  4. Тело запроса (Body):
    • Содержит данные, отправляемые в запросе.
Структура HTTP-ответа
HTTP/1.1 200 OK                         // Стартовая строка
Content-Type: text/plain                // Заголовок
Content-Length: 47                      // Заголовок

<<some content>>                        // Тело ответа
  • Стартовая строка (Status Line):
    • HTTP/1.1 – указывает на версию протокола HTTP, используемого в ответе.
    • 200 – код состояния, указывает на результат обработки запроса (например, 200 OK, 404 Not Found).
    • OK – статус сообщения.
  • Заголовки (Headers):
    • Content-Type: text/plain – указывает тип содержимого тела ответа.
    • Content-Length: 47 – указывает длину тела ответа в байтах.
  • Пустая строка:
    • Разделяет заголовки и тело ответа.
  • Тело ответа (Body):
    • Содержит данные, отправляемые в ответе.

Таким образом когда сервер получает HTTP-запрос от клиента, он уже знает как его разобрать на составные части в сложить в объект удобный для работы.

Tomcat: Понимание основ

Давайте рассмотрим среду выполнения нашего сервлета. Казалось бы, возьми унаследуйся от класса HttpServlet, переопредели метод doGet, обработай данные HttpRequest и верни результат как HttpResponse. Но под капотом у сервлета гудит движок с красивым именем Catalina, надо бы заглянуть.

Какие задачи решает Tomcat?

Как веб-сервер
  • Обслуживание статических ресурсов;
  • Маштабируемость и управление производительностью;
  • Обеспечение безопасности.
Как реализация сервлет API
  • Обработка динамических запросов;
  • Управление сессиями;
  • Поддержка JSP.

Компоненты Tomcat

Разделяя зону ответственности Tomcat на две части, мы можем выделить два независимых компонента существующих на одной платформе.

Connector:

Этот компонент отвечает за связь с клиентом. Он открывает сокет и ожидает подключения. Для каждого нового соединения создается отдельный поток, в котором и происходит обработка запроса. В зависимости от реализации, коннекторы могут работать с разными протоколами общения HTTP/1.1, HTTP/2, AJP. При этом все они преобразуют входящие данные в стандартные объекты Request и Response.

COntainer

Этот компонент выполняет обработку запросов и возвращает ответы. Он поддерживает конвейер Valves (интерфейс Pipeline), который регулирует логику обработки запросов в заданной последовательности (at runtime). В Tomcat функционируют несколько типов контейнеров, каждый из которых определяет инфраструктуру для работы с динамическим запросами:

  • Engine – главный контейнер, определяющий весь движок сервлетов Catalina. Он координирует работу внутренних контейнеров (Host) распределяя по ним поступающие запросы.
  • Host – контейнер для виртуальных адресов. В рамках Host может может существовать один или несколько подконтейнеров Context, в один из которых будет отправлен запрос для обработки.
  • Context – контейнер, который соответствует отдельному веб-приложению, включает подконтейнеры типа Wrapper.
  • Wrapper – контейнер самого низкого уровня, который содержит и управляет конкретным сервлетом внутри веб-приложения. Отвечает за обработку запроса этим сервлетом.

Контейнеры также связаны с дополнительными классами поддержки, такими как Loader (загрузчик классов), Manager (управление сессиями) и Resources (доступ к статическим ресурсам), которые расширяют функциональность и могут настраиваться индивидуально или быть доступными для родительских контейнеров.

Взаимодействие компонентов в Tomcat: Роль Сервиса

В архитектуре Tomcat, компоненты Connector и Engine выполняют различные, но взаимосвязанные функции. Connector отвечает за прием запросов от клиентов и передачу их внутрь сервера, тогда как Engine обрабатывает эти запросы, определяя, к какому приложению или сервлету они должны быть направлены.

Service: Посредник между мирами

Service в Tomcat действует как связующее звено между Connector и Engine. Он не только координирует их работу, но и управляет взаимодействием между ними. Один из ключевых элементов этого взаимодействия — это Mapper, который отвечает за маршрутизацию запросов.

Mapper и его друзья MapperListener и Digester

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

Digester, активно применяется для интерпретации конфигурационных файлов (например, web.xml, аннотации или динамическая регистрация сервлетов и фильтров), создавая или модифицируя Java объекты, которые определяют поведение контейнеров.

MapperListener обновляет Mapper в ответ на изменения в конфигурации контейнеров, добавляя новые или измененные маршруты. Это гарантирует корректную маршрутизацию всех запросов к нужным обработчикам.

Пример работы

Когда пользовательский запрос поступает в Tomcat, Connector напрямую обращается к Mapper для получения mappingData — структуры, содержащей всю необходимую информацию для маршрутизации запроса. Эта информация заполняет request и в дальнейшем используется контейнерами для определения его последующей маршрутизации.

Экземпляр веб-сервера

Итак, объект Service инкапсулирует в себе два главных компонента, обеспечивающих всю функциональность сервера. Всё это упаковывается в компонент Server, который действует как центральный контрольный пункт для внутренних сервисов и является высшим уровнем в иерархии компонентов Tomcat. Server управляет всеми аспектами серверных операций, включая запуск, остановку и обслуживание внутренних Service.

Скрипты в директории /bin, такие как startup.sh и shutdown.sh, позволяют управлять сервером прямо из командной строки.

HOST, CONTEXT, WRAPPER – поближе к нашему приложению

host

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

context

Определение Tomcat как сервлет-контейнера воплощается в жизнь на уровне интерфейса Context. Этот уровень служит ключевой интеграционной точкой, где инфраструктура Tomcat тесно связывается со спецификацией Servlet API, обеспечивая управление сервлетами, сессиями, фильтрами, слушателями событий и другими аспектами веб-приложения. Примеры интеграции:

  • Спецификация Servlet API определяет, что контейнер должен уметь маршрутизировать запросы к соответствующим сервлетам, основываясь на URL. Это достигается с помощью объекта Mapper (внутренний компонент Tomcat).
  • В соответствии с Servlet API, Context контейнер должен поддерживать управление сессиями. Tomcat реализует это требование c помощью специального компонента, отвечающего за сессии (Manager).
  • Servlet API требует, чтобы сервлет-контейнеры могли фильтровать запросы перед их обработкой сервлетами. Это достигается с помощью таких компонентов как: Pipeline и Valve. В StandardContextValve, на основании FilterConfig создается объект FilterChain, который и выполняет фильтрацию.
Создание контекста приложения

При старте Tomcat сканирует директорию `webapps`, интерпретируя каждую папку или WAR-архив как отдельное приложение, где имя папки или архива становится именем контекста. Процесс инициализации включает следующие шаги:

  1. Загрузка конфигурации: С помощью компонента Digester Tomcat анализирует `web.xml` и аннотации сервлетов для создания начальной конфигурации приложения.
  2. Создание и настройка: На основе анализа конфигурации, Digester создает соответствующие объекты, включая сервлеты, фильтры и слушатели.
  3. Настройка загрузчика классов: WebappClassLoader настраивается для загрузки java классов и ресурсов, нужных приложению, с использованием ленивой загрузки по требованию.
  4. Инициализация компонентов контекста: Каждый сервлет инициализируется через вызов метода init(ServletConfig). Объект ServletConfig, созданный ранее с помощью Digester при анализе web.xml и других конфигурационных данных, передается в метод init(). Этот объект содержит все необходимые параметры инициализации и предоставляет сервлету доступ к ServletContext. Это позволяет сервлетам получать информацию о своём окружении и ресурсах.

Context является фундаментальным компонентом, который агрегирует и координирует все элементы управления приложением в рамках сервера.

WRAPPER

Wrapper в Tomcat — это абстрактное представление экземпляра сервлета внутри контейнера. Этот компонент служит минимальной единицей в структуре движка Tomcat и не поддерживает дочерние элементы, что делает его крайним уровнем абстракции в контексте Engine-фреймворка.

Роль Wrapper в Инициалиализации сервлетов

После инициализации компонентов, каждый сервлет “оборачивается” в объект Wrapper, который управляет его жизненным циклом, начиная от вызова метода init() до destroy(). Wrapper также играет ключевую роль в перехватывании и обработке запросов.

Функция фильтрации через Valve

В процессе перехватывания запросов, Wrapper использует объекты типа Valve для реализации механизма фильтрации. Эти объекты позволяют осуществлять выбор фильтров для каждого запроса на основе URL-паттерна и имени целевого сервлета. После определения необходимых фильтров, Wrapper создает ApplicationFilterChain. Эта цепочка фильтров обеспечивает последовательную обработку запросов, завершаясь вызовом метода service(request, response) самого сервлета. На уровне Tomcat вся цепочка фильтров создается и обрабатывает StandartWrapperValve объектом.

Механизм промежуточной обработки (Middleware)

Этот механизм реализуется через два ключевых интерфейса: Pipeline и Valve, которые вместе обеспечивают гибкое управление потоком запросов и ответов внутри сервера.

Pipeline

Pipeline — это компонент, присутствующий в каждом типе контейнера в Tomcat, таком как Host, Context или Wrapper. Он представляет собой последовательность объектов Valve, организованную в виде связного списка. Последний элемент в этом списке, известный как “базовый Valve”, обязательно должен передать обработанный запрос дальше — либо к следующему контейнеру в иерархии, либо непосредственно к сервлету.

Valve

Каждый Valve обычно ассоциируется с определенным контейнером и вместе с другими Valve формирует Pipeline данного контейнера. Основной метод объекта Valve — invoke(Request, Response), который вызывается для каждого проходящего через Pipeline запроса. В этом методе запрос обрабатывается согласно определенным правилам, после чего обработка передается следующему Valve в цепочке.

Применение Valve

Valve могут выполнять различные функции в рамках обработки запросов, например:

  • Аутентификация и Авторизация: Проверка прав пользователя на доступ к ресурсам.
  • Логирование: Запись информации о запросах и ответах для аудита и мониторинга.
  • Управление Сессиями: Поддержка сессий пользователя, управление их созданием и закрытием.
  • Фильтрация Содержимого: Модификация входящих запросов или исходящих ответов для добавления заголовков, изменения содержимого или выполнения перенаправлений.

Таким образом, Pipeline и Valve в Tomcat позволяют разработчикам настраивать и контролировать обработку запросов на разных этапах и в различных контекстах приложения, обеспечивая тем самым мощные возможности для управления поведением сервера на всех уровнях его архитектуры.

Механизм обработки событий

Tomcat использует специализированные интерфейсы слушателей для отслеживания разнообразных событий в жизненном цикле веб-приложения. Например:

  • ServletContextListener реагирует на события, связанные с жизненным циклом сервлетного контекста.
  • HttpSessionListener отслеживает события создания и уничтожения сессий.
  • ServletRequestListener занимается событиями, связанными с обработкой запросов к сервлетам.

Каждый из этих интерфейсов предписывает методы, специфичные для обрабатываемых событий.

Регистрация слушателя

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

// Пример в web.xml
<listener>
    <listener-class>com.example.MyServletContextListener</listener-class>
</listener>

// Пример с аннотацией
@WebListener
public class MyServletContextListener implements ServletContextListener {
    // реализация методов
}

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

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

Tomcat: заключение

В этом обзоре я стремился показать основные процессы и компоненты, которые обеспечивают функционирование Tomcat. Комбинация Connector и Container позволяет классифицировать Tomcat как веб-сервер и сервлет-контейнер. Engine обеспечивает инфраструктуру, а декларативный стиль конфигурации (через web.xml и аннотации) в сочетании с автоматическим развертыванием приложений напоминает фреймворк. Однако, как оказалось, использование термина «фреймворк» может быть не совсем уместно, поскольку контейнерная структура Tomcat сильно завязана на своих внутренних механизмах и ограничена в возможностях кастомизации. Все доступные изменения и расширения функциональности основываются на интеграции с Servlet API, что подчеркивает его роль как сервлет-контейнера, а не полноценного фреймворка для разработки.

Servlet API

Контекст разработки веб-приложений на Java

Servlet API состоит из множества интерфейсов, которые задают стандарт:

  • работы с веб-приложениями (в контексте Tomcat);
  • для разработки веб-приложений на Java (в контексте приложения);

В этом обзоре я хочу поговорить о контексте разработки и инструментах, которые есть в распоряжении разработчика java веб-приложений. И скорее на высоком уровне абстракции.

Сервлеты – важная технология, контракт которой заключен в пакете jakarta.servlet. Однако, если мы говорим о контексте разработки, то следует упомянуть родительское пространство имен jakarta. Это основной пакет, который содержит различные спецификации Java EE, открывающих перед разработчиком дополнительную функциональность, которая может быть доступна в рамках приложения и отлично интегрируется с servlet api (jakarta.persistence, jakarta.transaction, jakarta.websocket и другие).

Интерфейс Servlet

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

метод init(ServletConfig): почему не конструктор

При загрузке контекста приложения, компонент Tomcat, известный как Digester, отвечает за сбор конфигурации из XML-файлов и аннотаций, а также за создание экземпляров сервлетов, используя их конструкторы. Однако, в этот момент контекст приложения ещё активно загружается и его инициализация не завершена, что делает невозможным гарантировать полноту и корректность всех конфигурационных данных.

Метод init() вызывается уже после того, как Digester завершает свою работу по инициализации контекста и созданию сервлетов. В этот момент каждому сервлету передаётся объект ServletConfig, содержащий полную конфигурацию сервлета и предоставляющий доступ к ServletContext, который содержит информацию обо всём приложении. Использование метода init() позволяет разработчикам выполнять сложную инициализацию, зависящую от полной конфигурации и контекста приложения, что было бы невозможно при использовании конструктора.

Метод Service(ServletRequest, ServletResponse): А может диспетчер?


Метод service(ServletRequest, ServletResponse) функционирует как центральный диспетчер для входящих запросов, правильно маршрутизируя их на соответствующие методы обработки (doGet(), doPost() и т.д.) в зависимости от типа запроса. Эта механика обеспечивает гибкость и эффективность в обработке запросов

Интерфейс ServletConfig

Предоставляет сервлету конфигурационные параметры, которые определяют его поведение в рамках сервлет-контейнера.

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

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

Следует обратить внимание на важный метод getServletContext(), который возвращает ссылку на веб-контекст в котором он выполняется.

Абстрактный класс GenericServlet

Можно выделить несколько аспектов в назначении этого класса:

Протоколонезависимость: GenericServlet делает сервлеты независимыми от конкретного протокола (например, HTTP), что позволяет их использовать в различных контекстах.

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

Методы логирования: GenericServlet предоставляет удобные методы для логирования, которые действительно являются обертками вокруг метода log() из ServletContext.

Упрощение разработки: Поскольку GenericServlet предоставляет реализации базовых методов интерфейсов Servlet и ServletConfig, разработчику обычно не нужно переопределять все эти методы в каждом сервлете. Это значительно сокращает количество шаблонного кода.

В целом это все что можно о нем сказать.

Интерфейс ServletContext: метаданные нашего приложения или не только?

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

Начальная связь с ServletContext устанавливается через ServletConfig, и любой доступ к ServletContext после инициализации, технически, является доступом через исходный объект ServletConfig, который был предоставлен сервлету.

Хотя ServletContext содержит много информации о конфигурации и состоянии веб-приложения, его роль выходит за рамки простого хранения метаданных. Он активно используется для управления компонентами приложения, такими как сервлеты, фильтры и слушатели событий, что делает его центральным узлом взаимодействия в сервлет-контейнере.

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

Часть методов
  • Работа с атрибутами и параметрами инициализации: управление данными, которые могут быть доступны всем компонентам в приложении.
  • Динамическая регистрация компонентов: добавление и настройка сервлетов, фильтров и слушателей на лету.
  • Управление ресурсами: методы для доступа к содержимому и структуре веб-приложения.
  • Диспетчеризация запросов: возможность перенаправлять запросы внутри приложения для обработки различными компонентами.
  • Логирование и информация о сервере: инструменты для записи журналов и получения информации о сервлет-контейнере и других параметрах окружения.

ServletContext API предоставляет механизмы для конфигурации приложения, управления его составными частями и доступа к ресурсам, что делаего его важным элементом сервлет-архитектуры.

ServletRequest – директива от клиента

ServletRequest – это интерфейс, представляющий директиву от клиента. Он содержит всю информацию о запросе. Обращаясь к интерфейсу Servlet, мы видим, что метод service() принимает объект именно этого типа. Важно отметить, что фактически передаваемым аргументом является объект org.apache.catalina.connector.Request. Этот объект служит оберткой над сыроватым org.apache.coyote.Request и реализует интерфейс jakarta.servlet.http.HttpServletRequest. Таким образом, мы уже на этапе передачи запроса в Engine подстелили себе соломку для работы с HTTP-протоколом.

Часть методов

Переходя к API ServletRequest, мы видим, что он включает множество методов, которые можно классифицировать по следующим категориям:

  • Информация о запросе
  • Содержимое и кодировка
  • Атрибуты и параметры
  • Асинхронная обработка
  • Локализация
  • Различные вспомогательные методы

Заметим, что некоторые методы имеют идентичные сигнатуры с методами в ServletContext, например, setAttribute(String, Object). Однако, есть существенные различия при их использовании:

ServletRequest: Жизненный цикл ограничен временем обработки одного запроса (Request Scope), после чего данные очищаются.

ServletContext: Существует на протяжении всего времени работы приложения (Application Scope), и данные сохраняются до явного удаления или перезапуска сервера.

Области видимости (Scopes)

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

Тикеты на концерт: Когда пользователь ищет доступные места на концерт, данные о доступных местах хранятся только во время обработки его запроса. Это информация временная, её сохранение между запросами не требуется, так как она может быстро устареть из-за бронирований другими пользователями.

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

Форум: На форуме может быть функция “Цитата дня”, которая отображается всем пользователям. Эта цитата обновляется один раз в день и хранится в Application Scope, таким образом, каждый пользователь видит одну и ту же цитату в течение дня независимо от своей сессии.

В Apache Tomcat существуют различные области видимости (scopes), которые определяют, как и где данные будут храниться, и откуда они будут доступны:

Request Scope:

  • Область действия: Данные доступны в рамках одного HTTP-запроса.
  • Использование: Хранение атрибутов, которые необходимы только во время обработки данного запроса. Это самая кратковременная область видимости.

Session Scope:

  • Область действия: Данные сохраняются в течение нескольких взаимосвязанных HTTP-запросов от одного и того же пользователя.
  • Использование: Хранение информации о пользовательской сессии, например, данные учетной записи пользователя, предпочтения, состояние корзины покупок и т.д.

Application Scope:

  • Область действия: Данные доступны всему веб-приложению, независимо от конкретного пользователя или сессии.
  • Использование: Хранение глобальных настроек и данных, таких как списки конфигураций, данные, доступные всем пользователям и сессиям.

Page Scope (в контексте JSP):

  • Область действия: Данные доступны в рамках одной JSP-страницы.
  • Использование: Это наименее часто используемый scope, применимый к данным, которые не должны быть доступны вне страницы, где они были созданы.

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

ServletResponse

Часть методов

ServletResponse является интерфейсом, который используется сервлетами для отправки ответов клиенту. Объект ServletResponse создаётся сервлет-контейнером (через org.apache.catalina.connector.Connector) и сопровождает запрос (ServletRequest) на протяжении всего цикла обработки.

Основные Методы

setLocale(Locale): Устанавливает локаль для ответа, что может влиять на форматирование данных (например, даты, числа) в соответствии с региональными настройками.

getWriter(): Возвращает объект PrintWriter, который используется для отправки текстового содержимого клиенту. Этот метод идеально подходит для ответа с текстовыми данными, такими как HTML, XML или простой текст.

getOutputStream(): Возвращает ServletOutputStream, предназначенный для отправки бинарных данных клиенту. Это может быть использовано для вывода изображений, файлов PDF или любого другого типа двоичных данных.

flushBuffer(): Очищает все содержимое из буфера ответа в клиент, гарантируя, что все данные, отправленные до вызова этого метода, были получены клиентом.

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

setCharacterEncoding(String): Устанавливает кодировку символов для контента ответа, что важно для корректного отображения текстовых данных клиентом.

setContentType(String): Определяет MIME-тип ответа, например text/html для HTML-страниц или application/json для JSON данных.

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

RequestDispatcher – местный связной

Сущность этого типа реализует внутренний механизм, который перенаправляет запрос на любой другой ресурс на сервере, будь то сервлет, HTML или JSP-файл. RequestDispatcher имеет два основных метода: forward и include.

Метод Forward

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

Как работает метод forward

Метод forward используется для полного перенаправления обработки запроса от одного ресурса (сервлета, JSP или HTML файла) к другому на сервере. Этот процесс состоит из нескольких ключевых шагов:

  1. Прекращение текущего ответа: Если текущий сервлет начал отправку ответа (заголовки и тело уже частично отправлены), вызов forward приведёт к исключению IllegalStateException. Это связано с тем, что forward предполагает полную передачу контроля другому ресурсу, который начнёт формирование нового ответа с чистого листа.
  2. Передача запроса: ServletRequest и ServletResponse передаются новому ресурсу. Любой вывод, который уже был помещен в буфер ответа, но еще не отправлен, будет очищен. Это обеспечивает, что новый ресурс может начать генерировать ответ, не беспокоясь о предыдущих выводах.
  3. Обработка целевым ресурсом: Целевой ресурс обрабатывает переданный запрос и генерирует ответ независимо от того, что делал исходный сервлет. Контекст запроса (например, атрибуты запроса) и параметры могут быть изменены или добавлены, чтобы соответствовать потребностям нового ресурса.
  4. Завершение обработки: После того как целевой ресурс закончит обработку запроса и отправит ответ, процесс forward завершается. Контроль не возвращается исходному сервлету, и любые действия после вызова forward в исходном сервлете не будут выполняться.
Пример использования метода forward
RequestDispatcher dispatcher = request.getRequestDispatcher("nextServlet"); dispatcher.forward(request, response);

RequestDispatcher получается для ресурса nextServlet. Метод forward затем используется для передачи запроса и ответа этому новому сервлету, который полностью берёт на себя обработку запроса.

Метод Include

Используется для вставки содержимого одного ресурса (такого как другой сервлет, JSP-файл или HTML-файл) в ответ, который генерируется текущим сервлетом. Это позволяет сервлету дополнить свой собственный ответ данными, полученными от других ресурсов, без перенаправления или прерывания обработки текущего запроса.

Как работает метод include

Когда сервлет вызывает метод include объекта RequestDispatcher, содержимое целевого ресурса вставляется прямо в текущий поток ответа сервлета. Это осуществляется таким образом:

  1. Сохранение контекста: Метод include сохраняет текущее состояние ответа, такое как тип содержимого и кодировка, прежде чем вставить ответ от вызываемого ресурса.
  2. Вставка содержимого: Вызываемый ресурс обрабатывает тот же самый запрос, что и исходный сервлет, и пишет свой вывод непосредственно в поток ответа исходного сервлета.
  3. Восстановление состояния: После завершения выполнения метода include, первоначальное состояние ответа восстанавливается, так что исходный сервлет может продолжить выводить в ответ без каких-либо прерываний или изменений в его заголовках и настройках.
Пример использования метода include

Допустим, у вас есть сервлет, который генерирует HTML-страницу, и вы хотите включить в неё содержимое другого JSP-файла, который генерирует пользовательское меню. Вы можете использовать include для вставки этого меню непосредственно в ответ сервлета:

<div>
    <% 
        RequestDispatcher dispatcher = request.getRequestDispatcher("/menu.jsp");
        dispatcher.include(request, response);
    %>
</div>

Этот код получает RequestDispatcher для JSP-файла /menu.jsp и затем вызывает include, передавая текущие объекты запроса и ответа. JSP-файл обрабатывается, и его вывод вставляется в поток ответа исходного сервлета.

ServletContextListener extends EventListener

Этот интерфейс предназначен только для реагирования на события инициализации и завершения жизненного цикла контекста приложения.

Инициализировать ресурсы при старте:

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

Освобождать ресурсы при завершении:

  • Когда приложение останавливается или перезапускается, метод contextDestroyed() вызывается для выполнения операций по очистке, таких как закрытие соединений с базами данных, остановка запущенных фоновых процессов или таймеров и удаление временных файлов.

Мой опыт использования ServletContextListener заключался в централизованной инициализации и конфигурации всего веб-приложения при его запуске. Моя основная задача была удостовериться, что все необходимые компоненты, такие как сервисы, DAO, мапперы и валидаторы, были инициализированы корректно и доступны в контексте всего приложения. Это избавило меня от необходимости использовать жёсткое кодирование настроек или паттерн “синглтон” для управления экземплярами.

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

Подпакет jakarta.servlet.http

Подпакет jakarta.servlet.http расширяет функциональность базового сервлета, представленного абстрактным классом GenericServlet из пакета jakarta.servlet. GenericServlet формирует основу для создания сервлетов, устанавливая базовые контракты между сервлетом и сервлет-контейнером.

Однако большинство сервлетов используют HTTP-протокол, что требует более специфичных инструментов и интерфейсов, чем те, что предлагает GenericServlet. Для этой цели был создан подпакет jakarta.servlet.http, который предоставляет расширенные возможности для работы с HTTP. Этот подпакет включает различные классы и интерфейсы, которые определяют правила взаимодействия между сервлетами на HTTP и серверным окружением. Эти компоненты помогают разработчикам более глубоко интегрировать функциональность HTTP в их веб-приложения.

Одним из ключевых классов в этом пакете является HttpServlet.

HttpServlet – меньше абстракции, больше HTTP.

HttpServlet — это абстрактный класс, расширяющий GenericServlet и предназначенный для обработки HTTP-запросов. Если сервлет ориентирован на HTTP, то он должен наследоваться от HttpServlet.

Метод service в HttpServlet

Метод service(ServletRequest, ServletResponse) служит центральной точкой входа в сервлет. Он проверяет тип запроса и преобразует ServletRequest в HttpServletRequest, что подразумевает, что используется стандартный коннектор для HTTP/1.1 или HTTP/2. А реальный объект скрывающийся за интерфейсом является org.apache.catalina.connector.Request расширяющий HttpServletRequest.

Метод service (перегруженная версия)

Также для наглядности doGet(req, resp):

Какой вывод следует?

  • Метод service() не выполняет никакой собственной логики обработки запросов, а лишь делегирует их обработку в зависимости от HTTP-метода.
  • Для настройки сервлета под конкретные нужды приложения разработчику следует переопределить методы, соответствующие поддерживаемым HTTP-методам.
  • Непереопределённые методы будут продолжать возвращать 405 Method Not Supported, что является стандартным поведением для методов, которые не были явно реализованы в сервлете.
Default Servlet

DefaultServlet — специальный сервлет Tomcat для обработки статических файлов. Он настраивается в web.xml и может быть переопределён для изменения обработки запросов к статическим ресурсам:

Я не видел это в спецификации Servlet API, поэтому был в замешательстве когда столкнулся с загадочным поведением в ответ на мои запросы. Выполнив операцию “копать” – я нашел DefaultServlet.

Проблемы Поведения По Умолчанию

По умолчанию он настроен обслуживать все запросы от корневого пути. Любой запрос на неопределённый ресурс будет обслужен DefaultServlet (как запрос на статический файл). Что приведет к следующей ситуации:

  • GET, POST, HEAD приведут к методу serveResource(), что вернет правильный вариант ответ 404 Not Found.
  • PUT, DELETE – не используют serveResource(), так как они не предполагают возврат результата и они просто вернут 404 Method Not Allowed.
  • PATCH – этот метод не поддерживается HttpServlet и вернет 501 Not Implemented.

Несколько неявное поведение, учитывая что для несуществующего ресурса хотелось бы видеть 404 Not Found. Думаю лучшим вариантом будет переопределить default сервлет и связать его c папкой /static.

// default mapping example
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

// custom mapping example
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/static/*</url-pattern>
</servlet-mapping>
Заголовки с Чувствительной Информацией

Давайте познакомимся с “sensitive information” headers. В HttpServlet есть следующее статическое финальное поле:

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

  • authorization: Этот заголовок используется для передачи учетных данных (например, токенов доступа или базовой аутентификации) для аутентификации пользователя.
  • cookie: Заголовок cookie используется для передачи данных о сессии или другой информации, которая сохраняется на клиентской стороне и отправляется на сервер при каждом запросе.
  • x-forwarded: Этот заголовок часто используется для передачи информации о первоначальном IP-адресе клиента, особенно в случае, когда запросы проходят через прокси-серверы или балансировщики нагрузки.
  • forwarded: Заголовок forwarded передает информацию о первоначальном клиенте и прокси-серверах, через которые прошел запрос.
  • proxy-authorization: Этот заголовок используется для передачи учетных данных для аутентификации клиента на прокси-сервере.

Следовательно эти данные будут должным образом обработаны или скрыты за маской в логах и сообещениях об ошибках.

HttpServletRequest

Request в контексте пакета connector

HttpServletRequest — это интерфейс в Java Servlet API, который расширяет базовый интерфейс ServletRequest, добавляя функциональность, специфическую для HTTP. Этот интерфейс обеспечивает разработчиков инструментами для управления информацией о входящих HTTP-запросах.

Контекст использования в Tomcat

Когда сервер Tomcat получает HTTP-запрос, он использует специальный коннектор для управления деталями протокола. Коннектор создает объект Request, который реализует интерфейс HttpServletRequest и выступает в роли поставщика информации о запросе. Именно этот объект скрывается за интерфейсами при работе с request, с которым взаимодействуют разработчики для получения данных запроса.

Основные возможности

Интерфейс HttpServletRequest предоставляет методы, которые помогают работать с данными типичными для HTTP-запроса, например:

Новые возможности интерфейса HttpServletRequest
  • Аутентификация пользователя: Методы для получения информации об аутентифицированном пользователе, такие как getRemoteUser() и getUserPrincipal().
  • Управление сессиями: Методы для работы с HTTP-сессиями, такие как getSession().
  • Куки: Методы для получения куков, отправленных с запросом, например, getCookies().
  • Параметры запроса: Методы для доступа к параметрам запроса, такие как getParameter(String) и getParameterMap().
  • Заголовки запроса: Методы для получения заголовков HTTP, такие как getHeader(String) и getHeaderNames().
  • Информация о запросе: Методы для получения подробной информации о запросе, включая URL, URI запроса, строку запроса и контекстный путь.
Методы для работы с URL

Часто возникает неопределенность в понимании что возвращают методы getRequestURI(), getRequestURL(), getQueryString(), getContextPath(). Рассмотрим следующую картинку:

Структура URL и её компоненты

  • Протокол: Определяет правила передачи данных. Примеры: HTTP, HTTPS, FTP.
    • Пример: HTTPS гарантирует защищенное соединение.
  • Хост: Доменное имя или IP-адрес сервера.
    • Пример: ale-os.com или localhost для локальной разработки.
  • Контекстный путь: Путь к корневой директории веб-приложения на сервере.
    • Пример: /myapp в Tomcat направляет к приложению, размещенному в директории /webapps/myapp/.
  • Ресурс: Конкретный файл или динамический ресурс для обработки.
    • Пример: /articles может указывать на сервлет, который работает со статьями.
  • Строка запроса: Дополнительные параметры, передаваемые серверу.
    • Пример: ?include=servlet&exclude=javax передает параметры include и exclude.

HttpServletRequest предоставляет метод getRequestURL(), который вернет весь URL, но без строки запроса (части которая идет после “?”).

HttpServletResponse

Response в контексте пакета connector

Интерфейс HttpServletResponse предоставляет сервлетам возможности для отправки HTTP-ответов клиентам. Он расширяет базовый интерфейс ServletResponse и включает специфические для HTTP функции, такие как управление статусными кодами, заголовками ответов и cookies.

Контекст использования в веб-приложениях

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

Основные возможности

Новые возможности можно классифицировать в несколько категорий:

  • Управление статусом ответа:
    • setStatus(int): Устанавливает статусный код ответа (например, 200 для успеха, 404 для “Не найдено”).
    • sendError(int, String): Отправляет статусную ошибку с описанием, что позволяет клиентам понимать причину ошибки.
  • Управление заголовками ответа:
    • setHeader(String, String): Устанавливает значение HTTP-заголовка. Если заголовок уже существует, его значение заменяется.
    • addHeader(String, String): Добавляет HTTP-заголовок к ответу. Если заголовок уже существует, добавляется новое значение к уже существующим.
  • Управление cookies:
    • addCookie(Cookie): Добавляет cookie к ответу. Это используется для управления состоянием сессии или для хранения пользовательских предпочтений на стороне клиента.
  • Перенаправление запросов:
    • sendRedirect(String): Осуществляет перенаправление клиента на другой URL. Этот метод часто используется в процессах аутентификации или при перенаправлении на новую страницу после выполнения какой-либо операции.
Новые возможности интерфейса HttpServletResponse

Примеры использования HttpServletResponse:

  1. Установка статусного кода: response.setStatus(HttpServletResponse.SC_NOT_FOUND);
    Этот метод устанавливает статус ответа 404, что означает, что запрашиваемый ресурс не найден.
  2. Добавление заголовка:
    response.setHeader("Content-Type", "text/html; charset=UTF-8");
    Это устанавливает тип содержимого ответа, сообщая браузеру, что следует ожидать HTML-документ в кодировке UTF-8.
  3. Отправка cookie:
    Cookie userCookie = new Cookie("user", "12345"); response.addCookie(userCookie);
    Создает и отправляет cookie с идентификатором пользователя, что может быть использовано для управления сессиями или персонализации.
  4. Перенаправление:
    response.sendRedirect("http://www.example.com");
    Осуществляет перенаправление пользователя на указанный URL, часто используется для переадресации после успешной обработки формы или аутентификации.
Заключение

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

HttpServletRequestWrapper и HttpServletResponseWrapper

Эти классы предоставляют готовые шаблоны для создания своих обёрток, которые наследуют методы и функциональность базовых интерфейсов (HttpServletRequest и HttpServletResponse).

Он позволяют переопределить стандартные методы или добавить дополнительную логику используя базовые интерфейсы для их транспортировки. Правда есть и недостаток в виде потребности в обратном приведении типов.

Пример, добавления метода для удобной отправки JSON-ответа:

public class CustomHttpServletResponseWrapper extends HttpServletResponseWrapper {

    public CustomHttpServletResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    public void sendJsonResponse(Object object) throws IOException {
        getResponse().setContentType("application/json");
        getResponse().setCharacterEncoding("UTF-8");
        PrintWriter out = getResponse().getWriter();
        out.write(new Gson().toJson(object)); 
        out.flush();
    }

    // ...
}

Слушатели

HttpSessionListener

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

Назначение: Отслеживает события создания и уничтожения HTTP-сессии.

Методы:

  • void sessionCreated(HttpSessionEvent se): Вызывается, когда сессия создается.
  • void sessionDestroyed(HttpSessionEvent se): Вызывается, когда сессия уничтожается.

Пример использования:

  • Отслеживание количества активных сессий.
  • Инициализация ресурсов для пользователя при создании сессии.
  • Освобождение ресурсов при завершении сессии.
HttpSessionAttributeListener

Назначение: Отслеживает изменения атрибутов в сессии (добавление, удаление, замена).

Методы:

  • void attributeAdded(HttpSessionBindingEvent event): Вызывается при добавлении атрибута в сессию.
  • void attributeRemoved(HttpSessionBindingEvent event): Вызывается при удалении атрибута из сессии.
  • void attributeReplaced(HttpSessionBindingEvent event): Вызывается при замене атрибута в сессии.

Пример использования:

  • Логирование изменений данных пользователя.
  • Реакция на изменение критических данных.
HttpSessionActivationListener

Назначение: Отслеживает события активации и пассивации сессии (процессы сериализации и десериализации сессии, например, при перемещении сессии между серверами в кластерной среде).

Методы:

  • void sessionWillPassivate(HttpSessionEvent se): Вызывается перед тем, как сессия будет пассивирована (сериализована).
  • void sessionDidActivate(HttpSessionEvent se): Вызывается после активации сессии (десериализации).

Пример использования:

  • Подготовка объекта к сериализации, например, закрытие временных ресурсов.
  • Восстановление состояния объекта после десериализации.
HttpSessionIdListener

Назначение: Отслеживает изменение идентификатора сессии (например, при выполнении session.invalidate() и последующем создании новой сессии).

Методы:

  • void sessionIdChanged(HttpSessionEvent event, String oldSessionId): Вызывается при изменении идентификатора сессии.

Пример использования:

  • Обновление идентификаторов сессий в базе данных или других местах, где отслеживаются идентификаторы сессий.
HttpSessionBindingListener

Назначение: Отслеживает добавление и удаление объектов как атрибутов в сессии. Объекты, реализующие этот интерфейс, могут реагировать на добавление или удаление себя из сессии.

Методы:

  • void valueBound(HttpSessionBindingEvent event): Вызывается, когда объект добавляется в сессию.
  • void valueUnbound(HttpSessionBindingEvent event): Вызывается, когда объект удаляется из сессии.

Пример использования:

  • Логирование.
  • Выполнение дополнительных действий при добавлении или удалении объекта в/из сессии.

В пакете http, также, существуют две реализации EventObject: HttpSessionEvent и HttpSessionBindingEvent.

  • HttpSessionEvent: Основная задача HttpSessionEvent — содержать ссылку на объект HttpSession, связанный с событием. Когда сессия создается или уничтожается, контейнер (например, Tomcat) создает экземпляр HttpSessionEvent и передает его слушателю, который реализует интерфейс HttpSessionListener.
  • HttpSessionBindingEvent: Этот класс используется для событий, связанных с добавлением, удалением или заменой атрибутов сессии. Он применяется в контексте интерфейсов HttpSessionAttributeListener и HttpSessionBindingListener. Когда атрибут добавляется, удаляется или заменяется в сессии, контейнер создает экземпляр HttpSessionBindingEvent и передает его соответствующему слушателю (HttpSessionAttributeListener). Слушатель может использовать методы getName() и getValue() для получения информации об измененном атрибуте и выполнения необходимой логики.

HttpFilter

HttpFilter — это базовый абстрактный класс для создания фильтров.

Этот класс упрощает разработку фильтров, представляя собой улучшенную версию GenericFilter. В HttpFilter автоматически приводятся объекты ServletRequest и ServletResponse к типам HttpServletRequest и HttpServletResponse соответственно. Это делается для того, чтобы разработчик мог работать непосредственно с HTTP-специфичными методами, такими как getHeader, getCookies, setStatus, и т.д.

public abstract class HttpFilter extends GenericFilter {
    @Override
    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        // Приведение типов
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // Вызов абстрактного метода, который должен реализовать разработчик
        doFilter(httpRequest, httpResponse, chain);
    }

    // Абстрактный метод, который нужно реализовать в подклассах
    protected abstract void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException;
}


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

Cookie

Куки представляет собой маленький фрагмент информации, которую сервер отправляет браузеру. Браузер может может сохранить его и передавать серверу при каждом запросе. Таким образом, можно сохранять информации о состоянии (взаимодействие клиента и сервера) Особенно может быть полезно при взаимодействии с stateless серверами. Хотя куки также широко используются с сессиями, позволяя идентифицировать конкретного пользователя и подтянуть связанную информацию.

В Томкат, например, по умолчанию, если браузер не передал такую куки как SessionId она будет сгенерирована автоматически и передана при ответе. Даже если вы работаете stateless режиме и не используете сессии.

Куки часто используются для:

  • Трекинга (отслеживания поведения пользователей)
  • Управления сеансом (логины, корзины для виртуальных покупок)
  • Персонализации (пользовательские предпочтения)

Сервлеты отправляют куки, используя метод addCookie(cookie) класса HttpServletResponse, который добавит заголовок “Set-Cookie”. Браузер обычно поддерживает до 50 куки для каждого домена и не более 3000 куки в общем, при этом размер каждого куки обычно не превышает 4 КБ. Куки, которые запомнил браузер пересылаются обратно серверу под одним заголовком “Cookie”. Обратная сторона использования куки – это накладные расходы на их пересылку, что может значительно ухудшать производительность.

Сессионные cookie

Такие куки не имеют аттрибута “Expires” или “Max-Age“, которые определяют время интервала временя в котором куки считается валидным. Свойство сессионных куки – это то, что они должны как бы удаляться при закрытии клиента, но почти всегда в современных браузерах включено автоматическое восстановление сеанса, так что они вероятнее всего будут восстановлены.

Set-Cookie: theme=light
Постоянные Cookie

Удаляются при наступлении определенной даты Expires или некоторого времени Max-Age

Set-Cookie: user_id=xyz123; Expires=Wed, 21 Oct 2025 07:28:00 GMT;

Другие атрибуты безопасности куки:

  • Secure: Такие куки передаются только через HTTPS.
  • HttpOnly: К таким куки невозможно получить доступ из JavaScript.
  • SameSite: Помогает предотвратить CSRF атаки, контролируя отправку куки с межсайтовыми запросами.
  • Domain: Ограничивает отправку куки на определенный домен. Если он не указан, то куки будут доступны только для того домена, который их создал, не включая поддомены (example.com). Если этот аттрибут задан Domain=example.com, то куки будет доступно для создавшего домена и его поддоменов (example.com, blog.example.com).
кэширование:
  1. HTTP 1.0 и кэширование: В протоколе HTTP версии 1.0 не предусмотрены сложные механизмы для управления кэшированием содержимого, зависящего от куки. Если страница использует куки, то часто рекомендуется не кэшировать такие страницы, потому что содержимое может быть специфично для каждого пользователя.
  2. HTTP 1.1 и управление кэшем: В HTTP 1.1 введены более продвинутые заголовки для управления кэшем, такие как Cache-Control. Эти заголовки позволяют более тонко настраивать кэширование, например, указывая, что некоторые ответы можно кэшировать, а некоторые — нет. Однако использование куки по-прежнему может ограничивать возможности кэширования из-за уникальности данных. Класс Cookie сам по себе (в контексте Java Servlet API) не предоставляет средств для управления кэшем на уровне HTTP 1.1, что означает, что разработчику может потребоваться самостоятельно управлять заголовками кэширования в ответах, когда используются куки.
public class SessionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Установка куки сессии
        HttpSession session = request.getSession(true);
        Cookie sessionCookie = new Cookie("session_id", session.getId());
        response.addCookie(sessionCookie);

        // Установка заголовков для предотвращения кэширования
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); // HTTP 1.1.
        response.setHeader("Pragma", "no-cache"); // HTTP 1.0.
        response.setDateHeader("Expires", 0); // Прошедшая дата для истечения срока годности.

        // Отправка пользовательской информации
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().println("<h1>Ваша корзина</h1>");
       
    }
}
Заключение:

Cookies — это способ передачи данных между клиентом и сервером, обычно в контексте сессий, но не сами сессии. Стандартный подход — хранение сессий на сервере и использование cookies для передачи Session ID. Альтернативные методы, такие как JWT, могут полностью заменить серверные сессии, при этом также могут использовать cookies, но для других целей, например, для хранения самого токена.

HttpSession

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

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

Кто создает объект HttpSession?

Создание объекта, реализующего интерфейс HttpSession, происходит на стороне Tomcat. Когда пользователь впервые взаимодействует с веб-приложением, сервер создает новую сессию, присваивает ей уникальный идентификатор (Session ID) и отправляет его клиенту в виде cookie. Браузер клиента сохраняет этот идентификатор и включает его в каждый последующий запрос к серверу. Это позволяет серверу идентифицировать сессию и извлечь соответствующие данные для обработки текущего запроса.

Ограничения и особенности использования

Хотя часто говорят о клиенте и сервере, сессия в основном предназначена для взаимодействия между браузером и сервером. HttpSession не поддерживает многопоточность. Это означает, что одновременный доступ к одной и той же сессии с разных устройств или параллельные запросы с одного устройства могут привести к проблемам с конкурентным доступом (race conditions).

Внутренний интерфейс Session в Tomcat

Tomcat также содержит внутренний интерфейс Session, который используется для управления сессиями на уровне контейнера и не является частью стандартного сервлетного API. Класс StandardSession, который реализует интерфейс HttpSession, также реализует внутренний интерфейс org.apache.catalina.Session. Однако для работы с сессиями в веб-приложениях обычно достаточно использовать стандартный интерфейс HttpSession из Servlet API.

Клиент и работа с сессиями

В некоторых случаях клиент может не захотеть присоединяться к сессии, например, если файлы cookie отключены. В таких ситуациях, пока клиент не присоединится к сессии и не предоставит валидный Session ID, сервер будет создавать новую сессию при каждом запросе и отправлять её идентификатор в виде cookie. При этом метод isNew() всегда будет возвращать true, так как каждый запрос будет ассоциироваться с новой сессией.

Замечание: Объект сессии валиден и сохраняет данные между запросами, однако его использование вне контекста HTTP-запроса не рекомендуется. Например, хранение ссылки на объект HttpSession в переменных класса между запросами может привести к ошибкам и некорректному поведению приложения. Поэтому работу с HttpSession следует ограничивать только в пределах текущего HTTP-запроса.

Время жизни сессии и ее истечение

У сессии есть время жизни, по истечении которого она автоматически завершится, если не будет активности. Это время можно настроить с помощью метода setMaxInactiveInterval(int seconds) или в конфигурации веб-приложения. Если сессия истекла, HttpSession будет недоступен, и сервер создаст новую сессию при следующем запросе.

JSP – Jasper

Apache Tomcat является эталонной реализацией Servlet API и включает в себя поддержку JavaServer Pages (JSP) через интегрированную библиотеку Jasper. Грубо говоря, Jasper представляет собой конвертер, который преобразует JSP-файлы содержащие HTML и Java код, в код Java сервлеты

Основная ценность Jasper заключается в его способности упрощать процесс разработки. Разработчики могут сосредоточиться на структуре и дизайне веб-страниц, используя стандартные HTML-теги и вставляя Java код там, где это необходимо для обеспечения динамичности и интерактивности. В результате, JSP как технология облегчает разработку веб-приложений, предоставляя мощные средства для интеграции бизнес-логики и пользовательского интерфейса.


Компиляция JSP в сервлет

  • Когда JSP страница запрашивается в первый раз, JSP контейнер автоматически компилирует её в Java сервлет. Этот процесс включает в себя преобразование всех JSP тегов, скриптлетов и выражений в Java код.
  • Полученный Java код сервлета затем компилируется в байт-код JVM (Java Virtual Machine), который может быть выполнен сервером.

Исполнение сервлета

  • После компиляции, страница JSP обрабатывается как обычный сервлет. Сервлет контейнер обрабатывает запросы к этому сервлету, выполняет скомпилированный код и возвращает результат обратно клиенту.
  • Вся бизнес-логика, определенная в JSP (через JSP теги, скриптлеты, или включенные файлы), исполняется в этом процессе.

Жизненный цикл JSP

  • Загрузка: JSP загружается в контейнер при первом запросе или при старте сервера, если это настроено.
  • Инициализация: При первом запросе JSP компилируется в сервлет и инициализируется.
  • Обработка запросов: Компилированный сервлет обрабатывает входящие запросы и генерирует HTML или другой ответ.
  • Уничтожение: Сервлет может быть уничтожен контейнером при его остановке или перезагрузке.

Пользовательские теги

JSTL (JSP Standard Tag Library)



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

Заметки

  • Сервлет является синглтоном: Инициализируется методом init() и завершает работу методом destroy(). Это означает, что один экземпляр сервлета обрабатывает все запросы, что требует особого внимания к потокобезопасности.
  • Потокобезопасность: Методы сервлетов вызываются для каждого запроса у синглтона. Используйте синхронизацию или другие механизмы для защиты общих ресурсов и предотвращения гонок.
  • Фильтры: Вынесите общую логику в фильтры (например, логирование, аутентификацию). Определите фильтры в web.xml или используйте аннотацию @WebFilter для их настройки.
  • Обработка ошибок: Настройте страницы ошибок в web.xml с помощью элементов <error-page>, и обрабатывайте исключения внутри сервлетов, используя подготовленные страницы ошибок.
  • Оптимизация производительности: Оптимизация производительности: Используйте HTTP-заголовки (Cache-Control, Expires) для кэширования и снижения нагрузки на сервер. Например, установите Cache-Control: max-age=3600 для кэширования ресурсов на один час.
  • Используйте аннотации: Аннотации (@WebServlet, @WebFilter, @WebListener) удобнее для конфигурации сервлетов, фильтров и слушателей, так как они позволяют управлять настройками непосредственно в коде и уменьшают зависимость от web.xml. Однако использование аннотаций имеет свои минусы: у нас нет централизованного места с настройками, что может затруднить управление конфигурацией в больших проектах.
  • Понимание HTTP-заголовков: Правильно указывайте тип содержимого в ответах (например, Content-Type: application/json для JSON-ответов) для корректной интерпретации данных клиентом.
  • CORS: Управляйте политиками CORS с помощью заголовков (Access-Control-Allow-Origin, Access-Control-Allow-Methods) для контроля доступа к ресурсам с других доменов. Например, установите “Access-Control-Allow-Origin: *” для разрешения доступа с любого домена.
  • MVC-паттерн: Разделяйте логику представления (JSP), контроллера (сервлеты) и модели (Java-классы) для улучшения поддержки и читабельности кода.

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

Брюс Экель

Управление потоками в сервлете — это как жонглирование бензопилами: впечатляюще, когда все идет правильно, но катастрофически, если потеряешь концентрацию.

О сервлетах и многопоточности — Автор неизвестен

Хороший разработчик может написать работающий код. Великий разработчик может написать код, который легко тестировать. Легендарный разработчик пишет сервлеты, которые никогда не падают!

О разработчиках и тестировщиках — Автор неизвестен

Оптимизация: процесс заставить сервлет работать настолько быстро, что никто не сможет понять, как он работает.

Об оптимизации — Автор неизвестен

Заключение

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top