A Controller, Service and DAO Example with Spring Boot and JSF – 使用Spring Boot和JSF的控制器、服务和DAO实例

最后修改: 2018年 9月 30日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

JavaServer Faces is a server-side, component-based user interface framework. It was originally developed as part of the Jakarta EE.

JavaServer Faces是一个服务器端的、基于组件的用户界面框架。它最初是作为Jakarta EE的一部分而开发的。

In this tutorial, we’ll learn how to integrate JSF into a Spring Boot application. As an example, we’ll implement a simple application to create a TO-DO list.

在本教程中,我们将学习如何将JSF集成到Spring Boot应用程序中。作为一个例子,我们将实现一个简单的应用程序来创建一个TO-DO列表。

2. Maven Dependencies

2.Maven的依赖性

We have to extend our pom.xml to use JSF technologies:

我们必须扩展我们的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>

The javax.faces artifact contains the JSF APIs, and the implementations as well. We can find detailed information here.

javax.faces工件包含JSF的API,以及实现。我们可以在这里找到详细的信息

3. Configuring the JSF Servlet

3.配置JSF Servlet

The JSF framework uses XHTML files to describe the content and structure of the user interface. The server side generates the JSF files from the XHTML descriptions.

JSF框架使用XHTML文件来描述用户界面的内容和结构。服务器端从XHTML描述中生成JSF文件。

Let’s start by creating a static structure in an index.xhtml file in the src/main/webapp directory:

让我们先在src/main/webapp目录下的index.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>
            <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>

The content will be available at <your-url>/index.jsf. However, please note that we’ll get an error message on the client side if we try to reach the content at this stage:

这些内容将在<your-url>/index.jsf上提供。然而,请注意,如果我们试图在这个阶段到达内容,我们将在客户端得到一个错误信息。

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

There won’t be any backend error message. Even so, we can figure out that we’ll need a JSF servlet to handle the request, and the servlet mapping to match the request with the handler.

不会有任何后端错误信息。即便如此,我们还是可以算出我们需要一个JSF servlet来处理这个请求,以及servlet映射来匹配请求和处理程序。

Since we’re in Spring Boot, we can easily extend our application class to handle the required configuration:

由于我们是在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;
    }
}

This looks great, and pretty reasonable, but unfortunately it’s still not good enough. When we try to open <your-url>/index.jsf now, we’ll get another error:

这看起来很好,而且相当合理,但不幸的是,这仍然不够好。当我们现在试图打开<your-url>/index.jsf时,我们会得到另一个错误。

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

Unfortunately, we need a web.xml beside the Java configuration. Let’s create it in src/webapp/WEB-INF:

不幸的是,除了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>

Now our configuration is ready to go, and we can open <your-url>/index.jsf:

现在我们的配置已经准备好了,我们可以打开<your-url>/index.jsf

Welcome in the TO-DO application!

This is a static message rendered from xhtml.

Before we create our user interface, we’ll create the backend of the application.

在我们创建用户界面之前,我们将创建应用程序的后端。

4. Implementing the DAO Pattern

4.实现DAO模式

DAO stands for data access object. Usually, the DAO class is responsible for two concepts: encapsulating the details of the persistence layer and providing a CRUD interface for a single entity. We can find a detailed description in this tutorial.

DAO是数据访问对象的意思。通常情况下,DAO类负责两个概念:封装持久化层的细节和为单个实体提供CRUD接口。我们可以在这个教程中找到详细描述。

To implement the DAO pattern, we’ll first define a generic interface:

为了实现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);
}

Now we’ll create our first, and only, domain class in this to-do application:

现在我们将在这个待办事项应用程序中创建我们的第一个,也是唯一的一个域类。

public class Todo {

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

    // standard getters and setters

}

The next class will be the implementation of Dao<Todo>. The beauty of this pattern is that we can provide a new implementation of this interface anytime.

下一个类将是Dao<Todo>的实现。这种模式的好处是,我们可以随时提供这个接口的新实现。

Consequently, we can change the persistence layer without touching the rest of the code.

因此,我们可以改变持久化层而不触及代码的其他部分。

For our example, we’ll use an in-memory storage class:

对于我们的例子,我们将使用一个内存存储类

@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. The Service Layer

5.服务层

The DAO layer’s main goal is to handle the details of the persistence mechanism, while the service layer stands on top of it to handle business requirements.

DAO层的主要目标是处理持久化机制的细节,而服务层则站在它的上面处理业务需求。

Notice that the DAO interface will be referenced from the service:

注意,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;
    }
}

Here, the service is a named component. We’ll use the name to reference the bean from the JSF context.

