Приятно познакомиться, Java Server Faces

Попробую рассказать и показать как писать JSF приложения. Начнём с простейших примеров, чтобы показать общую структуру приложения и объяснить что и для чего нужно.

Итак, поехали.

Пару слов о том, что вообще из себя представляет JSF. JSF - веб-MVC фреймворк, который основан на использовании компонентов и является частью JAVA EE.  Для базового понимания о том, как выглядит приложение на JSF нужно знать всего несколько вещей.

Первое - за представление в JSF отвечают JSP страницы или Facelets шаблоны. Вообще, Facelet'ы были созданы для замены JSP, но девизом jsp можно назвать следующее: "То, что мертво, умереть не может", поэтому в качестве view всё ещё временами им пользуются. Но для простоты понимания мы забудем о JSP и рассмотрим только facelets-шаблоны. По сути Facelets, это xml документы, который описывают страницы и умеют в jsf компоненты.

Вторая важная вещь - JSF приложения должны соответствовать требованиям Java Servlet Specification.
Исходя из этого для работы jsf приложения нужен контейнер сервлетов, а также конфигурация этих самых сервлетов.

Теперь перейдем к 'Hello, world' приложению на JSF. Для этого нам понадобится подготовить какой-нибудь сервер-приложений. Я возьму для этих целей wildfly. Если у вас  под рукой нет app sever'а, то можете воспользоваться  моей заметкой по установке и настройке wildfly.

Создадим обычное maven'овское приложение и добавим в него зависимости, которые необходимы для работы JSF.

Содержимое 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.tune-it</groupId>
    <artifactId>jsf-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>JSF example</name>
    <packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-api</artifactId>
            <version>2.2.18</version>
        </dependency>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-impl</artifactId>
            <version>2.2.18</version>
        </dependency>
    </dependencies>
</project>

Здесь стоит обратить на то, что сборка будет происходить не в jar файл, а в war файл.

 <packaging>war</packaging>

А также на то, что для корректной работы приложения нужны следующие зависимости:

<dependencies>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-api</artifactId>
            <version>2.2.18</version>
        </dependency>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-impl</artifactId>
            <version>2.2.18</version>
        </dependency>
</dependencies>

Далее создадим необходимую структуру директорий для нашего приложения.

jsfexample
-src
--main
---java
---resources
---webapp
----views
-----index.xhtml
----WEB-INF
-----web.xml
--test
-pom.xml

В этой структуре стоит обратить внимание на файл web.xml. Это дескриптор развертывания для веб-приложения. Он описан в спецификации сервлетов. Важное замечание: web.xml должен находиться внутри директории WEB-INF.

Посмотрим содержимое web.xml

<web-app version="2.5"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemalocation=" http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.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>
</web-app>

Если коротко, то здесь декларируется, так называемый, Faces Servlet, который будет отвечать за обработку всех страниц, имеющих расширение xhtml. Faces сервлет являются частью JSF и поставляется вместе с подключенными зависимостями. Он будет выполнять роль Controller'а в нашем MVC-фреймворке.

Последний, интересующий нас файл из этого примера - index.xhtml. Посмотрим на его содержимое.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">

<h:head>
    <title>Пример</title>
</h:head>
<h:body>
    Hello, world!
</h:body>
</html>

Окей, этот файл просто содержит верстку, которая будет отображаться браузером. Конечно, здесь есть несколько отличий от обычного html, но предлагаю пока не заострять на этом внимание.

Теперь попробуем собрать этот чудесный код и задеплоить в наш сервер приложений.

mvn clean package
cp target/jsf-example-1.0-SNAPSHOT.war path_to_wildfly/standalone/deployments/

У меня всё прошло успешно и в логах сервера приложения я увидел следующее:

INFO  Starting deployment of "jsf-example-1.0-SNAPSHOT.war" (runtime-name: "jsf-example-1.0-SNAPSHOT.war")
INFO  Initializing Mojarra 2.2.13.SP4  for context '/jsf-example-1.0-SNAPSHOT'
INFO  Registered web context: '/jsf-example-1.0-SNAPSHOT' for server 'default-server'

Окей, теперь идём в браузер и смотрим что получилось. Вопрос конечно в том, а куда собственно идти, но в логах сервер приложений подсказывает, что приложение зарегистрировано по следующему префиксу '/jsf-example-1.0-SNAPSHOT'. 

Поэтому, не долго думая, открою в браузере http://localhost:8080/jsf-example-1.0-SNAPSHOT/
Результат немного не тот, который ожидался. HTTP 403 Forbidden. Поэтому подправим URL, указав полный путь до xhtml файла.

