JSP (Jakarta Server Pages, до 2019 года — JavaServer Pages) — это технология серверных шаблонов для Java: файл .jsp содержит обычный HTML и специальные элементы, а контейнер сервлетов (например, Apache Tomcat) превращает его в Java-класс сервлета, компилирует и выполняет на каждый запрос. Браузер получает уже готовый HTML — Java-код до него не доходит.
Содержание
- Мини-словарь: что с чем путают
- Минимальная JSP-страница
- Жизненный цикл: от .jsp к сервлету
- Элементы JSP: что есть в шаблоне
- Ошибка с объявлением: одно поле на всех
- Правильная связка: сервлет готовит данные, JSP показывает
- Безопасность: EL сам ничего не экранирует
- javax или jakarta: главная ловушка старых примеров
- Если не получилось: симптомы и причины
- JSP в 2026 году: когда брать, когда нет
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — минимальная работающая страница, путь от .jsp до сервлета, EL и JSTL, правильная связка «сервлет + JSP», переход с javax.* на jakarta.* и честный ответ, нужен ли JSP в 2026 году. Все примеры проверены 23.09.2026 в Apache Tomcat 11.0.26 на OpenJDK 25.0.4 (образ tomcat:11).
Мини-словарь: что с чем путают
| Термин | Что это | Где живет |
|---|---|---|
| Сервлет | Java-класс, который принимает HTTP-запрос и формирует ответ (HttpServlet) |
код приложения |
| Контейнер сервлетов | Сервер, который управляет сервлетами: Tomcat, Jetty и др. | отдельный процесс или встроен в приложение |
| JSP | Шаблон страницы, из которого контейнер генерирует сервлет | файл .jsp |
| Jasper | JSP-движок Tomcat: транслирует .jsp в .java и компилирует |
внутри Tomcat |
| EL | Язык выражений ${...} для чтения данных в шаблоне |
внутри JSP |
| JSTL | Библиотека стандартных тегов: условия, циклы, вывод с экранированием | отдельные jar-файлы |
Главное различие: сервлет — это код, JSP — шаблон, из которого код генерируется автоматически. В хорошо устроенном приложении сервлет готовит данные, а JSP только отображает их.
Минимальная JSP-страница
Файл hello.jsp кладется в корень веб-приложения (в Tomcat — webapps/<имя приложения>/). Никакой настройки web.xml для него не нужно: запросы к *.jsp Tomcat по умолчанию отдает своему JSP-движку.
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.time.LocalDate" %>
<!DOCTYPE html>
<html>
<body>
<h1>Привет из JSP</h1>
<p>Сегодня: <%= LocalDate.now() %></p>
<p>Сумма: ${2 + 3}, сравнение: ${10 > 3}</p>
<p>Метод запроса: ${pageContext.request.method}</p>
</body>
</html>
Запрос curl http://localhost:8080/demo/hello.jsp вернул такой HTML (дата — день запуска):
<h1>Привет из JSP</h1>
<p>Сегодня: 2026-09-23</p>
<p>Сумма: 5, сравнение: true</p>
<p>Метод запроса: GET</p>
Что здесь есть. <%@ page ... %> — директива: настройки для трансляции (кодировка, импорты). <%= ... %> — выражение Java, результат попадает в HTML. ${...} — выражение EL, оно вычисляется при каждом запросе. Все остальное — статический текст, который уходит в ответ как есть.
Жизненный цикл: от .jsp к сервлету
У JSP тот же жизненный цикл, что у сервлета, плюс два шага в начале. Порядок такой:
- Трансляция. Движок (в Tomcat — Jasper) превращает
hello.jspв исходникhello_jsp.java. - Компиляция. Исходник компилируется в
hello_jsp.class. - Загрузка и создание экземпляра. Контейнер загружает класс и создает объект.
- Инициализация. Один раз вызывается
jspInit(). - Обработка запросов. На каждый запрос вызывается
_jspService(request, response). - Уничтожение. При остановке или перезагрузке приложения вызывается
jspDestroy().
Шаги 1-2 выполняются при первом обращении к странице (или заранее, если приложение прекомпилировано), поэтому первый запрос заметно медленнее следующих. В Tomcat сгенерированные файлы лежат в work/Catalina/localhost/<приложение>/org/apache/jsp/. Вот фрагмент реального counter_jsp.java:
public final class counter_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
...
public void _jspInit() {
...
public void _jspDestroy() {
...
public void _jspService(final jakarta.servlet.http.HttpServletRequest request, final jakarta.servlet.http.HttpServletResponse response)
Если изменить .jsp, Tomcat в режиме разработки (включен по умолчанию) заметит новую дату файла и повторит трансляцию и компиляцию — перезапускать сервер не нужно. В production этот режим обычно отключают или прекомпилируют страницы при сборке.
Элементы JSP: что есть в шаблоне
| Элемент | Синтаксис | Когда выполняется | Статус сейчас |
|---|---|---|---|
| Директива page | <%@ page import="..." %> |
при трансляции | используется |
| Директива include | <%@ include file="part.jspf" %> |
при трансляции, текст вклеивается в страницу | используется |
| Директива taglib | <%@ taglib prefix="c" uri="..." %> |
при трансляции | используется |
| Действие include | <jsp:include page="dyn.jsp"/> |
при каждом запросе, отдельный сервлет | используется |
| EL | ${user.name} |
при каждом запросе | основной способ вывода |
| Выражение | <%= expr %> |
при каждом запросе | legacy |
| Скриптлет | <% code; %> |
при каждом запросе | legacy |
| Объявление | <%! int x; %> |
поле класса сервлета | legacy, опасно |
Разница двух include видна в сгенерированном коде: для <%@ include %> Jasper вписал содержимое part.jspf прямо в inc_jsp.java (out.write("<p>footer</p>\n")), а для <jsp:include> создал отдельный dyn_jsp.java и вызов JspRuntimeLibrary.include(...). Статический include быстрее, динамический позволяет менять подключаемую страницу независимо.
Ошибка с объявлением: одно поле на всех
Частая ошибка из старых учебников — счетчик или «данные пользователя» в объявлении <%! %>. Вот неверный код counter.jsp:
<%@ page contentType="text/plain; charset=UTF-8" %>
<%! private int hits = 0; %>
<% int local = 0; hits++; local++; %>
hits=<%= hits %> local=<%= local %>
Результат трех запросов подряд (фактический вывод):
hits=1 local=1
hits=2 local=1
hits=3 local=1
Причина в том, что контейнер обычно держит один экземпляр сервлета и обслуживает им все запросы параллельно. Объявление <%! %> становится полем этого экземпляра (в сгенерированном коде — private int hits = 0;), поэтому оно общее для всех пользователей и не потокобезопасно. Переменная из скриптлета <% %> — локальная внутри _jspService и живет один запрос.
Исправление: не хранить состояние в полях страницы. Данные одного запроса передавать атрибутами запроса, данные пользователя — атрибутами сессии, общие счетчики — в потокобезопасных структурах в коде приложения (например, AtomicInteger), а не в JSP.
Правильная связка: сервлет готовит данные, JSP показывает
Логика в скриптлетах быстро превращается в нечитаемую смесь HTML и Java. Устоявшийся подход (Model 2, по сути MVC): запрос принимает сервлет, кладет данные в атрибуты и передает управление JSP, а JSP выводит их через EL и JSTL.
package demo;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@WebServlet("/products")
public class ProductsServlet extends HttpServlet {
public record Product(String name, int price) {}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
List<Product> products = List.of(
new Product("Клавиатура", 3500),
new Product("Мышь <USB>", 1200));
req.setAttribute("products", products);
req.setAttribute("q", req.getParameter("q"));
req.getRequestDispatcher("/WEB-INF/views/products.jsp").forward(req, resp);
}
}
Шаблон лежит в WEB-INF/views/products.jsp:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<body>
<p>Поиск: <c:out value="${q}"/></p>
<ul>
<c:forEach var="p" items="${products}">
<li><c:out value="${p.name}"/> - ${p.price} руб.</li>
</c:forEach>
</ul>
<c:if test="${empty products}"><p>Товаров нет</p></c:if>
</body>
</html>
Запрос к /demo/products?q=<script>alert(1)</script> (параметр закодирован через curl -G --data-urlencode) вернул:
<p>Поиск: <script>alert(1)</script></p>
<ul>
<li>Клавиатура - 3500 руб.</li>
<li>Мышь <USB> - 1200 руб.</li>
</ul>
Три вещи, которые здесь важны:
- JSP в
WEB-INFнедоступна напрямую. Запрос к/demo/WEB-INF/views/products.jspдал 404 — страницу можно открыть только через сервлет, который подготовил данные. - EL читает свойства объектов:
${p.name}вызываетgetName()у JavaBean или, как здесь, методname()у record. Поддержка record появилась в Jakarta EL 6.0, то есть в Tomcat 11. В Tomcat 10.1 та же страница падает сPropertyNotFoundException: Property [name] not found on type [demo.ProductsServlet$Product]— там модель данных делают обычным классом с геттерами. - JSTL не входит в Tomcat. Нужны два jar-файла — API и реализация Jakarta Standard Tag Library — в
WEB-INF/libили зависимостью сборки. Пример проверен сjakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2иorg.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1. В JSTL 3.0 адрес библиотеки —jakarta.tags.core; в старых проектах встречаетсяhttp://java.sun.com/jsp/jstl/core— эта реализация принимает и его, но в новом коде пишут новый адрес.
Безопасность: EL сам ничего не экранирует
Выражение ${...} в тексте страницы выводит значение как есть. Страница raw.jsp с той же директивой page (UTF-8) и строкой <p>Вы искали: ${param.q}</p> на тот же запрос вернула:
<p>Вы искали: <script>alert(1)</script></p>
Это XSS: браузер выполнит скрипт из параметра. Правило — все, что пришло извне или из базы, выводить через <c:out value="${...}"/> или ${fn:escapeXml(...)} (для fn нужна директива <%@ taglib prefix="fn" uri="jakarta.tags.functions" %>). Оба кодируют <, >, & и кавычки: строка "a'<b>& превратилась в "a'<b>&. У c:out экранирование включено по умолчанию, атрибут escapeXml="false" его отключает — в пользовательских данных его не ставят. Это защита вывода в HTML-тексте и в значениях атрибутов, заключенных в кавычки; для вставки в JavaScript, CSS или URL нужны свои правила кодирования. Валидация входных данных и параметризация SQL — отдельные меры, c:out их не заменяет.
javax или jakarta: главная ловушка старых примеров
В Jakarta EE 9 (2020) пакеты javax.servlet.* переименовали в jakarta.servlet.*. Tomcat 10 и 11 работают только с новым пространством имен, Tomcat 9 — со старым. Код из учебника 2018 года под Tomcat 11 не соберется:
package demo;
import javax.servlet.http.HttpServlet;
public class OldServlet extends HttpServlet {
}
demo/OldServlet.java:3: error: package javax.servlet.http does not exist
import javax.servlet.http.HttpServlet;
^
demo/OldServlet.java:5: error: cannot find symbol
public class OldServlet extends HttpServlet {
^
symbol: class HttpServlet
2 errors
Исправление — заменить импорты на jakarta.servlet... и обновить зависимости (API сервлетов, JSTL) на Jakarta-версии. Для готовых старых приложений у Apache есть Tomcat Migration Tool for Jakarta EE; Tomcat 11 содержит его библиотеку в lib.
Если не получилось: симптомы и причины
- 404 на
.jsp. Файл не в той папке (нужен корень приложения, неWEB-INF) или ошибка в имени приложения в URL. - 500 с
JasperException. Ошибка трансляции или компиляции страницы: в тексте ошибки Tomcat указывает строку.jsp. Частая причина — JSTL-тег без jar-файлов вWEB-INF/lib. ClassNotFoundException: javax.servlet.... Старый код или библиотека подjavaxна Tomcat 10+.- На странице видно
${...}буквально. EL отключен:isELIgnored="true"или очень старая версия вweb.xml. - Кракозябры вместо кириллицы. Не задан
pageEncoding="UTF-8"или файл сохранен не в UTF-8.
JSP в 2026 году: когда брать, когда нет
JSP — часть актуальной платформы Jakarta EE (Jakarta Pages поддерживается в Tomcat 11), но для новых проектов его выбирают редко. Ситуация зависит от задачи:
| Задача | Что обычно берут | Почему |
|---|---|---|
| Поддержка существующего приложения на JSP | JSP + EL + JSTL, без новых скриптлетов | переписывание дороже, чем аккуратная доработка |
| Новое серверное приложение на Spring Boot | Thymeleaf или другой шаблонизатор | JSP во встроенном контейнере Spring Boot имеет ограничения |
| Интерфейс с богатой логикой на клиенте | SPA (React, Vue, Angular) + REST API на Java | HTML собирается в браузере |
| Приложение на Jakarta Faces | Facelets (XHTML) | в Faces основной язык представления — не JSP |
Поэтому JSP стоит знать как основу: он показывает, как работают сервлеты, запросы, сессии и шаблоны на сервере, и регулярно встречается в корпоративных системах, которые нужно поддерживать и мигрировать.
Выводы
- JSP — серверный шаблон: контейнер транслирует
.jspв сервлет, компилирует его и вызывает_jspServiceна каждый запрос. - Трансляция и компиляция происходят при первом запросе или при сборке; в режиме разработки Tomcat пересобирает измененную страницу сам.
- Современный стиль — сервлет готовит данные, JSP выводит их через EL и JSTL; скриптлеты и объявления
<%! %>— legacy, объявления опасны общим состоянием. - EL не экранирует вывод: данные извне выводят через
c:outилиfn:escapeXml. - Tomcat 10+ работает только с
jakarta.*; для новых проектов чаще выбирают Thymeleaf или SPA, но JSP нужно знать для поддержки и миграции.
Где применяется / связь с практикой
Освойте тему на практике
JSP почти не встречается в изоляции: он живет внутри Java-приложения с сервлетами, базой данных, сборкой и развертыванием на сервере приложений. Чтобы поддерживать такие системы и переводить их на современный стек, нужно уверенно владеть самой Java, коллекциями, многопоточностью и веб-частью платформы. Системно это разбирают на курсе Java Developer. Professional.
Освойте тему на практике
Отдельные темы по Java и веб-разработке удобно посмотреть на открытых уроках Otus — это бесплатные вебинары с преподавателями-практиками.
FAQ
Чем JSP отличается от HTML-файла?
HTML отдается сервером как есть, а JSP сначала выполняется на сервере: EL и теги подставляют данные, и браузер получает уже итоговый HTML.
Можно ли запустить JSP без Tomcat?
Нужен любой контейнер с поддержкой Jakarta Pages — например, Jetty, GlassFish или WildFly. Открыть .jsp в браузере как файл не получится: выполнять его некому.
Нужен ли web.xml для JSP?
Для простых страниц и сервлетов с аннотацией @WebServlet — нет. web.xml нужен для страниц ошибок, фильтров, групп настроек JSP и других параметров развертывания.



