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

Практикум 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.
Legacy, но учебно ценно

В новых 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Разметка и компоненты UIindex.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:commandButtonTodoBean.addTask()
Отметить "выполнено"h:selectBooleanCheckbox, f:ajaxTask.setCompleted()
Удалить строкуh:commandButtonTodoBean.removeTask(task)
Отправить пустое полеh:messagesFacesMessage в addTask()
Увидеть статистикуEL в разметкеtasks.size(), completedCount

Карта этапов

ЭтапФокусРезультат
0Каркас Maven WARПустая структура каталогов
1pom.xmlЗависимости JSF, CDI, Jetty
2web.xml, beans.xmlFacesServlet и CDI
3TaskМодель задачи
4TodoBeanЛогика списка, @SessionScoped
5index.xhtml, формаДобавление и сообщения
6Таблица и AJAXh:dataTable, f:ajax
7CSSОформление страницы
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 нужны три группы библиотек:

  1. Servlet API — контракт HTTP-сервлета (реализацию даёт Jetty/Tomcat).
  2. JSF API + реализация (Mojarra) — компоненты, lifecycle, EL-интеграция.
  3. 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-coreCDI в "голом" servlet-контейнере без полного Java EE-сервера
jakarta.enterprise.cdi-apiАннотации @Named, @SessionScoped
jakarta.inject-api@Inject для будущего расширения

Разбор сборки

  • <packaging>war</packaging> — итог mvn packagetarget/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@SessionScoped bean и объекты в сессии могут сериализоваться между запросами; без serialVersionUID при изменении класса сессии "ломаются" после redeploy.
  • title и createdAtfinal — после создания задачи заголовок и время не меняются; меняется только 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()Свойство completedCountEL опускает 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".

Что происходит при нажатии "Добавить"

  1. Restore View — восстановление дерева компонентов (из ViewState или первичное построение).
  2. Apply Request ValuesnewTaskTitle ← текст из поля.
  3. Process Validations — в этом примере валидаторов нет.
  4. Update Model Values — подтверждение значений в bean.
  5. Invoke ApplicationtodoBean.addTask().
  6. 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

Чек-лист ручного тестирования

  1. http://localhost:8080/ открывается без 404 — значит, welcome-file и FacesServlet настроены верно.
  2. Кнопка "Добавить" с пустым полем — жёлтое предупреждение "Введите название задачи".
  3. Текст задачи появляется в таблице с датой в формате dd.MM.yyyy HH:mm.
  4. Чекбокс зачёркивает текст и увеличивает "Выполнено" без мигания всей страницы — работает f:ajax.
  5. "Удалить" убирает строку; при пустом списке видна подсказка "Список пуст…".
  6. F5 — задачи остаются — подтверждение @SessionScoped и HTTP-сессии.
  7. Новая вкладка браузера (или другой браузер) — отдельная сессия, список пуст (ожидаемо).

Альтернатива Jetty — Tomcat

После mvn package скопируйте target/jsf-simple.war в webapps Tomcat 10.1+ (Jakarta EE, не Tomcat 9 с javax.*). Приложение будет на http://localhost:8080/jsf-simple/ — контекст совпадает с именем WAR.

Отладка в IDE

Импортируйте Maven-проект в IntelliJ IDEA, запустите конфигурацию "Jetty" или "Tomcat Local" и поставьте breakpoint в addTask() — на postback выполнение остановится в bean. Общие приёмы — Отладка Java-кода в IDE.


Частые ошибки

СимптомВероятная причинаЧто проверить
404 на /index.xhtmlНет servlet-mapping для FacesServletweb.xml, шаблон *.xhtml
EL todoBean not foundCDI не инициализированbeans.xml, Weld listener, @Named
PropertyNotFoundExceptionНет getter/setter или опечатка в ELJavaBean-имена, ООП и инкапсуляция
ViewExpiredExceptionИстекла HTTP-сессия на сервереУвеличить timeout или @SessionScoped + redirect
Стили не применяютсяНеверный library или путь файлаresources/css/styles.css, library="css"
Чекбокс не обновляет счётчикНет f:ajax или узкий renderrender="@form" или явный список id
package jakarta.faces does not existJDK < 17 или не скачались зависимостиJDK 17+, mvn dependency:resolve
Кракозябры в текстеНет UTF-8 в проектеproject.build.sourceEncoding, заголовок ответа

Куда развивать дальше

  1. Bean Validation@NotBlank на newTaskTitle, <f:validateBean/>, сообщения рядом с полем (валидация в JSF).
  2. JPA и БД — сущность @Entity Task, @PersistenceContext, список переживает перезапуск сервера (Hibernate и JPA, работа с БД).
  3. Навигация — страницы edit.xhtml, faces-config.xml или implicit navigation по outcome.
  4. PrimeFaces — богатые таблицы, диалоги, фильтры (первая программа с PrimeFaces).
  5. Сервисный слой — вынести логику из bean в @ApplicationScoped TodoService (разделение UI и домена).
  6. Современный REST-стек — для greenfield-проектов чаще Spring Boot; JSF остаётся навыком сопровождения legacy.

Связанные статьи


Legacy и веб на JavaJavaServer Faces — теория, Первая программа на JSF, Практикум "Список задач", Spring Framework, Первая программа на Spring Boot.