http://localhost:8080/jsf-example-1.0-SNAPSHOT/views/index.xhtml

И вуаля! В браузере отображется желанная строчка.

Hello, world!

На текущий момент, использования JSF не даёт никаких преимуществ, ибо с такой задачей можно справиться голым nginx'ом и html файлом.

Давайте чуть усложним пример и будем выводить Hello + имя из Java кода.

 

 

Не так давно мы говорили о том, что в JSF - это MVC фреймворк. xhtml страницы являются View, а faces servlet - Controller. Сейчас настало время познакомиться с Model. Моделью в JSF являются, так называемые, 'Управляемые Бобы' или, если на английском, то Managed Beans. По сути бины это Java класс, помеченный некоторыми аннотациями, жизненм циклом которого управляют извне. 

Звучит достаточно абстрактно, но давайте посмотрим то, как это работает, на примере.

Добавим в наш предыдущий код в директорию java следующий класс.

package com.tuneit.example.jsf;

import javax.faces.bean.ManagedBean;

@ManagedBean
public class ExampleBean {

    private String name = "Александр";

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Теперь у нас есть управляемый боб, который содержит поле name. Давайте теперь сотворим магию.

 

 

Поправим index.xhtml, следующим образом:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">

<h:head>
    <title>Пример</title>
</h:head>
<h:body>
    Hello, #{exampleBean.name}!
</h:body>
</html>

Если после этого пересобрать и развернуть war файл, то в браузере теперь будет отображаться:

Hello, Александр!

Как видите только что призошла магическая ситуация, когда в страницу  подставились данные из Java кода. Это происходит в следующей строке кода:

#{exampleBean.name}

Здесь внутри конструкции '#{}' обращаемся к некому 'exampleBean'. JSF штука умная, и он знает, что у нас есть ManagedBean с таким именем, поэтому он обращение будет направлено к объекту этого класса.

С таким примером уже стало лучше, но нужно понимать, что сейчас вы используете фреймворк в очень простой ситуации. Объясню в общих чертах как это работает.

Когда вы делаете HTTP запрос на URL 'http://localhost:8080/jsf-example-1.0-SNAPSHOT/views/index.xhtml', то запрос поступает на обработку Faces сервлету, который определяет, что нужно отдать соответствующий xhtml файл. Перед отправкой xhtml преобразуется на сервере в html и именно на этой стадии, на сервере, происходит обращение к полям bean'а.

Так что в этом примере JSF не делает ничего больше чем любой шаблонизатор, например Freemarker.

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

Выглядеть это будет примерно так:

 

 

Для начала добавим Java класс, описывающий сотрдуника (Employee.java).

package com.tuneit.example.model;


import lombok.Data;

@Data
public class Employee {
    private String firstName;
    private String lastName;
    private Integer age;


}

Сразу скажу, что я тут себе в зависимости добавил lombok, чтобы не писать в классах Getter'ы и Setter'ы. Но вы можете этого не делать и просто добавить для каждого поля пару getter/setter.

Теперь посмотрим на то, как теперь выглядит наш боб ExampleBean:

package com.tuneit.example.jsf;

import com.tuneit.example.model.Employee;
import lombok.Data;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import java.util.ArrayList;
import java.util.List;

@ManagedBean
@ViewScoped
@Data
public class ExampleBean {

    private Employee newEmployee = new Employee();

    private List<Employee> employees = new ArrayList<Employee>();


    public void addEmployee() {
        employees.add(newEmployee);
        newEmployee = new Employee();
    }
}

Здесь появилось несколько новых полей, метод для добавления нового сотрудника в список, а также две новые аннотации.

Если с полями и методом всё понятно и так, то про аннотацию поясню.

 @Data - как вы можете видеть в импорт секции пришла из библиотеки lombok, которую я подключил, потому что лень писать Getter'ы и Setter'ы на все поля.

А вот аннотация @ViewScoped - уже часть функционала JSF. О ней я расскажу чуть позже. Для начала досмотрим пример. 

Содержимое файла index.xhtml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core">

<h:head>
    <title>Пример</title>
</h:head>
<h:body>
    <h:form id="newEmployeeForm">
        <div>
            <h:outputLabel for="first-name" value="Имя"/>
            <h:inputText id="first-name" value="#{exampleBean.newEmployee.firstName}"/>
        </div>
        <div>
        <h:outputLabel for="last-name" value="Фамилия"/>
        <h:inputText id="last-name" value="#{exampleBean.newEmployee.lastName}"/>
        </div>
        <div>
        <h:outputLabel for="age" value="Возраст"/>
        <h:inputText id="age" value="#{exampleBean.newEmployee.age}"/>
        </div>
        <h:commandButton value="Добавить" action="#{exampleBean.addEmployee()}">
            <f:ajax execute="newEmployeeForm" render="employeeTable" />
        </h:commandButton>
    </h:form>
    <h:panelGroup id="employeeTable">
        <h:dataTable value="#{exampleBean.employees}" var="employee">
            <h:column>
                <f:facet name="header">Имя</f:facet>
                #{employee.firstName}
            </h:column>

