Практикум JSF — список задач
О практикуме
Соберём веб-приложение "Список задач" на JavaServer Faces (JSF) 4 и Jakarta EE 10. Пользователь сможет добавлять задачи, отмечать их выполненными, удалять строки и видеть статистику — всё через браузер, без перезагрузки страницы при смене чекбокса.
Стек проекта:
- Maven WAR — упаковка web-приложения для servlet-контейнера (структура и сборки);
- Facelets — XHTML-разметка с тегами
h:иf:(теория Facelets); - CDI (Weld) — создание и внедрение managed beans (JavaBeans и CDI);
- Jetty Maven Plugin — локальный запуск без установки Tomcat.
В новых backend-проектах чаще берут Spring Boot, а JSF встречается в корпоративных порталах и системах, которые поддерживают годами. Этот практикум не про "модный стек", а про понимание серверного MVC, postback и состояния формы на сервере — навыков, полезных и при чтении legacy-кода.
Что такое JSF в контексте этого проекта
JavaServer Faces (JSF) — спецификация Jakarta EE для построения пользовательского интерфейса на сервере. Браузер получает обычный HTML, но каждый клик по кнопке JSF-компонента отправляет postback — POST-запрос, который проходит через жизненный цикл JSF (шесть фаз от Restore View до Render Response). Подробно — в JavaServer Faces — фреймворк для веб-интерфейсов.
В нашем приложении три слоя MVC:
| Слой MVC | Роль в проекте | Файлы |
|---|---|---|
| Model | Данные задачи | Task.java |
| View | Разметка и компоненты UI | index.xhtml, styles.css |
| Controller | Обработка действий пользователя | TodoBean.java, FacesServlet |
Связь View ↔ Model идёт через Expression Language (EL) — выражения вида #{todoBean.newTaskTitle}. JSF читает и записывает свойства bean по JavaBean-конвенции (ООП и классы).
Ключевые термины
- Managed bean — Java-класс, живущий в определённой области видимости (request, session, view); в JSF 2.2+ это CDI-бины с
@Namedи@SessionScoped. - Postback — повторный POST на ту же страницу после действия пользователя (кнопка, чекбокс); типичный паттерн JSF, в отличие от REST, где каждый URL — отдельный ресурс (HTTP и веб-интеграции).
- Facelets — шаблонизатор для
.xhtml; тегиh:inputText,h:commandButtonпревращаются в HTML на этапе Render Response. - WAR (Web Application Archive) — JAR-подобный архив с
WEB-INF,classesи статикой; разворачивается в Tomcat, Jetty, Payara. - FacesServlet — единственная точка входа для всех
*.xhtml; без него контейнер отдал бы файл как статику, минуя JSF.
Почему именно этот проект
- охватывает типичный legacy-сценарий — форма + таблица + серверное состояние;
- показывает связку managed bean ↔ EL ↔ компоненты JSF без сторонних UI-библиотек (PrimeFaces — в первой программе на JSF);
- компактен для одного вечера (2–3 часа), но близок к реальной поддержке старых порталов;
- после прохождения легче читать теорию JSF — вы уже видели lifecycle "вживую".
Предварительные знания: JDK 17+, Maven 3.8+, классы и коллекции (ООП, коллекции, при желании Stream API для getCompletedCount). Желательно пройти первую программу на JSF. Готовый проект для сверки: F:\Projects\JVM\Java\JavaServerFacesSimple.
Что получится
| Действие пользователя | Компонент JSF | Метод / свойство bean |
|---|---|---|
| Ввести текст и нажать "Добавить" | h:inputText, h:commandButton | TodoBean.addTask() |
| Отметить "выполнено" | h:selectBooleanCheckbox, f:ajax | Task.setCompleted() |
| Удалить строку | h:commandButton | TodoBean.removeTask(task) |
| Отправить пустое поле | h:messages | FacesMessage в addTask() |
| Увидеть статистику | EL в разметке | tasks.size(), completedCount |
Карта этапов
| Этап | Фокус | Результат |
|---|---|---|
| 0 | Каркас Maven WAR | Пустая структура каталогов |
| 1 | pom.xml | Зависимости JSF, CDI, Jetty |
| 2 | web.xml, beans.xml | FacesServlet и CDI |
| 3 | Task | Модель задачи |
| 4 | TodoBean | Логика списка, @SessionScoped |
| 5 | index.xhtml, форма | Добавление и сообщения |
| 6 | Таблица и AJAX | h:dataTable, f:ajax |
| 7 | CSS | Оформление страницы |
| 8 | Запуск | mvn jetty:run, проверка в браузере |
После каждого этапа имеет смысл собрать или запустить проект — привычка из разработки и отладки: маленькие шаги, частая проверка.
Этап 0 — каркас проекта
Цель этапа — создать стандартную структуру WAR-проекта Maven.
Web-приложение на Java не начинается с public static void main (точка входа JVM). Точку входа задаёт servlet-контейнер: он читает WEB-INF/web.xml, поднимает сервлеты и слушатели, затем ждёт HTTP-запросы. Maven для таких проектов использует packaging war и каталог src/main/webapp — см. Структура и сборки Java-проектов.
Создайте корневую папку:
mkdir jsf-simple && cd jsf-simple
Каталоги (Windows PowerShell):
New-Item -ItemType Directory -Force -Path `
src/main/java/com/example/bean, `
src/main/java/com/example/model, `
src/main/webapp/WEB-INF, `
src/main/webapp/resources/css
Итоговая схема:
jsf-simple/
├── pom.xml
└── src/main/
├── java/com/example/
│ ├── bean/ ← TodoBean (controller)
│ └── model/ ← Task (model)
└── webapp/
├── index.xhtml ← view
├── WEB-INF/
│ ├── web.xml
│ └── beans.xml
└── resources/css/
└── styles.css
Разбор структуры
src/main/java— компилируемый Java-код; пакеты повторяют путь папок (com.example.bean).src/main/webapp— корень web-приложения; отсюда контейнер раздаёт страницы и статику.WEB-INF— защищённая зона: файлы внутри не доступны по прямому URL из браузера.resources/css/— соглашение JSF для статических ресурсов;library="css"вh:outputStylesheetуказывает на эту папку.
Этап 1 — pom.xml
Цель этапа — подключить Jakarta Faces 4, CDI (Weld) и плагин Jetty для локального запуска.
Файл pom.xml (Project Object Model) описывает координаты артефакта, версии Java, зависимости и плагины сборки. Для JSF нужны три группы библиотек:
- Servlet API — контракт HTTP-сервлета (реализацию даёт Jetty/Tomcat).
- JSF API + реализация (Mojarra) — компоненты, lifecycle, EL-интеграция.
- CDI (Weld) —
@Named,@SessionScoped, discovery бинов.
Создайте pom.xml в корне проекта:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jsf-simple</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>JSF Simple Todo App</name>
<description>Simple JavaServer Faces web application</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jakarta.faces.version>4.0.5</jakarta.faces.version>
<jetty.port>8080</jetty.port>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.faces</groupId>
<artifactId>jakarta.faces-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.faces</artifactId>
<version>${jakarta.faces.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet-core</artifactId>
<version>5.1.2.Final</version>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
<build>
<finalName>jsf-simple</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>11.0.20</version>
<configuration>
<httpConnector>
<port>${jetty.port}</port>
</httpConnector>
<webApp>
<contextPath>/</contextPath>
</webApp>
</configuration>
</plugin>
</plugins>
</build>
</project>
Разбор зависимостей
| Зависимость | Назначение |
|---|---|
jakarta.servlet-api (provided) | Типы HttpServlet, ServletContext; в WAR не кладётся — их даёт контейнер |
jakarta.faces-api | Интерфaces JSF — UIComponent, FacesContext |
jakarta.faces (Mojarra) | Реализация спецификации — парсер Facelets, рендереры HTML |
weld-servlet-core | CDI в "голом" servlet-контейнере без полного Java EE-сервера |
jakarta.enterprise.cdi-api | Аннотации @Named, @SessionScoped |
jakarta.inject-api | @Inject для будущего расширения |
Разбор сборки
<packaging>war</packaging>— итогmvn package→target/jsf-simple.war.maven-war-plugin— упаковываетwebappи скомпилированные классы.jetty-maven-plugin— командаmvn jetty:runподнимает приложение на http://localhost:8080/ без отдельной установки Tomcat.
Проверка:
mvn -q validate
Если Maven не находит артефакты — проверьте интернет и зеркала репозитория (первая программа на Java, раздел про Maven).
Этап 2 — web.xml и beans.xml
Цель этапа — зарегистрировать FacesServlet, включить CDI и указать стартовую страницу.
Два конфигурационных файла в WEB-INF задают поведение контейнера и CDI. Без них страница index.xhtml либо отдастся как статический XML, либо bean todoBean не будет найден в EL.
web.xml — дескриптор web-приложения
src/main/webapp/WEB-INF/web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<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>JSF Simple Todo App</display-name>
<listener>
<listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
</listener>
<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>
<welcome-file-list>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
<context-param>
<param-name>jakarta.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<context-param>
<param-name>jakarta.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
</web-app>
Разбор web.xml
<listener>Weld — при старте приложения инициализирует CDI-контейнер; без него@Namedне сработает.FacesServlet— центральный контроллер JSF (раздел про FacesServlet);load-on-startupгрузит его при деплое, а не при первом запросе.url-pattern*.xhtml— любой запрос кindex.xhtmlпроходит через lifecycle JSF, а не через default servlet.welcome-file-list— при обращении к/контейнер перенаправит наindex.xhtml.PROJECT_STAGE=Development— подробные сообщения об ошибках в логах; в production ставятProduction.FACELETS_SKIP_COMMENTS=true— HTML-комментарии<!-- -->в.xhtmlне попадают в вывод (удобно для учебных пометок в разметке).
beans.xml — активация CDI
src/main/webapp/WEB-INF/beans.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans 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/beans_4_0.xsd"
bean-discovery-mode="all">
</beans>
Разбор beans.xml
- Файл может быть пустым по содержанию, но его наличие говорит Weld сканировать классы приложения.
bean-discovery-mode="all"— CDI видит все классы с scope-аннотациями, не только с@Named.- Связь с JavaBeans — компонентная модель: managed bean в JSF 2.2+ — это CDI-бин с EL-именем.
Этап 3 — модель Task
Цель этапа — описать сущность задачи — заголовок, момент создания, флаг "выполнено".
Model в MVC — данные и правила предметной области. Task не знает про HTTP, XHTML и JSF; это обычный Java-класс. Такое разделение упрощает тесты и последующее подключение JPA (Hibernate и JPA — практический старт).
src/main/java/com/example/model/Task.java:
package com.example.model;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Task implements Serializable {
private static final long serialVersionUID = 1L;
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private final String title;
private final LocalDateTime createdAt;
private boolean completed;
public Task(String title) {
this.title = title;
this.createdAt = LocalDateTime.now();
}
public String getTitle() {
return title;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public String getFormattedCreatedAt() {
return createdAt.format(FORMATTER);
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
}
Разбор класса Task
implements Serializable—@SessionScopedbean и объекты в сессии могут сериализоваться между запросами; безserialVersionUIDпри изменении класса сессии "ломаются" после redeploy.titleиcreatedAt—final— после создания задачи заголовок и время не меняются; меняется толькоcompleted. Так проще рассуждать о состоянии строки таблицы.LocalDateTime— современный API даты/времени (java.time); форматирование вынесено вgetFormattedCreatedAt(), чтобы XHTML не вызывалDateTimeFormatterнапрямую.isCompleted/setCompleted— JavaBean-имена для boolean-свойства; JSF и EL обращаются к нему как#{task.completed}.getFormattedCreatedAt()— "вычисляемое свойство" для View: в EL пишем#{task.formattedCreatedAt}, без скобок и без знания формата на странице.
Этап 4 — managed bean TodoBean
Цель этапа — инкапсулировать список задач и действия "добавить" / "удалить".
Controller в нашем приложении — TodoBean. Он хранит коллекцию Task, принимает текст новой задачи, валидирует ввод и отдаёт данные представлению через getters. Это managed bean в области session — один экземпляр на пользователя, пока жива HTTP-сессия (сессия и postback).
src/main/java/com/example/bean/TodoBean.java:
package com.example.bean;
import com.example.model.Task;
import jakarta.enterprise.context.SessionScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Named("todoBean")
@SessionScoped
public class TodoBean implements Serializable {
private static final long serialVersionUID = 1L;
private final List<Task> tasks = new ArrayList<>();
private String newTaskTitle;
public void addTask() {
if (newTaskTitle == null || newTaskTitle.isBlank()) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN,
"Введите название задачи", null));
return;
}
tasks.add(new Task(newTaskTitle.trim()));
newTaskTitle = null;
}
public void removeTask(Task task) {
tasks.remove(task);
}
public List<Task> getTasks() {
return tasks;
}
public int getCompletedCount() {
return (int) tasks.stream().filter(Task::isCompleted).count();
}
public String getNewTaskTitle() {
return newTaskTitle;
}
public void setNewTaskTitle(String newTaskTitle) {
this.newTaskTitle = newTaskTitle;
}
}
Разбор аннотаций
@Named("todoBean")— регистрирует бин в CDI и задаёт имя для EL; на странице пишем#{todoBean...}, не полное имя класса.@SessionScoped— бин создаётся при первом обращении и живёт в HTTP-сессии; список задач сохраняется между postback-запросами и перезагрузками страницы (F5), пока не истечёт сессия.- Альтернативы из теории JSF:
@RequestScoped(новый бин на каждый запрос),@ViewScoped(на одну JSF-страницу) — для todo-списка логична именно session.
Разбор методов
| Метод | Тип в JSF | Поведение |
|---|---|---|
addTask() | Action method | Вызывается кнопкой; void — остаёмся на той же странице |
removeTask(Task) | Action method с параметром | JSF сопоставляет строку dataTable с объектом Task |
getTasks() | Свойство tasks | Питает h:dataTable value="#{todoBean.tasks}" |
getCompletedCount() | Свойство completedCount | EL опускает get и () |
get/setNewTaskTitle | Двусторонняя привязка | h:inputText value="#{todoBean.newTaskTitle}" |
Разбор addTask()
newTaskTitle.isBlank()— проверка пустого ввода (строки в Java); при ошибке добавляетсяFacesMessageс уровнемWARN.- Первый аргумент
addMessage(null, ...)— глобальное сообщение; его покажетh:messages globalOnly="true". - После успешного добавления
newTaskTitle = null— поле ввода очистится при следующем Render Response.
Разбор getCompletedCount()
- Использует Stream API:
filter(Task::isCompleted).count(). - Вызывается при каждом рендере блока статистики — для учебного списка это нормально; в больших таблицах счётчик кешируют или пересчитывают по событию.
Сборка:
mvn -q compile
Этап 5 — страница index.xhtml, форма
Цель этапа — сверстать форму добавления и вывод сообщений. Таблицу добавим на следующем этапе.
View — Facelets-страница .xhtml. Теги с префиксом h: — HTML-компоненты JSF (inputText, commandButton, form); f: — ядро (валидаторы, AJAX, facet'ы заголовков колонок). Пространства имён Jakarta Faces 4: xmlns:h="jakarta.faces.html", xmlns:f="jakarta.faces.core" (Facelets и префиксы).
Создайте src/main/webapp/index.xhtml:
<!DOCTYPE html>
<html lang="ru"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head>
<title>Список задач — JSF</title>
</h:head>
<h:body>
<div class="container">
<header>
<h1>Список задач</h1>
<p class="subtitle">Простое приложение на JavaServer Faces</p>
</header>
<h:form id="todoForm">
<h:messages globalOnly="true" styleClass="messages"/>
<div class="add-form">
<h:inputText id="newTask"
value="#{todoBean.newTaskTitle}"
placeholder="Новая задача..."
styleClass="task-input"/>
<h:commandButton value="Добавить"
action="#{todoBean.addTask}"
styleClass="btn btn-primary"/>
</div>
</h:form>
</div>
</h:body>
</html>
Разбор разметки
<h:form>— обязательная обёртка для интерактивных компонентов. При submit браузер отправляет POST (postback) с скрытыми полямиjavax.faces.ViewState— сериализованное состояние дерева компонентов.value="#{todoBean.newTaskTitle}"— двусторонняя привязка (two-way binding):- на фазе Apply Request Values JSF записывает текст из input в bean;
- на Render Response — подставляет значение из bean в HTML.
action="#{todoBean.addTask}"— на фазе Invoke Application вызывается метод без аргументов.h:messages globalOnly="true"— выводит только сообщения, добавленные сclientId = null(наши предупреждения о пустом поле).styleClass— JSF-аналог HTML-атрибутаclass; рендерер превратит его вclass="task-input".
Что происходит при нажатии "Добавить"
- Restore View — восстановление дерева компонентов (из ViewState или первичное построение).
- Apply Request Values —
newTaskTitle← текст из поля. - Process Validations — в этом примере валидаторов нет.
- Update Model Values — подтверждение значений в bean.
- Invoke Application —
todoBean.addTask(). - Render Response — HTML страницы с обновлённым полем и сообщением (если было).
Подробная таблица фаз — в теории JSF.
Запуск:
mvn jetty:run
Откройте http://localhost:8080/. Задачу можно добавить, но список пока не виден — модель и bean уже работают; не хватает только h:dataTable на следующем этапе.
Этап 6 — таблица, статистика и AJAX
Цель этапа — вывести список в h:dataTable и обновлять "выполнено" без полной перезагрузки страницы.
h:dataTable — итератор по коллекции: атрибут value указывает на List, var задаёт имя строки для EL. f:ajax встроен в JSF 2+ (AJAX в JSF) и запускает сокращённый lifecycle только для указанных компонентов.
Замените содержимое <h:form>...</h:form> в index.xhtml:
<h:form id="todoForm">
<h:messages globalOnly="true" styleClass="messages"/>
<div class="add-form">
<h:inputText id="newTask"
value="#{todoBean.newTaskTitle}"
placeholder="Новая задача..."
styleClass="task-input"/>
<h:commandButton value="Добавить"
action="#{todoBean.addTask}"
styleClass="btn btn-primary"/>
</div>
<div class="stats">
Всего: <strong>#{todoBean.tasks.size()}</strong>
· Выполнено: <strong>#{todoBean.completedCount}</strong>
</div>
<h:dataTable value="#{todoBean.tasks}"
var="task"
styleClass="task-table"
rendered="#{not empty todoBean.tasks}">
<h:column>
<f:facet name="header">Статус</f:facet>
<h:selectBooleanCheckbox value="#{task.completed}">
<f:ajax event="change" render="@form"/>
</h:selectBooleanCheckbox>
</h:column>
<h:column>
<f:facet name="header">Задача</f:facet>
<span class="#{task.completed ? 'completed' : ''}">#{task.title}</span>
</h:column>
<h:column>
<f:facet name="header">Создана</f:facet>
#{task.formattedCreatedAt}
</h:column>
<h:column>
<f:facet name="header"/>
<h:commandButton value="Удалить"
action="#{todoBean.removeTask(task)}"
styleClass="btn btn-danger"/>
</h:column>
</h:dataTable>
<h:panelGroup layout="block" styleClass="empty-hint"
rendered="#{empty todoBean.tasks}">
Список пуст. Добавьте первую задачу выше.
</h:panelGroup>
</h:form>
Разбор таблицы и EL
#{todoBean.tasks.size()}— в EL можно вызывать методы без аргументов;size()списка обновляется после каждого add/remove.rendered="#{not empty todoBean.tasks}"— компонент не рендерится, если список пуст; это JSF-way условного UI (предпочтительнее JSTL<c:if>внутри формы — см. JSTL и JSF).var="task"— внутри строк таблицы#{task.title}ссылается на текущий элементList<Task>.value="#{task.completed}"на чекбоксе — двусторонняя привязка кTask.setCompleted(); при AJAX postback JSF найдёт тот же объект в session-scoped списке.
Разбор AJAX
<f:ajax event="change" render="@form"/>
event="change"— AJAX срабатывает при переключении чекбокса.render="@form"— после Invoke Application перерисовать всю форму — статистику "Выполнено", зачёркивание текста, счётчик "Всего".- Без
f:ajaxпользователю пришлось бы нажимать отдельную кнопку "Обновить" — полный postback.
Разбор удаления
action="#{todoBean.removeTask(task)}"— JSF передаёт в метод объект строки, с которой связана кнопка.- После удаления lifecycle доходит до Render Response — таблица перестраивается без удалённой строки.
Разбор пустого состояния
h:panelGroup layout="block"— рендерит<div>с текстом-подсказкой.rendered="#{empty todoBean.tasks}"— виден только когда таблица скрыта; пользователь не видит пустую<table>.
Этап 7 — стили CSS
Цель этапа — оформить страницу и подключить таблицу стилей через JSF, а не жёстким <link>.
Статические ресурсы JSF лежат в src/main/webapp/resources/<library>/. Компонент h:outputStylesheet генерирует URL с хешем версии — браузер корректно кеширует CSS при redeploy.
В <h:head> добавьте сразу после <title>:
<h:outputStylesheet library="css" name="styles.css"/>
Создайте src/main/webapp/resources/css/styles.css:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem 1rem;
color: #333;
}
.container {
max-width: 720px;
margin: 0 auto;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header h1 {
font-size: 1.75rem;
color: #2d3748;
}
.subtitle {
color: #718096;
margin-top: 0.25rem;
font-size: 0.95rem;
}
.add-form {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.task-input {
flex: 1;
padding: 0.65rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
.task-input:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 0.65rem 1.25rem;
border: none;
border-radius: 8px;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
}
.btn:active {
transform: scale(0.98);
}
.btn-primary {
background: #667eea;
color: #fff;
}
.btn-primary:hover {
background: #5a67d8;
}
.btn-danger {
background: #fc8181;
color: #fff;
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
}
.btn-danger:hover {
background: #f56565;
}
.stats {
color: #718096;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.task-table {
width: 100%;
border-collapse: collapse;
}
.task-table th {
text-align: left;
padding: 0.75rem;
border-bottom: 2px solid #e2e8f0;
color: #4a5568;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.task-table td {
padding: 0.75rem;
border-bottom: 1px solid #edf2f7;
vertical-align: middle;
}
.task-table tr:hover td {
background: #f7fafc;
}
.completed {
text-decoration: line-through;
color: #a0aec0;
}
.empty-hint {
text-align: center;
color: #a0aec0;
padding: 2rem;
font-style: italic;
}
.messages {
list-style: none;
margin-bottom: 1rem;
}
.messages li {
padding: 0.75rem 1rem;
border-radius: 8px;
background: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
}
Разбор CSS и JSF
- Класс
.completedсвязан с EL на span:class="#{task.completed ? 'completed' : ''}"— при AJAX-обновлении формы зачёркивание появляется без F5. .messages— стилизует<ul>, который рендеритh:messages.h:outputStylesheetотдаёт URL вида/javax.faces.resource/styles.css.xhtml?ln=css— не путайте с прямым путём/resources/css/styles.css, который в JSF может не сработать без resource handler.
Общие принципы вёрстки форм — в веб-разделе; здесь CSS минимален и не претендует на дизайн-систему.
Этап 8 — запуск и проверка
Цель этапа — убедиться, что WAR собирается и все пользовательские сценарии работают.
mvn clean package
mvn jetty:run
Чек-лист ручного тестирования
- http://localhost:8080/ открывается без 404 — значит,
welcome-fileиFacesServletнастроены верно. - Кнопка "Добавить" с пустым полем — жёлтое предупреждение "Введите название задачи".
- Текст задачи появляется в таблице с датой в формате
dd.MM.yyyy HH:mm. - Чекбокс зачёркивает текст и увеличивает "Выполнено" без мигания всей страницы — работает
f:ajax. - "Удалить" убирает строку; при пустом списке видна подсказка "Список пуст…".
- F5 — задачи остаются — подтверждение
@SessionScopedи HTTP-сессии. - Новая вкладка браузера (или другой браузер) — отдельная сессия, список пуст (ожидаемо).
Альтернатива Jetty — Tomcat
После mvn package скопируйте target/jsf-simple.war в webapps Tomcat 10.1+ (Jakarta EE, не Tomcat 9 с javax.*). Приложение будет на http://localhost:8080/jsf-simple/ — контекст совпадает с именем WAR.
Импортируйте Maven-проект в IntelliJ IDEA, запустите конфигурацию "Jetty" или "Tomcat Local" и поставьте breakpoint в addTask() — на postback выполнение остановится в bean. Общие приёмы — Отладка Java-кода в IDE.
Частые ошибки
| Симптом | Вероятная причина | Что проверить |
|---|---|---|
404 на /index.xhtml | Нет servlet-mapping для FacesServlet | web.xml, шаблон *.xhtml |
EL todoBean not found | CDI не инициализирован | beans.xml, Weld listener, @Named |
PropertyNotFoundException | Нет getter/setter или опечатка в EL | JavaBean-имена, ООП и инкапсуляция |
ViewExpiredException | Истекла HTTP-сессия на сервере | Увеличить timeout или @SessionScoped + redirect |
| Стили не применяются | Неверный library или путь файла | resources/css/styles.css, library="css" |
| Чекбокс не обновляет счётчик | Нет f:ajax или узкий render | render="@form" или явный список id |
package jakarta.faces does not exist | JDK < 17 или не скачались зависимости | JDK 17+, mvn dependency:resolve |
| Кракозябры в тексте | Нет UTF-8 в проекте | project.build.sourceEncoding, заголовок ответа |
Куда развивать дальше
- Bean Validation —
@NotBlankнаnewTaskTitle,<f:validateBean/>, сообщения рядом с полем (валидация в JSF). - JPA и БД — сущность
@Entity Task,@PersistenceContext, список переживает перезапуск сервера (Hibernate и JPA, работа с БД). - Навигация — страницы
edit.xhtml,faces-config.xmlили implicit navigation по outcome. - PrimeFaces — богатые таблицы, диалоги, фильтры (первая программа с PrimeFaces).
- Сервисный слой — вынести логику из bean в
@ApplicationScoped TodoService(разделение UI и домена). - Современный REST-стек — для greenfield-проектов чаще Spring Boot; JSF остаётся навыком сопровождения legacy.
Связанные статьи
- JavaServer Faces — фреймворк для веб-интерфейсов — lifecycle, CDI, AJAX, PrimeFaces
- Первая программа на JavaServer Faces — счётчик, Maven-каркас, PrimeFaces
- JavaBeans — компонентная модель — свойства, события, связь с CDI
- Структура и сборки Java-проектов — WAR, Maven, плагины
- Stream API в Java —
filter,countвgetCompletedCount - Ключевые классы —
java.time—LocalDateTime, форматирование - HTTP как основа веб-интеграций — POST, сессия, postback
- Отладка Java-кода в IDE — breakpoint на action method
Legacy и веб на Java — JavaServer Faces — теория, Первая программа на JSF, Практикум "Список задач", Spring Framework, Первая программа на Spring Boot.