Перейти к основному содержимому

5.03. JavaServer Faces

Разработчику Архитектору

JavaServer Faces

Контекст появления и задачи JSF

JavaServer Faces (JSF) — это спецификация, определяющая серверную компонентную модель для создания пользовательских интерфейсов веб-приложений на платформе Java. Первая версия JSF (1.0) была утверждена в рамках JSR-127 в 2004 году; с тех пор она прошла несколько эволюционных этапов (JSF 1.2, JSF 2.0–2.3, переход в Jakarta EE 8–10), укрепляя свою позицию как инструмент для разработки enterprise-решений с акцентом на декларативное описание интерфейса, переиспользуемость и соответствие архитектуре MVC.

Ключевое отличие JSF от других веб-подходов — моделирование пользовательского интерфейса на сервере как дерева компонентов с собственным состоянием, поведением и жизненным циклом. Это позволяет разработчику оперировать абстракциями, близкими к desktop-фреймворкам (например, Swing), но адаптированными под stateless-природу HTTP.

Основная задача JSF — устранить разрыв между визуальным представлением (view) и бизнес-логикой (model), обеспечив при этом:

  • чёткое разделение ответственности (MVC);
  • декларативное описание интерфейса (через разметку);
  • автоматическое управление состоянием между запросами;
  • встроенную систему валидации и конвертации данных;
  • поддержку событийной модели, аналогичной событиям в desktop-приложениях;
  • интеграцию с другими частями Jakarta EE (CDI, Bean Validation, JPA и др.).

JSF работает в связке с сервлетами, контейнером (Servlet Container), и использует шаблонизатор (Facelets) как основной инструмент для построения представления.


Архитектурная база

Перед погружением в JSF необходимо уточнить его позицию в стеке Java-вебтехнологий.

Все Java-вебприложения, независимо от используемого фреймворка, работают внутри сервлет-контейнера — части Jakarta EE-совместимого сервера (например, Apache Tomcat, Jetty, WildFly, GlassFish). Сервлет-контейнер управляет жизненным циклом сервлетов, обеспечивает обработку HTTP-запросов, многопоточность, безопасность и другие низкоуровневые аспекты.

Сервлет — это Java-класс, реализующий интерфейс javax.servlet.Servlet (в Jakarta EE — jakarta.servlet.Servlet). Он представляет собой обработчик запроса: на каждый входящий HTTP-запрос контейнер вызывает метод service(), который, в свою очередь, делегирует выполнение doGet(), doPost() и т.п. в случае наследования от HttpServlet.

JSF налагает поверх модели абстракцию. В частности, JSF требует, чтобы все запросы к JSF-страницам проходили через единый центральный сервлет — FacesServlet. Именно он является единственной точкой входа для всех JSF-ресурсов и играет роль контроллера (C) в паттерне MVC.

Конфигурация FacesServlet обычно выполняется в файле web.xml или через аннотации:

<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>

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

Важно: FacesServlet не генерирует HTML напрямую. Он координирует работу компонентов, управляющих рендерингом, — это одна из ключевых идей компонентной модели.


Компонентная модель и дерево представления

JSF-приложение строится из UI-компонентов — объектов Java-классов, наследующих от UIComponent. Каждый компонент инкапсулирует:

  • визуальное представление (рендеринг в HTML);
  • поведение (обработка событий, обновление состояния);
  • модель данных (связь с бэкенд-объектами через value expressions);
  • атрибуты (идентификатор, стили, доступность и т.д.).

Примеры компонентов:

  • HtmlInputText (соответствует <h:inputText> в разметке);
  • HtmlCommandButton (<h:commandButton>);
  • HtmlDataTable (<h:dataTable>).

Эти компоненты во время обработки запроса образуют дерево представления (View Tree) — иерархическую структуру, полностью описывающую текущую страницу с её состоянием. Дерево создаётся при первом обращении к странице (initial view build), сохраняется между запросами (в сессии или клиенте), и восстанавливается при последующих взаимодействиях.

Состояние дерева может храниться:

  • на стороне сервера (server — по умолчанию): сериализованное дерево кладётся в HttpSession;
  • на стороне клиента (client): дерево сериализуется в скрытые поля (<input type="hidden" name="javax.faces.ViewState" ... />), отправляется на сервер при каждом запросе и десериализуется заново.

