Перейти к содержанию

Java Servlets

Сервлет - Это серверный Java-компонент, который обрабатывает HTTP-запросы и формирует динамический ответ: HTML, JSON и т.п. - работает внутри контейнера сервлетов: Apache Tomcat, Jetty, GlassFish - один экземпляр сервлета обслуживает много запросов параллельно, каждый в своём потоке - поверх сервлетов построены JSP (см. JSP), JSF и Spring MVC (см. Servlet stack) - в архитектуре Model 2 сервлет - контроллер, а JSP - представление (см. Model 1 и Model 2)


Как сервлет обрабатывает запрос

  1. Клиент отправляет HTTP-запрос.
  2. Веб-сервер передаёт запрос в контейнер сервлетов, который управляет жизненным циклом сервлетов.
  3. Контейнер находит сервлет по URL и вызывает его, передавая данные запроса.
  4. Сервлет обрабатывает запрос и формирует ответ.
  5. Ответ идёт обратно: сервлет → контейнер → веб-сервер → клиент.

Как создать сервлет

  1. Создать класс, который наследуется от HttpServlet.
  2. Переопределить doGet(), doPost() и другие методы для нужных HTTP-методов.
  3. Зарегистрировать сервлет: аннотацией @WebServlet или в web.xml (см. web.xml).
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html; charset=UTF-8");
        response.getWriter().println("<h1>Привет, " + request.getParameter("name") + "</h1>");
    }
}

Методы сервлета

  • init() - контейнер вызывает один раз при инициализации сервлета: при первом запросе к нему или при старте приложения, если задан load-on-startup
  • service() - вызывается на каждый запрос. В HttpServlet он смотрит на HTTP-метод и вызывает нужный doXxx()
  • doGet(HttpServletRequest request, HttpServletResponse response) - обрабатывает GET-запросы
  • doPost(HttpServletRequest request, HttpServletResponse response) - обрабатывает POST-запросы
  • doPut(), doDelete() и другие - по одному на HTTP-метод
  • destroy() - контейнер вызывает при завершении работы сервлета, чтобы освободить ресурсы

Сервлеты, CGI и FastCGI

  • CGI - каждый запрос порождает новый процесс
  • FastCGI - постоянные процессы, каждый обрабатывает много запросов. Работает с любым языком программирования
  • Java-сервлеты - один процесс JVM, один экземпляр сервлета на много запросов, каждый запрос в своём потоке
CGI FastCGI Сервлеты
Единица обработки Новый процесс на запрос Пул постоянных процессов Поток в одной JVM
Стоимость запроса Высокая: запуск процесса Низкая Низкая
Состояние между запросами Нет В памяти процесса В памяти JVM, встроенные HTTP-сессии
Язык Любой Любой Java
Что нужно Веб-сервер Веб-сервер + менеджер процессов Контейнер сервлетов

Плюсы сервлетов: - многопоточность: один экземпляр сервлета обрабатывает много запросов - встроенная поддержка HTTP-сессий - легко переносятся между серверами и платформами

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


Контейнер сервлетов

Контейнер сервлетов - Это серверная платформа для развёртывания и выполнения сервлетов. Она управляет их жизненным циклом и связывает сервлеты с веб-сервером. - примеры: Apache Tomcat, Jetty, GlassFish, WildFly - создаёт, инициализирует и уничтожает сервлеты - принимает HTTP-запросы, вызывает нужный сервлет и отправляет клиенту ответ - обслуживает запросы параллельно, один сервлет - много клиентов одновременно - управляет доступом к ресурсам, аутентификацией и авторизацией - поддерживает сессии


Жизненный цикл сервлета

  1. Запуск сервера. Контейнер стартует и слушает HTTP-запросы на порту.
  2. Загрузка сервлета. Пришёл первый запрос к сервлету: контейнер загружает его класс. Если класс уже загружен, использует существующий экземпляр.
  3. Создание экземпляра. Контейнер создаёт один экземпляр и вызывает init().
  4. Обработка запроса. Каждый запрос контейнер обрабатывает в потоке из своего пула и вызывает service(), а он - doGet() или doPost().
  5. Ответ. Сервлет формирует HTTP-ответ, контейнер отправляет его клиенту.
  6. Завершение. Сервер останавливается или освобождает ресурсы: контейнер вызывает destroy() и удаляет экземпляр.