            <h:column>
                <f:facet name="header">Фамилия</f:facet>
                #{employee.lastName}
            </h:column>

            <h:column>
                <f:facet name="header">Возраст</f:facet>
                #{employee.age}
            </h:column>
        </h:dataTable>
    </h:panelGroup>
</h:body>
</html>

Что происходит в xhtml'ке? Если коротко, тот здесь добавилась форма ввода информации о новом сотруднике с тремя полями ввода (имя, фамилия, возраст) и кнопка добавления. Вот блок кода, отвечающий за рендеринг формы:

 <h:form id="newEmployeeForm">
        <div>
            <h:outputLabel for="first-name" value="Имя"/>
            <h:inputText id="first-name" value="#{exampleBean.newEmployee.firstName}"/>
        </div>
        <div>
        <h:outputLabel for="last-name" value="Фамилия"/>
        <h:inputText id="last-name" value="#{exampleBean.newEmployee.lastName}"/>
        </div>
        <div>
        <h:outputLabel for="age" value="Возраст"/>
        <h:inputText id="age" value="#{exampleBean.newEmployee.age}"/>
        </div>
        <h:commandButton value="Добавить" action="#{exampleBean.addEmployee()}">
            <f:ajax execute="newEmployeeForm" render="employeeTable" />
        </h:commandButton>
    </h:form>

Рассмотрим подробнее этот код. Есть 3 тега inputText с префиксом h. Префикс h - относится к пространству имён для jsf'ных тегов, вначале файла происходит задание префикса через xmlns.

У полей ввода есть атрибут, который связывает их с нужным полем внутри bean'а. Далее идёт тег h:commandButton, который будет показывать кнопку отправки. В атрибуте action указывается метод бина, который будет вызван при отравке формы.

Вродё всё просто. Однако, внутри кнопки есть ещё тег f:ajax, который непонятно что делает. Во-первых, стоит заметить, что для использования этого тега мы подключили ещё одно пространство (взгляните на атрибут тега html). Этот тег служит для того, чтобы делать, как можно догадаться из названия, ajax запросы. В нашем примере мы указываем, что при нажатии на кнопку, необходимо передать на сервер значения, которые содержатся внутри компонента с ИД newEmployeeForm, а после обработки события сгенирировать на   сервере и вернуть новый html код для компонента с ИД employeeTable.

Благодаря этому атрибуту, мы смогли указать, что при нажатии на кнопку, необходимо обновить значения в полях firstName, lastName, age, а после выполнения метода addEmployee вернуть новое содержимое таблицы сотрудников. 

Опущу, содержимое блока генерирующего таблицу сотрдуников, т. к. там всё достаточно просто.

А теперь вернёмся к классу ExampleBean и объясним зачем появилась аннотация @ViewScoped. Суть в том, что у каждого бина есть жизненный цикл, в рамках которого он существует. Если ничего не указывать, то по умолчанию время жизни бина ограничивается запросом.

Представим, что в нашем примере у бина стоял бы scope по умолчанию. Тогда при первом обращении к бину, нам бы вернулась страница такая, какая и нужна, однако, при заполнении формы и каждой отправке на сервер данных экземпляр класса ExampleBean создавался бы заново. И между запросами бы не сохранялось содержимое поля employees. Из-за этого в таблице всегда после добавления очищалось предыдущеё содержимое таблицы и генерировалось бы новое. Из-за этого, мы смогли бы видеть только последнего добавленного сотрудника.

Для того чтобы такого не случилось, мы указали, что scope этого бина будет 'view'. Это означает, что  бина будет создан при первом обращении к странице и не уничтожится до тех пор, пока не будет выполнен переход на другую страницу или же полная перерисовка всей страницы.

Ссылка на исходники

На этом, пожалуй, откланяюсь.

Вперед

Коротко о себе:

Работаю программистом в компании Tune IT.

Ненавижу селфи-палки.

Ещё на эту же тему:

Java SE 7: Develop Rich Client Applications
Обновление компонента с атрибутом rendered в JSF
D77738GC10 Java EE 6: Develop Web Applications with JSF
D85120GC20 Java EE 7: Front-end Web Application Development
Java EE 7: New Features