«1. Введение

JavaServer Faces — это инфраструктура пользовательского интерфейса на основе серверных компонентов. Первоначально он разрабатывался как часть Jakarta EE. В этом руководстве мы рассмотрим, как интегрировать JSF в приложение Spring Boot.

В качестве примера мы реализуем простое приложение для создания списка дел.

2. Зависимости Maven

Мы должны расширить наш pom.xml для использования технологий JSF:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!--JSF-->
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.faces</artifactId>
    <version>2.3.7</version>
</dependency>

Артефакт javax.faces также содержит API JSF и реализации. Подробную информацию можно найти здесь.

3. Настройка сервлета JSF

Среда JSF использует файлы XHTML для описания содержимого и структуры пользовательского интерфейса. На стороне сервера файлы JSF создаются из описаний XHTML.

Давайте начнем с создания статической структуры в файле index.xhtml в каталоге src/main/webapp:

<f:view xmlns="http://www.w3c.org/1999/xhtml"
  xmlns:f="http://java.sun.com/jsf/core"
  xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <meta charset="utf-8"/>
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
        <title>TO-DO application</title>
    </h:head>
    <h:body>
        <div>
            <p>Welcome in the TO-DO application!</p>
            <p style="height:50px">
                This is a static message rendered from xhtml.
            </p>
        </div>
    </h:body>
</f:view>

Содержимое будет доступно по адресу \u003cyour-url\u003e/index.jsf. Тем не менее, мы получаем сообщение об ошибке на стороне клиента, если попытаемся получить доступ к содержимому на этом этапе:

There was an unexpected error (type=Not Found, status=404).
No message available

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

Так как мы находимся в Spring Boot, мы можем легко расширить наш класс приложения для обработки требуемой конфигурации:

@SpringBootApplication
public class JsfApplication extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(JsfApplication.class, args);
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean() {
        FacesServlet servlet = new FacesServlet();
        ServletRegistrationBean servletRegistrationBean = 
          new ServletRegistrationBean(servlet, "*.jsf");
        return servletRegistrationBean;
    }
}

Это выглядит великолепно и довольно разумно, но, к сожалению, все еще недостаточно хорошо. Теперь, когда мы попытаемся открыть \u003cyour-url\u003e/index.jsf, мы получим еще одну ошибку:

java.lang.IllegalStateException: Could not find backup for factory javax.faces.context.FacesContextFactory.

К сожалению, помимо конфигурации Java нам нужен файл web.xml. Давайте создадим его в src/webapp/WEB-INF:

<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>*.jsf</url-pattern>
</servlet-mapping>

Теперь наша конфигурация готова к работе. Откройте \u003cyour-url\u003e/index.jsf:

Welcome in the TO-DO application!

This is a static message rendered from xhtml.

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

4. Реализация шаблона DAO

DAO означает объект доступа к данным. Обычно класс DAO отвечает за два понятия. Инкапсуляция деталей уровня сохраняемости и предоставление интерфейса CRUD для одного объекта. Подробное описание вы найдете в этом уроке.

Чтобы реализовать шаблон DAO, мы сначала определим общий интерфейс:

public interface Dao<T> {

    Optional<T> get(int id);
    Collection<T> getAll();
    int save(T t);
    void update(T t);
    void delete(T t);
}

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

public class Todo {

    private int id;
    private String message;
    private int priority;

    // standard getters and setters

}

Следующий класс будет быть реализацией Dao\u003cTodo\u003e. Прелесть этого шаблона в том, что мы можем предоставить новую реализацию этого интерфейса в любое время.

Следовательно, мы можем изменить уровень сохраняемости, не затрагивая остальную часть кода.

В нашем примере мы будем использовать класс хранения в памяти:

@Component
public class TodoDao implements Dao<Todo> {

    private List<Todo> todoList = new ArrayList<>();
    
    @Override
    public Optional<Todo> get(int id) {
        return Optional.ofNullable(todoList.get(id));
    }

    @Override
    public Collection<Todo> getAll() {
        return todoList.stream()
          .filter(Objects::nonNull)
          .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
    }

    @Override
    public int save(Todo todo) {
        todoList.add(todo);
        int index = todoList.size() - 1;
        todo.setId(index);
        return index;
    }

    @Override
    public void update(Todo todo) {
        todoList.set(todo.getId(), todo);
    }

    @Override
    public void delete(Todo todo) {
        todoList.set(todo.getId(), null);
    }
}

5. Сервисный уровень

Основной целью уровня DAO является обработка деталей механизма сохраняемости. В то время как сервисный уровень стоит над ним для обработки бизнес-требований.

Обратите внимание, что на интерфейс DAO будет ссылаться служба:

@Scope(value = "session")
@Component(value = "todoService")
public class TodoService {

    @Autowired
    private Dao<Todo> todoDao;
    private Todo todo = new Todo();

    public void save() {
        todoDao.save(todo);
        todo = new Todo();
    }

    public Collection<Todo> getAllTodo() {
        return todoDao.getAll();
    }