(load class) → new → init() → service() × N запросов, в разных потоках → destroy()

Note

В исходной версии было «для каждого запроса контейнер создаёт новый поток». Потоки берутся из пула контейнера (в Tomcat - пул worker-потоков) и переиспользуются. Экземпляр сервлета при этом один на все потоки, поэтому изменяемые поля сервлета не потокобезопасны: состояние запроса держи в локальных переменных.


Диспетчеризация запросов

Диспетчеризация - Это передача запроса от одного сервлета другому сервлету или странице.

Forward (внутреннее перенаправление) - запрос передаётся другому сервлету или JSP внутри того же сервера, без участия клиента. - браузер этого не видит, URL в адресной строке прежний - request и его атрибуты те же, их видит получатель

RequestDispatcher dispatcher = request.getRequestDispatcher("/anotherServlet");
dispatcher.forward(request, response);
// include(request, response) - вставить ответ другого ресурса в свой и продолжить

Redirect (внешнее перенаправление) - сервер отвечает кодом 302 и заголовком Location, браузер сам делает новый запрос. - клиент видит новый URL в адресной строке - это новый запрос, атрибуты старого request пропадают

response.sendRedirect("http://www.example.com");
Forward Redirect
Кто делает переход Сервер Браузер, по ответу 302
Запросов Один Два
URL в браузере Прежний Новый
Атрибуты request Сохраняются Теряются
Куда можно Только внутри приложения Куда угодно

См. Redirect и Forward в Spring MVC.


Фильтры

Фильтр - Это объект, который перехватывает запрос до сервлета и ответ после него и может их проверить или изменить. - добавляет сквозную функциональность: аутентификацию, логирование, сжатие, фильтрацию контента, кеширование - фильтры выстраиваются в цепочку, каждый передаёт запрос дальше через chain.doFilter()

Как работает: 1) Клиент отправляет HTTP-запрос. 2) Фильтр перехватывает запрос, проверяет или меняет его. 3) Запрос передаётся сервлету. 4) Сервлет обрабатывает запрос и формирует ответ. 5) Фильтр перехватывает ответ до отправки клиенту и при необходимости меняет его. 6) Ответ уходит клиенту.

Создание фильтра - реализовать интерфейс Filter: - init(FilterConfig filterConfig) - инициализация, вызывается один раз при создании фильтра - doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - логика фильтра, вызывается на каждый запрос - destroy() - вызывается перед удалением фильтра - с Servlet 4.0 init() и destroy() - default-методы, переопределять их нужно, только если есть что делать

@WebFilter("/myServlet")
public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // инициализация фильтра
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("Запрос перед обработкой сервлетом");
        chain.doFilter(request, response);   // дальше по цепочке: следующий фильтр или сервлет
        System.out.println("Ответ после обработки сервлетом");
    }

    @Override
    public void destroy() {
        // освободить ресурсы
    }
}

Примеры использования: - Аутентификация - проверить, вошёл ли пользователь, и при отсутствии доступа перенаправить на страницу логина - Логирование - IP-адрес, время, метод каждого запроса - Кеширование - заголовки HTTP-ответа для управления кешированием - Сжатие - сжать HTTP-ответ, чтобы сэкономить трафик

См. Filters в Spring MVC.


HTTP-сессии

HTTP-сессия - Это состояние пользователя, которое сервер хранит между запросами. HTTP - stateless протокол, и сессия связывает разные запросы с одним пользователем.

HttpSession session = request.getSession();   // взять текущую или создать новую
session.setAttribute("username", "JohnDoe");
String username = (String) session.getAttribute("username");

request.getSession(false);     // только существующую, иначе null
session.setMaxInactiveInterval(1800);   // таймаут неактивности, секунды
session.invalidate();          // завершить сессию, например при логауте

Способы передачи идентификатора сессии. Каждый запрос связывается с сессией по идентификатору (Session ID), так сервер различает пользователей. - Cookie - по умолчанию: сервер ставит cookie JSESSIONID, браузер присылает её в каждом запросе - URL rewriting - идентификатор дописывается в URL: /cart;jsessionid=ABC123. Работает без cookie, но ID виден в адресной строке, логах и заголовке Referer. Ссылки нужно пропускать через response.encodeURL() - SSL-сессия - идентификатор сессии TLS. Используется редко

Способ задаётся в web.xml:

