Creating a web application using Spring Boot and Jakarta Server Faces (JSF) combines the strengths of Spring Boot’s rapid development capabilities with JSF’s rich component-based UI framework. This tutorial will walk you through creating a simple CRUD (Create, Read, Update, Delete) application using these technologies.
Related posts
The series includes the following posts:
- #nobuild Web Application Development with Spring Boot
- Creating a Web Application with Spring Boot and Vue.js
- Building a Web Application with Spring Boot and Jakarta Server Faces
- Creating a Web Application with Thymeleaf and HTMX
Repository and Setup
You can clone the https://github.com/dmakariev/examples
repository.
git clone https://github.com/dmakariev/examples.git
cd examples/jbang/spring-boot-compare
This repository provides a hands-on example of how to set up and run these projects using JBang.
Run the application
jbang springbootCompare.java
Open the application in the browser http://localhost:8080
Setting Up the Project
Start by creating a Spring Boot project and add the necessary dependencies for Jakarta Server Faces (JSF). You can use the following dependencies in your pom.xml
:
<dependencies>
<!-- JoinFaces library to integrate JSF with Spring Boot -->
<dependency>
<groupId>org.joinfaces</groupId>
<artifactId>faces-spring-boot-starter</artifactId>
<version>5.3.0</version>
</dependency>
<!-- minimal CSS for styling -->
<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>simpledotcss</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
add to application.properties
joinfaces.faces-servlet.enabled=true
joinfaces.faces.automatic-extensionless-mapping=true
Creating the Backing Bean, aka. the Controller
First, let’s set up our project dependencies and configuration (used by JBang). We’ll use the JoinFaces library to integrate JSF with Spring Boot. The simpledotcss
library will provide minimal CSS for styling.
FacesPersonBean.java
//DEPS org.joinfaces:faces-spring-boot-starter:5.3.0
//DEPS org.mvnpm:simpledotcss:2.3.1
//JAVA_OPTIONS -Djoinfaces.faces-servlet.enabled=true
//JAVA_OPTIONS -Djoinfaces.faces.automatic-extensionless-mapping=true
//FILES META-INF/resources/person-crud-faces.xhtml=person-crud-faces.xhtml
package com.makariev.examples.jbang;
import org.springframework.web.context.annotation.SessionScope;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.faces.context.FacesContext;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIViewRoot;
import jakarta.faces.application.ViewHandler;
import java.io.Serializable;
@Component("personBean")
@SessionScope
@Getter
@Setter
public class FacesPersonBean implements Serializable {
private final PersonRepository personRepository;
private List<Person> persons;
private Person formData;
private boolean editMode;
private Long editedPersonId;
private int pageSize = 5;
private int currentPage = 1;
private long totalPages;
public FacesPersonBean(PersonRepository personRepository) {
this.personRepository = personRepository;
}
@PostConstruct
public void init() {
formData = new Person();
loadPersons();
}
public void loadPersons() {
var page = personRepository.findAll(PageRequest.of(currentPage - 1, pageSize));
persons = page.getContent();
totalPages = page.getTotalPages();
}
public void create() {
formData = new Person();
editMode = false;
executeScript("showPersonDialog()");
}
public void save() {
FacesContext facesContext = FacesContext.getCurrentInstance();
List<FacesMessage> messages = new ArrayList<>();
if (formData.getFirstName() == null || formData.getFirstName().isEmpty()) {
messages.add(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Validation Error", "First Name is required."));
}
if (formData.getLastName() == null || formData.getLastName().isEmpty()) {
messages.add(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Validation Error", "Last Name is required."));
}
if (formData.getBirthYear() == null || formData.getBirthYear() <= 0) {
messages.add(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Validation Error", "Birth Year must be a positive number."));
}
if (!messages.isEmpty()) {
for (FacesMessage message : messages) {
facesContext.addMessage(null, message);
}
return;
}
if (editMode) {
Person person = personRepository.findById(editedPersonId).orElseThrow();
person.setFirstName(formData.getFirstName());
person.setLastName(formData.getLastName());
person.setBirthYear(formData.getBirthYear());
personRepository.save(person);
} else {
personRepository.save(formData);
}
loadPersons();
formData = new Person();
executeScript("closeDialog()");
}
public void edit(Person person) {
this.formData = person;
this.editMode = true;
this.editedPersonId = person.getId();
executeScript("showPersonDialog()");
}
public void delete(Person person) {
personRepository.delete(person);
loadPersons();
}
public void changePage(int page) {
currentPage = page;
loadPersons();
}
public List<Integer> getPageNumbers() {
List<Integer> pageNumbers = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNumbers.add(i);
}
return pageNumbers;
}
private void executeScript(String script) {
FacesContext facesContext = FacesContext.getCurrentInstance();
facesContext.getPartialViewContext().getEvalScripts().add(script);
}
}
This Java class, FacesPersonBean
, is a Spring component with session scope. It manages the CRUD operations for Person
entities. Let’s break down its key parts:
- Dependencies: We include
JoinFaces
andsimpledotcss
libraries for JSF integration and minimal CSS styling. - SessionScope:
@SessionScope
indicates that the bean’s lifecycle is tied to the user’s session. - PostConstruct Initialization: The init method is annotated with
@PostConstruct
to initialize theformData
and load the list of persons when the bean is created. - CRUD Methods: Methods like
loadPersons
,create
,save
,edit
, anddelete
handle the CRUD operations. These methods interact with thePersonRepository
to perform database operations. - Pagination: The
changePage
method andgetPageNumbers
provide pagination functionality.
person-crud-faces.xhtml
<!DOCTYPE html [
<!ENTITY nbsp " ">
<!ENTITY copy "©">
<!ENTITY bull "•"> ]>
<html xmlns:f="jakarta.faces.core"
xmlns:h="jakarta.faces.html"
xmlns:p="jakarta.faces.passthrough"
xmlns:ui="jakarta.faces.facelets"
xmlns:jsf="jakarta.faces">
<h:head>
<title>Person CRUD Application</title>
<h:outputStylesheet library="_static" name="simpledotcss/2.3.1/simple.css"/>
</h:head>
<h:body>
<h:form id="mainForm">
<header>
<h1>Person CRUD Application</h1>
</header>
<main>
<h:commandButton value="Add Person" action="#{personBean.create}">
<f:ajax execute="@this" render=":mainForm:personDialogContent" onevent="handleDialog"/>
</h:commandButton>
<table jsf:id="table">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Year of Birth</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<ui:repeat value="#{personBean.persons}" var="person">
<tr>
<td>#{person.firstName}</td>
<td>#{person.lastName}</td>
<td>#{person.birthYear}</td>
<td>
<h:commandButton value="Edit" action="#{personBean.edit(person)}">
<f:ajax execute="@this" render=":mainForm:personDialogContent" onevent="handleDialog"/>
</h:commandButton>
<h:commandButton value="Delete" action="#{personBean.delete(person)}" style="margin-left:10px">
<f:ajax execute="@this" render="@form"/>
</h:commandButton>
</td>
</tr>
</ui:repeat>
</tbody>
</table>
<nav style="display: flex; justify-content: center; margin-top: 20px;">
<ul style="display: flex; list-style: none; padding: 0;">
<ui:repeat value="#{personBean.pageNumbers}" var="page">
<li style="margin: 0 5px;">
<h:commandLink action="#{personBean.changePage(page)}" value="#{page}" style="#{personBean.currentPage == page ? 'font-weight:bold' : ''}">
<f:ajax execute="@this" render="@form"/>
</h:commandLink>
</li>
</ui:repeat>
</ul>
</nav>
</main>
<footer>
<p>© 2024 Person CRUD Application. All rights reserved.</p>
</footer>
<dialog id="personDialog">
<h:panelGroup id="personDialogContent">
<h2><h:outputText value="#{personBean.editMode ? 'Edit' : 'Add'} Person" /></h2>
<h:messages globalOnly="true" layout="table" id="personFormMessages" showDetail="true"/>
<fieldset>
<h:outputLabel for="firstName" value="First Name" />
<h:inputText id="firstName" value="#{personBean.formData.firstName}" p:placeholder="First Name"/>
<h:outputLabel for="lastName" value="Last Name" />
<h:inputText id="lastName" value="#{personBean.formData.lastName}" p:placeholder="Last Name"/>
<h:outputLabel for="birthYear" value="Year of Birth" />
<h:inputText id="birthYear" value="#{personBean.formData.birthYear}" type="number" p:placeholder="Year of Birth"/>
</fieldset>
<menu>
<h:commandButton value="#{personBean.editMode ? 'Update' : 'Add'}" action="#{personBean.save}">
<f:ajax execute="@form" render="personFormMessages table"/>
</h:commandButton>
<h:commandButton value="Cancel" type="button" onclick="closeDialog(); return false;" style="margin-left:10px">
</h:commandButton>
</menu>
</h:panelGroup>
</dialog>
</h:form>
<script>
function handleDialog(data) {
if (data.status === 'success') {
showPersonDialog();
}
}
function showPersonDialog() {
document.getElementById('personDialog').showModal();
}
function closeDialog() {
document.getElementById('personDialog').close();
}
</script>
</h:body>
</html>
This XHTML file defines the user interface of our CRUD application. Let’s break down its key parts:
- Namespaces: The xmlns attributes define the namespaces for JSF core, HTML, passthrough attributes, Facelets, and JSF components.
- Header: The h:head tag includes the page title and links to the CSS stylesheet.
- Main Form: The h:form tag wraps the entire form. Inside it, we have:
- Header: Displays the application title.
- Main Section: Contains the “Add Person” button, the table of persons, and pagination controls.
- Footer: Displays a copyright message.
- Dialog: A modal dialog for adding or editing persons.
Conclusion
In this tutorial, we created a web application using Spring Boot and Jakarta Server Faces. We covered setting up the project, defining the backend logic, and creating the frontend UI. This combination provides a powerful framework for building modern web applications with rich user interfaces and robust backend support.
Happy coding!