Выбор стратегии влияет на масштабируемость (хранилище на клиенте снижает нагрузку на память сервера, но увеличивает объём трафика и уязвимость к подделке).

Каждый компонент имеет:

  • идентификатор (id) — уникальный в пределах naming container;
  • client ID — полный идентификатор, включающий иерархию контейнеров (например, form:username);
  • связь с моделью через выражение вида #{bean.property};
  • набор обработчиков событий (action listeners, value change listeners);
  • валидаторы и конвертеры, прикреплённые явно или неявно.

Таким образом, UI в JSF — это динамическая, управляемая сервером объектная структура, превращаемая в HTML только на этапе рендеринга.


Жизненный цикл JSF-запроса

Жизненный цикл (Lifecycle) — это фундаментальный концепт JSF. Каждый HTTP-запрос, попадающий в FacesServlet, проходит через строго определённую последовательность фаз. Этот процесс гарантирует, что изменения состояния, валидация и бизнес-логика выполняются в корректном порядке, а побочные эффекты не возникают преждевременно.

Спецификацией определено шесть фаз (в Jakarta EE 9+ имена классов начинаются с jakarta.faces вместо javax.faces):

1. Restore View (Восстановление представления)

На этой фазе FacesServlet определяет, существует ли уже дерево компонентов для данного view ID (например, /login.xhtml).

  • Если запрос — initial request (GET к новой странице), дерево создаётся заново (build view) на основе Facelets-шаблона.
  • Если запрос — postback (POST-запрос, инициированный JSF-формой), дерево восстанавливается из сохранённого состояния (из сессии или из ViewState, отправленного клиентом).

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

2. Apply Request Values (Применение значений запроса)

Компоненты, поддерживающие ввод (например, UIInput), получают «сырые» строковые значения из параметров HTTP-запроса (по именам, соответствующим их client ID).
На этом этапе:

  • значения не конвертируются и не валидируются — только привязка «строка → компонент»;
  • генерируются события типа ValueChangeEvent, если значение изменилось по сравнению с предыдущим состоянием (но обрабатываются они позже, в фазе Process Validations);
  • выполняется так называемый immediate validation: если у компонента установлен флаг immediate="true", валидация и обновление модели происходят уже на этой фазе, и последующие фазы могут быть пропущены.

Эта фаза обеспечивает «сырую» привязку формы — аналогично тому, как request.getParameter() работает в чистом сервлете.

3. Process Validations (Обработка валидации)

Здесь проверяется корректность введённых данных:

  • сначала выполняются локальные валидаторы компонентов (например, required="true", validator-атрибут);
  • затем применяются глобальные валидаторы (через <f:validator> или Bean Validation @NotNull, @Size и т.п., если интеграция включена);
  • в случае ошибок валидации в FacesContext добавляются сообщения, фаза Update Model Values и все последующие пропускаются, и управление переходит сразу к Render Response — чтобы отобразить ошибки на странице без изменения модели.

Важно: конвертация типов (например, строки в LocalDate) происходит до валидации — поэтому валидаторы работают уже с объектами нужного типа.

4. Update Model Values (Обновление модели)