<session-config>
    <session-timeout>30</session-timeout>          <!-- минуты -->
    <tracking-mode>COOKIE</tracking-mode>          <!-- COOKIE, URL, SSL -->
</session-config>

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

ServletContext - Это интерфейс, через который сервлеты работают с веб-приложением целиком. Один на приложение, существует всё время его работы.

Назначение: - Общие данные - атрибуты, доступные всем сервлетам приложения - Параметры конфигурации - <context-param> из web.xml - Логирование - методы log() для сообщений и ошибок приложения - Ресурсы - доступ к файлам внутри приложения: HTML, JSP, текстовые файлы - Обмен между сервлетами - данные и RequestDispatcher для передачи запроса

Как сервлет работает с контекстом:

ServletContext context = getServletContext();
context.setAttribute("sharedData", "This is shared data");
String sharedData = (String) context.getAttribute("sharedData");

String appName = context.getInitParameter("appName");          // <context-param> из web.xml
InputStream in = context.getResourceAsStream("/WEB-INF/data.txt");

Области видимости атрибутов:

Объект Живёт Кому видно
HttpServletRequest Один запрос, включая forward Обработчикам этого запроса
HttpSession Пока активен пользователь Всем запросам одного пользователя
ServletContext Всё время работы приложения Всем сервлетам и всем пользователям

Дескриптор развёртывания web.xml

web.xml - Это файл настройки веб-приложения в WEB-INF/. Часть его заменяют аннотации, когда без него не обойтись - ниже.

<!-- параметр всего приложения: context.getInitParameter("appName") -->
<context-param>
    <param-name>appName</param-name>
    <param-value>My Web Application</param-value>
</context-param>

<!-- сервлет и его параметр: getInitParameter("maxUsers") внутри сервлета -->
<servlet>
    <servlet-name>MyServlet</servlet-name>
    <servlet-class>com.example.MyServlet</servlet-class>
    <init-param>
        <param-name>maxUsers</param-name>
        <param-value>100</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>   <!-- создать при старте, а не при первом запросе -->
</servlet>
<servlet-mapping>
    <servlet-name>MyServlet</servlet-name>
    <url-pattern>/my</url-pattern>
</servlet-mapping>

<!-- фильтр на все URL -->
<filter>
    <filter-name>MyFilter</filter-name>
    <filter-class>com.example.MyFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>MyFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Настройки JSP задаются в <jsp-config>.


Когда web.xml нужен

С Servlet 3.0 (Java EE 6) сервлеты, фильтры и слушатели регистрируются аннотациями @WebServlet, @WebFilter, @WebListener, и web.xml необязателен. Он остаётся нужен, когда: - нужны параметры контекста, настройки сессии, страницы ошибок <error-page>, ограничения безопасности <security-constraint>, <jsp-config> - важен порядок фильтров: у @WebFilter порядок не определён, его задаёт только порядок <filter-mapping> в web.xml - настройки нужно менять без перекомпиляции


JAX-RS и web.xml

JAX-RS работает поверх сервлета: реализация (Jersey, RESTEasy) регистрирует свой сервлет, и он раздаёт запросы ресурсам с @Path.

Без web.xml - современные серверы (WildFly, Payara, TomEE, GlassFish) поддерживают JAX-RS из коробки и находят ресурсы по аннотациям @Path, @GET, @POST. Достаточно класса с @ApplicationPath. Так работает с Java EE 6.

@ApplicationPath("/api")   // общий префикс всех REST-ресурсов
public class RestApplication extends Application {
}

С web.xml - если контейнер не умеет регистрировать JAX-RS сам: старый контейнер до Servlet 3.0 или Jersey без модуля автоматической регистрации (только jersey-container-servlet-core). Тогда сервлет реализации прописывают вручную:

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
    <display-name>My JAX-RS Web App</display-name>

    <servlet>
        <servlet-name>JAX-RS Servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <!-- параметр Jersey: в каких пакетах искать ресурсы -->
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>com.myapp.rest</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>JAX-RS Servlet</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping>
</web-app>

Note

В исходной версии заголовок был http://java.sun.com/xml/ns/javaee, web-app_3_0.xsd, version="3.0" - это Java EE 6. Для Jakarta EE 10 (Servlet 6.0) пространство имён https://jakarta.ee/xml/ns/jakartaee. И регистрация через @ApplicationPath работает с Java EE 6, а не с Java EE 7.