    public int saveTodo(Todo todo) {
        validate(todo);
        return todoDao.save(todo);
    }

    private void validate(Todo todo) {
        // Details omitted
    }

    public Todo getTodo() {
        return todo;
    }
}

Здесь служба является именованным компонентом. Мы будем использовать это имя для ссылки на компонент из контекста JSF.

Кроме того, этот класс имеет область сеанса, которая будет удовлетворять этому простому приложению.

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

Дополнительные инструкции по этому вопросу доступны в этом руководстве.

6. Контроллер

Как и в JSP-приложении, контроллер будет управлять навигацией между различными представлениями.

Далее мы реализуем минималистичный контроллер. Он будет переходить с начальной страницы на страницу списка дел:

@Scope(value = "session")
@Component(value = "jsfController")
public class JsfController {

    public String loadTodoPage() {
        checkPermission();
        return "/todo.xhtml";
    }

    private void checkPermission() {
        // Details omitted
    }
}

Навигация основана на возвращаемом имени. Следовательно, loadTodoPage отправит нас на страницу todo.xhtml, которую мы реализуем дальше.

7. Соединение JSF и Spring Beans

Давайте посмотрим, как мы можем ссылаться на наши компоненты из контекста JSF. Во-первых, мы расширим index.xthml:

<f:view 
  xmlns="http://www.w3c.org/1999/xhtml"
  xmlns:f="http://java.sun.com/jsf/core"
  xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
       // same code as before
    </h:head>
    <h:body>
        <div>
           // same code as before
           <h:form>
             <h:commandButton value="Load To-do page!" action="#{jsfController.loadTodoPage}" />
           </h:form>
        </div>
    </h:body>
</f:view>

Здесь мы добавили commandButton внутри элемента формы. Это важно, поскольку каждый элемент UICommand (например, commandButton) должен быть помещен внутри элемента UIForm (например, формы).

«На этом этапе мы можем запустить наше приложение и изучить \u003cyour-url\u003e/index.jsf:

К сожалению, мы получим ошибку при нажатии на кнопку:

There was an unexpected error (type=Internal Server Error, status=500).
javax.el.PropertyNotFoundException:
/index.xhtml @11,104 action="#{jsfController.loadTodoPage}":
Target Unreachable, identifier [jsfController] resolved to null

В сообщении четко указано проблема: jsfController разрешен к нулю. Соответствующий компонент либо не создан, либо, по крайней мере, невидим из контекста JSF.

В данной ситуации верно последнее.

Нам нужно связать контекст Spring с контекстом JSF в файле webapp/WEB-INF/faces-config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd"
  version="2.2">
    <application>
        <el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
    </application>
</faces-config>

Теперь, когда наш контроллер готов к работе, нам понадобится файл todo.xhtml!

8. Взаимодействие с сервисом из JSF

Наша страница todo.xhtml будет иметь две цели. Во-первых, он будет отображать все элементы списка дел.

Во-вторых, предлагайте возможность добавлять в список новые элементы.

Для этого компонент пользовательского интерфейса будет напрямую взаимодействовать с объявленным ранее сервисом:

<f:view xmlns="http://www.w3c.org/1999/xhtml"
  xmlns:f="http://java.sun.com/jsf/core"
  xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <meta charset="utf-8"/>
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
        <title>TO-DO application</title>
    </h:head>
    <h:body>
        <div>
            <div>
                List of TO-DO items
            </div>
            <h:dataTable value="#{todoService.allTodo}" var="item">
                <h:column>
                    <f:facet name="header"> Message</f:facet>
                    #{item.message}
                </h:column>
                <h:column>
                    <f:facet name="header"> Priority</f:facet>
                    #{item.priority}
                </h:column>
            </h:dataTable>
        </div>
        <div>
            <div>
                Add new to-do item:
            </div>
            <h:form>
                <h:outputLabel for="message" value="Message: "/>
                <h:inputText id="message" value="#{todoService.todo.message}"/>
                <h:outputLabel for="priority" value="Priority: "/>
                <h:inputText id="priority" value="#{todoService.todo.priority}" converterMessage="Please enter digits only."/>
                <h:commandButton value="Save" action="#{todoService.save}"/>
            </h:form>
        </div>
    </h:body>
</f:view>

Вышеупомянутые две цели реализованы в двух отдельных элементах div.

В первом случае мы использовали элемент dataTable для представления всех значений из todoService.AllTodo.

Второй div содержит форму, в которой мы можем изменить состояние объекта Todo в TodoService.

Мы используем элемент inputText для приема пользовательского ввода, где второй ввод автоматически преобразуется в тип int. С помощью commandButton пользователь может сохранить (сейчас в памяти) объект Todo с помощью todoService.save.

9. Заключение

Фреймворк JSF можно интегрировать в фреймворк Spring. Вы должны выбрать, какой фреймворк будет управлять bean-компонентами. В этом уроке мы использовали фреймворк Spring.

Однако модель области немного отличается от модели JSF. Таким образом, вы можете рассмотреть возможность определения пользовательских областей в контексте Spring.

Как всегда, код доступен на GitHub.