1. 简介

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

在本教程中,我们将学习如何将 JSF 集成到 Spring Boot 应用中。 作为一个示例,我们将实现一个简单的待办事项(TO-DO)管理应用。

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>jakarta.faces</artifactId>
    <version>4.1.0-M1</version>
</dependency>

其中 jakarta.faces 包含了 JSF 的 API 以及具体实现。更多细节可以参考 官方文档

3. 配置 JSF Servlet

JSF 框架使用 XHTML 文件来描述 UI 的结构和内容,服务器会根据这些描述生成对应的 JSF 页面。

我们先创建一个静态结构文件 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>

此时访问 <your-url>/index.jsf 会出现如下错误:

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

客户端没有报错,但服务端也未响应,说明我们还需要配置一个 JSF Servlet 来处理请求,并设置 URL 映射。

在 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;
    }
}

看起来合理,但还不够。当我们尝试访问 <your-url>/index.jsf 时会出现以下错误:

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

⚠️ 很遗憾,除了 Java 配置外,我们还需要一个 web.xml 文件。src/webapp/WEB-INF 下创建该文件:

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

现在再次访问 <your-url>/index.jsf 就能看到如下输出:

Welcome in the TO-DO application!

This is a static message rendered from xhtml.

接下来我们将开始构建后端逻辑。

4. 实现 DAO 模式

DAO(Data Access Object)用于封装持久层细节,并提供针对单个实体的 CRUD 接口。

首先定义一个通用接口:

public interface Dao<T> {

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

接着创建领域类 Todo

public class Todo {

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

    // standard getters and setters

}

然后实现 Dao<Todo> 接口。这个模式的优势在于我们可以随时更换持久化实现而不影响其他代码:

@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. Service 层

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

📌 注意事项:

  • 使用 @Component("todoService") 命名组件,方便 JSF 引用。
  • 设置为 session 作用域,适用于简单场景。
  • 如果需要更精细控制,可考虑自定义作用域。

6. Controller 控制器

Controller 负责页面跳转逻辑:

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

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

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

返回字符串即为导航路径,比如这里返回 /todo.xhtml 表示跳转到该页面。

7. JSF 与 Spring Bean 的连接

为了让 JSF 能访问 Spring Bean,我们需要在 webapp/WEB-INF/faces-config.xml 中添加 EL 解析器配置:

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

然后修改 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>
       // 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>

⚠️ 所有 <h:commandButton> 必须包裹在 <h:form> 内部。

点击按钮后会跳转到 todo.xhtml 页面。

TO DO application

8. JSF 与 Service 的交互

todo.xhtml 页面有两个功能:

  1. 显示所有待办事项;
  2. 提供表单添加新任务。
<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>

📌 重点说明:

  • 使用 #{todoService.allTodo} 获取列表数据;
  • <h:inputText> 绑定属性,自动转换类型;
  • <h:commandButton> 触发保存操作。

9. 总结

通过上述步骤,我们成功地将 JSF 集成进了 Spring Boot 应用中。虽然两个框架的作用域模型不同,但通过适当配置(如使用 SpringBeanFacesELResolver),可以很好地协同工作。

⚠️ 建议在复杂项目中自定义作用域以提升灵活性。

完整代码见 GitHub 示例:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-mvc


原始标题:A Controller, Service and DAO Example with Spring Boot and JSF