Java Servlets¶
Сервлет - Это серверный Java-компонент, который обрабатывает HTTP-запросы и формирует динамический ответ: HTML, JSON и т.п. - работает внутри контейнера сервлетов: Apache Tomcat, Jetty, GlassFish - один экземпляр сервлета обслуживает много запросов параллельно, каждый в своём потоке - поверх сервлетов построены JSP (см. JSP), JSF и Spring MVC (см. Servlet stack) - в архитектуре Model 2 сервлет - контроллер, а JSP - представление (см. Model 1 и Model 2)
Как сервлет обрабатывает запрос¶
- Клиент отправляет HTTP-запрос.
- Веб-сервер передаёт запрос в контейнер сервлетов, который управляет жизненным циклом сервлетов.
- Контейнер находит сервлет по URL и вызывает его, передавая данные запроса.
- Сервлет обрабатывает запрос и формирует ответ.
- Ответ идёт обратно: сервлет → контейнер → веб-сервер → клиент.
Как создать сервлет¶
- Создать класс, который наследуется от
HttpServlet. - Переопределить
doGet(),doPost()и другие методы для нужных HTTP-методов. - Зарегистрировать сервлет: аннотацией
@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-startupservice()- вызывается на каждый запрос. В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-запросы, вызывает нужный сервлет и отправляет клиенту ответ - обслуживает запросы параллельно, один сервлет - много клиентов одновременно - управляет доступом к ресурсам, аутентификацией и авторизацией - поддерживает сессии
Жизненный цикл сервлета¶
- Запуск сервера. Контейнер стартует и слушает HTTP-запросы на порту.
- Загрузка сервлета. Пришёл первый запрос к сервлету: контейнер загружает его класс. Если класс уже загружен, использует существующий экземпляр.
- Создание экземпляра. Контейнер создаёт один экземпляр и вызывает
init(). - Обработка запроса. Каждый запрос контейнер обрабатывает в потоке из своего пула и вызывает
service(), а он -doGet()илиdoPost(). - Ответ. Сервлет формирует HTTP-ответ, контейнер отправляет его клиенту.
- Завершение. Сервер останавливается или освобождает ресурсы: контейнер вызывает
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 пропадают
| 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.