О чем эта статья

В этой статье я хочу кратко пройтись по основным сущностям из спецификации Jakarta Servlet.
Если ты Java-разработчик, то с большой вероятностью ты используешь в работе Spring Framework. Чтобы было интереснее и полезнее, в конце каждого раздела я покажу как используется описываемая сущность в Спринге.

Servlet

Что такое Servlet

Servlet это компонент, который находится под управлением контейнера, отвечающий за обработку запросов. Получается такое разделение ответственности:

Servlet Container

  • Работает с сетью
  • Парсит клиентские запросы/формирует ответы по протоколу HTTP
  • Управляет сервлетами
  • Передает управление конкретному сервлету на основании информации из запроса

Servlet

  • Принимает от контейнера объекты ServletRequest, ServletResponse
  • Использует информацию из ServletRequest для формирования контента в ServletResponse
  • Имеет доступ к своему контексту

Интерфейс Servlet

public interface Servlet {
    void init(ServletConfig config) throws ServletException;  
    ServletConfig getServletConfig();  
    void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;  
    String getServletInfo();  
    void destroy();  
}

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

HttpServlet

HttpServlet — абстрактный класс, упрощающий создание сервлетов для работы с HTTP-запросами.
Сервлеты обрабатывают запросы с помощью метода service(...)
Предлагаю посмотреть на то, как это реализовано в HttpServlet


@Override  
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {  
  
    HttpServletRequest request;  
    HttpServletResponse response;  
  
    try {  
        request = (HttpServletRequest) req;  
        response = (HttpServletResponse) res;  
    } catch (ClassCastException e) {  
        throw new ServletException(lStrings.getString("http.non_http"));  
    }  
    service(request, response);  
}

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
  
    String method = req.getMethod();
	if (method.equals(METHOD_GET)) {  
		// ...
		doGet(req, resp);  
		// ...
	}   
	else if (method.equals(METHOD_HEAD)) {  
	    // ...
	    doHead(req, resp);  
	  
	} else if (method.equals(METHOD_POST)) {  
	    doPost(req, resp);  
	} else if (method.equals(METHOD_PUT)) {  
	    doPut(req, resp);  
	} else if (method.equals(METHOD_DELETE)) {  
	    doDelete(req, resp);  
	} else if (method.equals(METHOD_OPTIONS)) {  
	    doOptions(req, resp);  
	} else if (method.equals(METHOD_TRACE)) {  
	    doTrace(req, resp);  
	} else {  
	    // ...
	    resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);  
	}
}

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
    String msg = lStrings.getString("http.method_get_not_supported");  
    sendMethodNotAllowed(req, resp, msg);  
}

// ...аналогичные методы для других HTTP глаголов

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

Получается удобное API, где мы переопределяем только те глаголы, которые хотим обрабатывать.
Вот пример сервлета, который будет всегда отвечать Hello, world! на GET-запросы

public class HelloServlet extends HttpServlet {  
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {  
        response.setContentType("text/html");  
  
        PrintWriter out = response.getWriter();  
        out.println("<html><body>");  
        out.println("<h1>Hello, world!</h1>");  
        out.println("</body></html>");  
    }  
}

Servlet в Spring

Spring WebMVC использует DispatcherServlet для обработки HTTP запросов.

Filter

Что такое Filter

Перед тем, как запрос попадет в сервлет, он может пройти цепочку “фильтров”.
Это типичный паттерн для вэб-приложений, в некоторых фреймворках это называется middleware (Django, ExpressJS)

Вот так выглядит интерфейс фильтра

public interface Filter {  
    default void init(FilterConfig filterConfig) throws ServletException {  
    }  
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)  throws IOException, ServletException;  
    default void destroy() {  
    }  
}

Фильтры используют метод doFilter(...) для обработки запросов. Тут стоит обратить внимание на FilterChain в параметрах метода doFilter.
Контейнер сервлетов собирает цепочку фильтров на основании информации из web.xml, либо сканируя классы с аннотацией @WebFilter

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