Если валидация прошла успешно, значения компонентов (уже сконвертированные в нужные Java-типы) передаются в целевые свойства управляемых бинов (backing beans) через value expressions (#{user.name}).
Это — момент, когда пользовательский ввод «становится частью приложения».
Если во время установки значения возникает исключение (например, PropertyNotFoundException), оно обрабатывается как ошибка, и фазы после этой — пропускаются.

5. Invoke Application (Вызов прикладной логики)

На этой фазе выполняется бизнес-логика, инициированная пользовательским действием:

  • вызывается action method (например, #{user.login} из <h:commandButton action="#{user.login}" />);
  • выполняются action listeners (через <f:actionListener> или атрибут actionListener).

Метод действия может возвращать строку — логический исход (outcome), который используется для навигации (например, "success"/home.xhtml). В JSF 2.0+ навигация часто управляется декларативно — через соглашения об именовании (login.xhtmllogin.xhtml?faces-redirect=true) или программно через NavigationHandler.

Это — единственная фаза, где допустимы побочные эффекты: запись в БД, отправка писем, изменение состояния сессии.

6. Render Response (Формирование ответа)

Независимо от того, какие фазы были пройдены, эта фаза всегда выполняется в конце цикла.
Каждый компонент в дереве вызывает свой рендерер (Renderer), который генерирует HTML-фрагмент (или другой клиентский код — например, JSON для AJAX).
Рендереры — отдельные классы, привязанные к компонентам. Это позволяет:

  • подменить рендеринг компонента (кастомизация отображения);
  • поддерживать разные клиенты (HTML, WML, мобильные форматы — хотя на практике используется почти исключительно HTML);
  • инкапсулировать логику генерации разметки.

На выходе формируется полный HTML-документ (или частичный ответ для AJAX), который отправляется клиенту через HttpServletResponse.


Facelets

От JSP к Facelets

Изначально JSF 1.x допускал использование JavaServer Pages (JSP) в качестве средства описания представлений. Однако JSP, будучи технологией, ориентированной на скриптовую вставку Java-кода в HTML, плохо сочеталась с компонентной моделью JSF:

  • динамическое построение дерева компонентов в JSP происходило во время компиляции JSP-страницы, что нарушало предсказуемость;
  • вложенность тегов JSF и JSTL могла приводить к неочевидному порядку выполнения;
  • отсутствовала поддержка шаблонов и композиции на уровне спецификации.

С выходом JSF 2.0 (2009) в качестве стандартного и рекомендованного механизма шаблонизации был утверждён Facelets — XML-ориентированный фреймворк, изначально созданный как сторонний проект (Jacob Hookom), а затем интегрированный в спецификацию. Facelets работает с файлами расширения .xhtml и обеспечивает:

  • полную совместимость с XHTML (валидный XML);
  • ленивую инициализацию дерева компонентов — дерево строится во время выполнения запроса, а не при компиляции страницы;
  • поддержку композиции и шаблонов на уровне тегов (<ui:composition>, <ui:include>, <ui:define> и др.);
  • кастомизацию поведения тегов через TagHandler’ы;
  • интроспекцию и отладку — благодаря строгой XML-структуре.

Facelets — это часть движка построения дерева представления. Он парсит разметку, создаёт экземпляры UI-компонентов в памяти, устанавливает их атрибуты (включая выражения #{...}) и формирует дерево, которое затем проходит жизненный цикл.

Пространства имён и основные теги

Facelets-страница начинается с объявления XML и пространств имён (namespaces), определяющих доступные теги:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets"
xmlns:c="jakarta.tags.core">

(В Jakarta EE 9+ префикс java.sun.com заменён на jakarta.*; в более старых версиях использовался http://java.sun.com/jsf/....)

1. h: — HTML-компоненты (UI delegates)

Эти теги создают экземпляры стандартных UI-компонентов и привязывают их к HTML-элементам:

  • <h:inputText />HtmlInputText<input type="text" />
  • <h:selectOneMenu />HtmlSelectOneMenu<select><option>...</select>
  • <h:dataTable />HtmlDataTable<table><tr><td>...</table>
  • <h:outputText />HtmlOutputText → текстовый узел (экранируется по умолчанию для защиты от XSS)
  • <h:graphicImage />HtmlGraphicImage<img src="..." />

Фундаментальное свойство этих тегов — двусторонняя привязка данных (two-way binding) через атрибут value:

<h:inputText value="#{user.email}" required="true" />
  • при рендеринге: значение из user.getEmail() подставляется в value HTML-элемента;
  • при postback: введённая строка из запроса передаётся в user.setEmail(...) (после конвертации и валидации).

2. f: — ядро JSF: поведение, конвертеры, валидаторы

Эти теги декорируют другие компоненты, добавляя логику:

  • <f:ajax /> — подключение AJAX-поведения к компоненту;
  • <f:convertDateTime />, <f:convertNumber /> — конвертеры для форматирования;
  • <f:validateLength />, <f:validateRegex /> — встроенные валидаторы;
  • <f:validator />, <f:converter /> — подключение кастомных реализаций;
  • <f:attribute /> — передача произвольных данных в компонент (например, для кастомного рендера);
  • <f:selectItem />, <f:selectItems /> — динамическое наполнение выпадающих списков/радиогрупп.

Все теги f: работают по принципу attached objects: они регистрируются в родительском компоненте и участвуют в его жизненном цикле.

Пример комплексной привязки:

<h:inputText id="birthdate" value="#{user.birthDate}">
<f:convertDateTime pattern="dd.MM.yyyy" />
<f:validateRegex pattern="\d{2}\.\d{2}\.\d{4}" />
<f:ajax event="blur" render="msg" />
</h:inputText>
<h:message id="msg" for="birthdate" />

Здесь к одному полю ввода последовательно применяются: конвертер даты, валидатор формата, AJAX-прослушка события потери фокуса (с обновлением элемента msg), и отображение ошибки.

3. ui: — шаблонизация и композиция

Facelets предоставляет мощные средства для структурирования представлений:

  • <ui:composition template="..."> — определяет, что текущая страница — содержимое, вставляемое в шаблон;
  • <ui:define name="..."> — заполняет именованный блок (<ui:insert>) в шаблоне;
  • <ui:include src="..."> — включает внешний фрагмент (аналог #include в JSP, но с полной изоляцией области видимости);
  • <ui:repeat> — итерация по коллекции на уровне компонентов (в отличие от <c:forEach>, который работает на этапе построения дерева и не создаёт отдельных компонентов для каждой итерации — это критически важно для обработки состояния).

Шаблон layout.xhtml:

<html ...>
<h:head><title><ui:insert name="title">Default Title</ui:insert></title></h:head>
<h:body>
<div id="header">...</div>
<div id="content"><ui:insert name="content" /></div>
<div id="footer">...</div>
</h:body>
</html>

Страница home.xhtml:

<ui:composition template="/layout.xhtml">
<ui:define name="title">Главная</ui:define>
<ui:define name="content">
<h1>Добро пожаловать!</h1>
<h:form>
<h:inputText value="#{user.name}" />
<h:commandButton value="Сохранить" action="#{user.save}" />
</h:form>
</ui:define>
</ui:composition>

4. c: — JSTL Core (осторожно!)

Теги из jakarta.tags.core (<c:if>, <c:forEach>, <c:set>) выполняются на этапе построения дерева представления, до восстановления состояния и до жизненного цикла JSF. Это означает:

  • <c:if test="#{user.loggedIn}"> вычисляется один раз при создании страницы — даже если loggedIn изменится в процессе запроса, условие не перепроверяется;
  • <c:forEach> создаёт один компонент, многократно отрендеренный, а не несколько независимых компонентов — что ломает привязку состояния в формах.

Поэтому JSTL в JSF рекомендуется использовать только для статической логики сборки страницы (например, генерация метатегов, условное подключение CSS), а не для динамического поведения. Для управления видимостью компонентов следует использовать rendered="#{...}".


Управляемые бины

От managed beans к CDI-бинам

В ранних версиях JSF (до 2.0) управление зависимостями и жизненным циклом бинов осуществлялось через собственный механизм — JSF Managed Beans, конфигурируемый в faces-config.xml или аннотациями (@ManagedBean, @RequestScoped и др.). Однако с появлением Contexts and Dependency Injection (CDI) в Java EE 6 (JSR-299) JSF перешёл на использование CDI как стандартного DI-фреймворка.

С JSF 2.2+ рекомендуется использовать:

  • @Named (из jakarta.inject) вместо @ManagedBean;
  • области видимости из jakarta.enterprise.context: @RequestScoped, @SessionScoped, @ApplicationScoped, @ViewScoped, @FlowScoped.

Пример CDI-бина:

@Named
@ViewScoped
public class UserController implements Serializable {
@Inject private UserService userService;

private String name;
private User user;

@PostConstruct
public void init() {
user = new User();
}

public String save() {
userService.persist(user);
return "profile?faces-redirect=true";
}

// геттеры/сеттеры
}

Обратите внимание: класс должен быть сериализуемым, если используется @ViewScoped или @SessionScoped — это требование для сохранения состояния между запросами.

Области видимости и их значение

  • @RequestScoped — экземпляр создаётся на каждый HTTP-запрос. Подходит для неизменяемых данных или подготовки данных для отображения. Не сохраняет состояние между postback’ами.
  • @ViewScoped — бин живёт, пока пользователь остаётся на той же JSF-странице (view). Уничтожается при навигации на другую страницу или при истечении таймаута. Идеален для форм с многоэтапным вводом (например, wizard).
  • @FlowScoped — группирует несколько страниц в логический поток (flow), например, процесс оформления заказа. Состояние сохраняется до завершения потока.
  • @SessionScoped — привязан к HTTP-сессии. Используется для данных пользователя (авторизация, настройки). Требует осторожности: утечки памяти, конкурентный доступ.
  • @ApplicationScoped — один экземпляр на всё приложение. Для глобальных конфигураций, кэшей.

@ViewScoped — одна из самых важных областей в JSF: она сочетает удобство stateful-разработки (сохранение данных формы между postback’ами) с масштабируемостью (не загромождает сессию).


AJAX в JSF

JSF 2.0 ввёл встроенную поддержку AJAX через тег <f:ajax>, что позволило отказаться от внешних библиотек (например, RichFaces, когда-то популярного расширения).

Основные атрибуты <f:ajax>:

  • event — событие, инициирующее запрос (например, click, change, blur, action). По умолчанию: action для кнопок, valueChange для полей ввода.
  • execute — список клиентских ID компонентов, значения которых отправляются на сервер (аналог partialSubmit). По умолчанию — @this (только текущий компонент). Можно указать @form, @all, или id1 id2.
  • render — список ID компонентов, которые обновляются в DOM после ответа (аналог update). По умолчанию — @none.
  • listener — метод-слушатель (void method(AjaxBehaviorEvent event)), вызываемый на сервере перед фазой Render Response.
  • onbegin, oncomplete, onsuccess, onerror — JavaScript-обработчики на клиенте.

Пример: живой поиск:

<h:inputText value="#{searchBean.query}">
<f:ajax event="keyup" execute="@this" render="results" />
</h:inputText>
<h:dataTable id="results" value="#{searchBean.results}" var="item">
<h:column>#{item.name}</h:column>
</h:dataTable>

Важные нюансы:

  • AJAX-запрос в JSF — это всё тот же полноценный JSF-цикл, но с уточнённым набором обрабатываемых компонентов (execute) и обновляемых (render).
  • Если в execute не попал компонент с required="true", но он пуст — ошибка валидации не возникнет (т.к. фаза Process Validations пройдёт только по execute-компонентам).
  • Для отмены отправки при ошибке валидации используется атрибут resetValues="true" (JSF 2.2+) — он сбрасывает «грязные» значения компонентов до состояния из модели, если валидация провалена.

Обработка событий

JSF предлагает многоуровневую систему событий:

Тип событияКогда возникаетГде обрабатываетсяПример использования
ActionEventПри нажатии кнопки/ссылки (<h:commandButton>, <h:commandLink>)В actionListener или через actionЛогирование действий пользователя
ValueChangeEventПри изменении значения поля (<h:inputText> и др.)В valueChangeListenerДинамическая подгрузка связанных данных (например, города при выборе региона)
PhaseEventНа входе/выходе каждой фазы жизненного циклаВ PhaseListenerАудит, профилирование, кастомная обработка ошибок
PostConstructViewEventПосле создания @ViewScoped бина и до Render ResponseСлушатель @Observes PostConstructViewEventИнициализация данных, привязанных к конкретному view
PreRenderViewEventПеред Render ResponseСлушатель @Observes PreRenderViewEventПроверка прав доступа перед отрисовкой страницы

Современный подход — использовать CDI-события (Event<T> и @Observes), а не реализацию интерфейсов вроде ActionListener. Это обеспечивает слабую связанность и тестируемость.


Расширения JSF

Хотя стандартная библиотека компонентов (h:) предоставляет базовые UI-элементы (поля ввода, кнопки, таблицы), её возможностей недостаточно для построения современных, интерактивных и доступных (accessible) интерфейсов. Эту нишу занимают сторонние библиотеки компонентов, которые:

  • расширяют стандартный набор (календари, древовидные списки, диаграммы, drag-and-drop);
  • инкапсулируют клиентский JavaScript (часто на основе jQuery UI или собственных реализаций);
  • обеспечивают темизацию и соответствие дизайн-системе;
  • добавляют расширенные AJAX-возможности (lazy loading, virtual scrolling).

Наиболее значимые из них:

1. PrimeFaces — доминирующая enterprise-библиотека

PrimeFaces — наиболее широко используемое расширение JSF. По состоянию на 2025 год поддерживает Jakarta Faces 4.x, включает более 100 компонентов и активно развивается.
Ключевые особенности:

  • Единая клиентская основа: все компоненты используют единый JavaScript-фреймворк (PrimeFaces Core), что устраняет конфликты версий библиотек;
  • Поддержка тем: 20+ официальных тем (Material, Bootstrap, Fluent), возможность кастомизации через Sass;
  • Расширенные таблицы: сортировка, фильтрация, пагинация, экспорт (CSV, Excel, PDF), lazy loading;
  • Интеграция с Push-технологиями: через p:socket и p:poll — поддержка WebSocket и long polling;
  • Мобильная адаптация: отдельный набор компонентов (PrimeFaces Mobile), хотя в последние годы акцент смещён на responsive-дизайн.

Пример:

<p:dataTable var="car" value="#{carService.cars}" paginator="true" rows="10">
<p:column headerText="Модель" sortBy="#{car.model}">
<h:outputText value="#{car.model}" />
</p:column>
<p:column headerText="Год" filterBy="#{car.year}">
<h:outputText value="#{car.year}" />
</p:column>
</p:dataTable>

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

2. OmniFaces — утилитарное расширение «умного JSF»

OmniFaces не предоставляет UI-компонентов. Это — библиотека утилит и корректировок, решающая типичные боли JSF:

  • <o:socket> — упрощённая интеграция WebSocket без boilerplate-кода;
  • <o:validateBean /> — принудительная валидация всего бина по правилам Bean Validation до фазы Update Model Values;
  • <o:importConstants />, <o:importFunctions /> — импорт статических значений и методов в EL-выражения;
  • Faces#redirect(), Messages#addGlobalError() — типобезопасные вспомогательные методы;
  • Автоматическое исправление проблем с @ViewScoped в CDI (через @ViewScoped из OmniFaces, если контейнер не поддерживает Jakarta Faces 2.2+).

OmniFaces рекомендован разработчиками спецификации как «лекарство от JSF-головной боли» и часто применяется совместно с PrimeFaces.

3. BootsFaces / ButterFaces — интеграция с Bootstrap

Эти библиотеки генерируют HTML, соответствующий CSS-фреймворку Bootstrap, что позволяет использовать существующие дизайн-системы без кастомной стилизации. Однако их активность снизилась после 2020 года — во многом из-за роста популярности PrimeFaces и перехода на CSS-in-JS подходы в новых проектах.

Важно: использование расширений не отменяет понимания базового JSF. Наоборот — знание жизненного цикла и компонентной модели критично для отладки проблем, возникающих «под капотом» библиотек (например, некорректный client ID в render="@form" при вложенных p:tabView).


Интеграция с Jakarta EE

JSF редко используется изолированно. Его сила — в глубокой интеграции с другими стандартами Jakarta EE.

1. CDI (Contexts and Dependency Injection)

Как уже отмечалось, CDI заменил устаревший механизм managed beans. Но интеграция идёт глубже:

  • Продюсеры и диспозеры: динамическое создание зависимостей:
    @Produces
    @RequestScoped
    public EntityManager createEntityManager() {
    return emf.createEntityManager();
    }
    @Disposes
    public void closeEntityManager(EntityManager em) {
    if (em.isOpen()) em.close();
    }
  • Квалификаторы: разрешение неоднозначности (@Production, @Mock);
  • Интерцепторы: сквозная логика (логирование, транзакции, безопасность) через @AroundInvoke.

2. JPA (Java Persistence API)

JSF не требует JPA, но в enterprise-приложениях они почти всегда используются вместе:

  • В @ViewScoped бине может храниться EntityManager или ссылка на сервисный слой;
  • LazyInitializationException — классическая проблема: попытка доступа к lazy-загружаемому полю (@OneToMany) после закрытия EntityManager. Решения:
    • инициализация коллекций в бине (Hibernate.initialize() или JOIN FETCH);
    • использование @Transactional на уровне CDI-бинов (через @Transactional из jakarta.transaction);
    • применение DTO-слоя и маппинга (MapStruct, ModelMapper).

3. Bean Validation (Jakarta Validation)

Спецификация JSR-380 (и её продолжение в Jakarta EE) интегрируется в JSF «из коробки»:

  • Аннотации @NotNull, @Size, @Pattern, @Email применяются к полям бина;
  • JSF автоматически вызывает валидацию в фазе Process Validations;
  • Сообщения об ошибках извлекаются из ValidationMessages.properties или кастомных ResourceBundle.

Пример:

public class User {
@NotNull(message = "Имя обязательно")
@Size(min = 2, max = 50, message = "Имя должно быть от 2 до 50 символов")
private String name;

@Email(message = "Некорректный email")
private String email;
}

В Facelets достаточно <h:message for="email" /> — сообщение подставится автоматически.

4. Security (Jakarta Security)

Аутентификация и авторизация:

  • @RolesAllowed("ADMIN") на методах бинов — вызов метода будет заблокирован при отсутствии роли;
  • ExternalContext.isUserInRole("ADMIN") — проверка в EL: rendered="#{facesContext.externalContext.isUserInRole('ADMIN')}";
  • Интеграция с @LoginToContinue и стандартными механизмами Jakarta Security (Form, JWT, OpenID Connect).

Тестирование JSF-приложений

Тестирование JSF — одна из наиболее сложных задач, обусловленная его серверной природой и зависимостью от контекста (FacesContext). Подходы:

1. Unit-тестирование бинов и сервисов

Наиболее эффективный и быстрый уровень:

  • Mock-объекты (Mockito, EasyMock) для зависимостей;
  • CDI-Unit или Weld SE Embedded для запуска CDI-контекста вне контейнера;
  • Тестирование логики без участия JSF.

Пример (JUnit 5 + Mockito):

@ExtendWith(MockitoExtension.class)
class UserControllerTest {
@Mock private UserService userService;
@InjectMocks private UserController controller;

@Test
void save_shouldPersistUser() {
controller.setName("Тимур");
String outcome = controller.save();

verify(userService).persist(argThat(u -> "Тимур".equals(u.getName())));
assertEquals("profile?faces-redirect=true", outcome);
}
}

2. Интеграционное тестирование с Arquillian

Arquillian запускает приложение внутри настоящего или embedded-сервера (WildFly, TomEE), что позволяет:

  • Проверять CDI-инъекции, транзакции, безопасность;
  • Тестировать JSF-бинды и навигацию;
  • Использовать @ArquillianResource для доступа к URL, FacesContext.

Ограничение: высокая ресурсоёмкость, медленный запуск.

3. Тестирование представления (View Testing)

Через MockFacesContext (OmniFaces) или TestFacesContext (PrimeFaces Test):

try (MockFacesContext context = new MockFacesContext()) {
context.setViewRoot(new UIViewRoot());
// имитируем фазы жизненного цикла
controller.setName("Тимур");
controller.save();
// проверяем состояние
}

4. End-to-End тестирование

Selenium WebDriver + TestContainers (для поднятия БД и сервера):

  • Проверка UI-взаимодействий;
  • Валидация AJAX-обновлений;
  • Тестирование навигации и обработки ошибок.

Производительность и масштабируемость

JSF часто критикуют за «тяжеловесность». Однако при грамотной настройке он способен обслуживать высоконагруженные системы.

1. Управление состоянием (State Saving)

  • jakarta.faces.STATE_SAVING_METHOD=client — снижает нагрузку на память сервера, но увеличивает объём трафика (ViewState в base64 может достигать сотен КБ). Компрессия (com.sun.faces.compressViewState=true) обязательна.
  • jakarta.faces.PARTIAL_STATE_SAVING=true (по умолчанию с JSF 2.0) — сохраняется только изменённая часть состояния, а не всё дерево. Критически важно для @ViewScoped.
  • Ограничение глубины истории ViewState (jakarta.faces.NUMBER_OF_VIEWS_IN_SESSION) — предотвращает утечки при частых postback’ах.

2. Оптимизация жизненного цикла

  • Использование immediate="true" для операций «отмены» — пропуск валидации и обновления модели;
  • Минимизация execute в AJAX — отправка только необходимых данных;
  • Кэширование рендереров и конвертеров (если они stateless).

3. Клиентская оптимизация

  • Объединение и минификация JS/CSS (PrimeFaces предоставляет primefaces.MOVE_SCRIPTS_TO_BOTTOM=true);
  • Lazy loading тяжёлых компонентов (p:tabView dynamic="true");
  • Использование CDN для библиотэк.