这里,服务是一个命名的组件。我们将使用这个名字来引用JSF上下文中的bean。

This class also has a session scope, which will be satisfying for this simple application.

这个类也有一个会话范围,对于这个简单的应用来说,它将是令人满意的。

For more information on Spring scopes, we can take a look at this tutorial. Since Spring’s built-in scopes have a different model than JSF, it’s worth considering defining a custom scope.

关于Spring作用域的更多信息,我们可以看一下这个教程。由于Spring的内置作用域与JSF的模型不同,因此值得考虑定义一个自定义的作用域。

More guidance on this is available in this tutorial.

这个教程中,有更多关于这方面的指导。

6. The Controller

6.控制器

Just like in a JSP application, the controller will handle the navigation between the different views.

就像在JSP应用程序中,控制器将处理不同视图之间的导航。

Next, we’ll implement a minimalistic controller. It’ll navigate from the opening page to the to-do list page:

接下来,我们将实现一个简约的控制器。它将从打开的页面导航到待办事项列表页面。

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

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

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

The navigation is based on the returned name. Thus, the loadTodoPage will send us to the todo.xhtml page, which we’ll implement next.

导航是基于返回的名称。因此,loadTodoPage将把我们送到todo.xhtml页面,我们接下来将实现这个页面。

7. Connecting JSF and Spring Beans

7.连接JSF和Spring Bean

Now let’s see how we can reference our components from the JSF context. First, we’ll extend the index.xthml:

现在让我们看看如何从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>

Here we introduced a commandButton inside of a form element. This is important, since every UICommand element (e.g. commandButton) has to be placed inside of a UIForm element (e.g. form).

这里我们在表单元素中引入了一个commandButton这很重要,因为每个UICommand元素(例如commandButton) 必须放在一个UIForm元素(例如form)里面。

At this stage, we can start our application and examine <your-url>/index.jsf:

在这个阶段,我们可以启动我们的应用程序并检查<you-url>/index.jsf

TO DO application

Unfortunately, we’ll get an error when we click on the button:

不幸的是,当我们点击按钮时,我们会得到一个错误。

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

The message clearly states the problem: the jsfController resolved to null. The corresponding component is either not created, or it’s invisible from the JSF context.

消息清楚地指出了问题所在:jsfController解析为null.相应的组件要么没有创建,要么从JSF上下文中看不见。

In this situation the latter is true.

在这种情况下,后者是真实的。

We need to connect the Spring context with the JSF context within the webapp/WEB-INF/faces-config.xml:

我们需要在webapp/WEB-INF/faces-config.xml中连接Spring上下文和JSF上下文:

<?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>

Now that our controller is ready to work, we’ll need the todo.xhtml.

现在,我们的控制器已经准备好工作,我们将需要todo.xhtml.

8. Interacting With a Service From JSF

8.与JSF中的服务互动

Our todo.xhtml page will have two purposes. First, it’ll display all the to-do elements.

我们的 todo.xhtml页面将有两个目的。首先,它将显示所有待办事项的元素。

Second, it’ll offer the opportunity to add new elements to the list.

第二,它将提供机会向列表中添加新的元素。

For that, the UI component will interact directly with the service declared earlier:

为此,UI组件将直接与先前声明的服务进行交互。

<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>

The two purposes mentioned above are implemented in two separate div elements.

上面提到的两个目的是在两个独立的div元素中实现的。

In the first, we used a dataTable element to represent all the values from todoService.AllTodo.

首先,我们用一个dataTable元素来表示todoService.AllTodo的所有值。

The second div contains a form where we can modify the state of the Todo object in the TodoService.

第二个div包含一个表单,我们可以修改Todo对象在TodoService.的状态。

We use the inputText element to accept user input, whereas the second input is automatically converted into an int.  With the commandButton, the user can persist (into the memory now) the Todo object with the todoService.save.

我们使用inputText元素来接受用户的输入,而第二个输入则自动转换为int。通过commandButton,用户可以通过todoService.save来持久化(进入现在的内存)Todo对象。

9. Conclusion

9.结论

The JSF framework can be integrated into the Spring framework. We have to choose which framework will manage the beans, and in this article, we used the Spring framework.

JSF框架可以被集成到Spring框架中。我们必须选择哪个框架来管理Bean,在这篇文章中,我们使用了Spring框架。

However, the scope model is a bit different than the JSF framework, so we might consider defining custom scopes in the Spring context.

然而,作用域模型与JSF框架有些不同,所以我们可以考虑在Spring上下文中定义自定义作用域。

As always, the code is available over on GitHub.

像往常一样,代码可在GitHub上获得