public class HelloWorldFilter extends HttpFilter {

    public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        // код, который будет выполнен до того, как мы продвинемся дальше по цепочке
        // FilterChain строит сам контейнер сервлетов, объединяя фильтры, которые нацелены на один и тот же url
        chain.doFilter(req, res);
        // код, который будет выполнен после того, как последующие фильтры (а если повезет то и сам сервлет) выполнят свою логику
    }
}

Если с сервлетами все просто, то тут есть особенность: нам нужно двигаться дальше по цепочке, либо останавливать обработку запроса на текущем звене.

Filter в Spring

  • Модуль Spring Web использует ряд фильтров для обработки запросов, например CorsFilter.
  • Модуль Spring Security предоставляет (помимо фильтров) удобное API для построения своей цепочки фильтров - SecurityFilterChain. Эта цепочка автоматически встраивается в основную цепочку фильтров.

ServletContext

ServletContext как общее состояние web-приложения

ServletContext содержит информацию о всех ресурсах web-приложения, а также хранит общее глобальное состояние.
Некоторые параметры контекста можно задать в web.xml, либо контекст можно сконфигурировать программно, с помощью ServletContextListener или ServletContainerInitializer

Объекты запросов имеют ссылку на ServletContext, следовательно они могут полноценно использовать это глобальное состояние: как для чтения, так и для записи.

Динамическая регистрация Servlet, Filter, EventListener

ServletContext позволяет динамически регистрировать сервлеты, фильтры и слушатели эвентов (с ними можно ознакомиться в спецификации)

Настройка ServletContext в Spring

Spring Web использует WebApplicationInitializer SPI для конфигурации ServletContext.
Это позволяет настраивать контекст программно, не используя web.xml.
Например, DispatcherServlet создается программно (исходники)

Объект запроса

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

Контейнер сервлетов подготавливает объекты ServletRequest и ServletResponse, после чего передает управление сервлету (напрямую или через цепочку фильтров).
Так как мы обрабатываем HTTP-запросы, то мы будем работать с объектами HttpServletRequest и HttpServletResponse.

Помимо информации о самом запросе (headers, URL, method…), в каждом запросе есть атрибуты, которые позволяют хранить состояние в контексте запроса.
Из полезного в объекте запроса еще есть ссылка на объект сессии, а так же на ServletContext.

Использование атрибутов запроса в Spring

Если объявить bean со скоупом REQUEST, то beanFactory попытается получить его из атрибутов запроса (исходники1, исходники2).

Sessions

Что такое сессия

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

В спецификации для этого предусмотрен интерфейс HttpSession

public interface HttpSession {  
    long getCreationTime();  
    String getId();  
    long getLastAccessedTime();  
    ServletContext getServletContext();  
    void setMaxInactiveInterval(int interval);  
    int getMaxInactiveInterval();  
    Object getAttribute(String name);  
    Enumeration<String> getAttributeNames();  
    void setAttribute(String name, Object value);  
    void removeAttribute(String name);  
    void invalidate();  
    boolean isNew();  
}

В этом API есть все нужное для работы с сессиями. Получить доступ к этому объекту можно вызвав у объекта запроса метод getSession() (javadoc) Информация о сессиях хранится в контейнере сервлетов (Пример из Tomcat).

Установление сессии

Один из самых распространенных механизмов для установления сессии (но не единственный) это cookie. Спецификация требует от контейнеров имплементации этого механизма.
Стандартным названием сессионной куки является JSESSIONID

Использование сессий в Spring

  • Spring Security умеет ассоциировать SecurityContext с сессиями (исходники). В Spring Security в принципе есть своя инфраструктура, которая работает с сессиями (доки)
  • Если объявить bean со скоупом SESSION, то beanFactory попытается получить этот бин из атрибутов сессии (исходники)

Конец

Спасибо за внимание! Подписывайтесь на мой Telegram канал чтобы не пропустить новые статьи.