Java Module 23 – Jakarta Faces (JSF): component-based web UIs
The Faces life cycle, phase by phase
Follow a Faces request through Restore View, Apply Request Values, Process Validations, Update Model Values, Invoke Application and Render Response.
What you will learn
- Name the six phases and what each one does to the component tree
- Predict the phases of a first request, a valid post and a post that fails validation
- Use immediate, a phase listener and the view state to observe and control processing
Before you start
What you’ll learn · Before you start
What you will learn
- Name the six phases and what each one does to the component tree
- Predict the phases of a first request, a valid post and a post that fails validation
- Use immediate, a phase listener and the view state to observe and control processing
Before you start
On this page
The Faces life cycle is the fixed order of six phases in which Faces handles every request for a page: Restore View, Apply Request Values, Process Validations, Update Model Values, Invoke Application and Render Response. Each phase does one job to the page’s component tree. The first restores the tree, or builds it for a first visit. The second gives each component its submitted text, and the third converts and validates that text. The fourth copies the valid values into the beans, the fifth runs the action of the button that was pressed, and the sixth writes the HTML and saves the view. A first GET needs only phases 1 and 6. A post runs all six, unless a value fails: then Faces jumps from phase 3 to phase 6 and never touches the beans.
This lesson follows the Faces life cycle phase by phase on a course-registration form, with a phase listener that records every phase, setter and action. The recordings ran on Mojarra 4.1.18, Tomcat 11.0.26 and Eclipse Temurin 25.0.4.1.
The six phases
The six phases of a Faces request, and when a request jumps to the last one
Text description of the diagram
Six boxes stand in one column, joined by arrows from top to bottom.
- Restore View builds the component tree, or restores the saved one. A first request goes on to phase 6. The arrow to the next box is labelled "a post".
- Apply Request Values: each component takes its submitted text. After an immediate action, the request goes on to phase 6.
- Process Validations converts and validates the values and queues messages. If a value fails, the request goes on to phase 6. The arrow to the next box is labelled "every value valid".
- Update Model Values: the valid values go into the beans.
- Invoke Application: the action of the pressed button runs.
- Render Response writes the HTML and saves the view.
| Phase | What it does to the tree | Your code it calls |
|---|---|---|
| 1 Restore View | Restores the saved tree on a post, or builds a new one | None |
| 2 Apply Request Values | Each input stores its submitted text, and the pressed button queues its action | The actions of immediate buttons |
| 3 Process Validations | Converters turn the text into values, validators check them, and failures queue messages | Validators and value-change listeners |
| 4 Update Model Values | Each valid value goes into the bean through its expression | Setters |
| 5 Invoke Application | The queued actions run, and Faces picks the view to show next | Action methods and action listeners |
| 6 Render Response | The tree writes the HTML, and the view state is saved | Getters |
The phases are the constants of jakarta.faces.event.PhaseId, numbered 1 to 6, and ANY_PHASE stands for all of
them.
Watching the phases with a phase listener
A PhaseListener is called before and after the phases that its getPhaseId() names. PhaseTrail names
ANY_PHASE and adds each phase to a list kept in the request’s FacesContext; the bean’s setters and actions add
their own lines, and the page prints the list:
Java (PhaseTrail.java) · lifecycle/src/main/java/com/example/lifecycle/PhaseTrail.java
package com.example.lifecycle;
import jakarta.faces.event.PhaseEvent;
import jakarta.faces.event.PhaseId;
import jakarta.faces.event.PhaseListener;
/**
* Adds the name of every phase to the request's trail as the phase begins. ANY_PHASE asks Faces to call it before and
* after each of the six phases; faces-config.xml registers it.
*/
public class PhaseTrail implements PhaseListener {
@Override
public PhaseId getPhaseId() {
return PhaseId.ANY_PHASE;
}
@Override
public void beforePhase(PhaseEvent event) {
Trail.of(event.getFacesContext()).add(event.getPhaseId().toString());
}
@Override
public void afterPhase(PhaseEvent event) {
}
} Java (Registration.java) · lifecycle/src/main/java/com/example/lifecycle/Registration.java
package com.example.lifecycle;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
/** The backing bean of form.xhtml. Its setters and actions note in the trail when Faces calls them. */
@Named
@RequestScoped
public class Registration {
private String name;
private Integer semester;
public String getName() {
return name;
}
public void setName(String name) {
Trail.add(" setName(\"" + name + "\")");
this.name = name;
}
public Integer getSemester() {
return semester;
}
public void setSemester(Integer semester) {
Trail.add(" setSemester(" + semester + ")");
this.semester = semester;
}
public void save() {
Trail.add(" save()");
}
public void cancel() {
Trail.add(" cancel()");
}
} XHTML · lifecycle/src/main/webapp/form.xhtml
<!DOCTYPE html>
<!-- A registration form whose page also shows what Faces did for this request: the trail of phases, setters and
actions, the values the bean holds while the page is rendered, and the messages. -->
<html lang="en" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core" xmlns:ui="jakarta.faces.facelets">
<h:head>
<title>Course registration</title>
</h:head>
<h:body>
<h1>Course registration</h1>
<h:form id="reg">
<h:outputLabel for="name" value="Name"/>
<h:inputText id="name" label="Name" value="#{registration.name}" required="true"/>
<h:outputLabel for="semester" value="Semester"/>
<h:inputText id="semester" label="Semester" value="#{registration.semester}" required="true">
<f:validateLongRange minimum="1" maximum="8"/>
</h:inputText>
<h:commandButton id="save" value="Save" action="#{registration.save}"/>
<!-- immediate: the action runs at the end of Apply Request Values, before any validation. -->
<h:commandButton id="cancel" value="Cancel" action="#{registration.cancel}" immediate="true"/>
</h:form>
<h:messages id="messages"/>
<p id="stored">name=#{registration.name}, semester=#{registration.semester}</p>
<ol id="trail">
<ui:repeat value="#{trailView.steps}" var="step"><li class="step">#{step}</li></ui:repeat>
</ol>
</h:body>
</html> XML · lifecycle/src/main/webapp/WEB-INF/faces-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Registers the phase listener for every request of the application, and declares admin.xhtml a protected view. -->
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_1.xsd"
version="4.1">
<lifecycle>
<phase-listener>com.example.lifecycle.PhaseTrail</phase-listener>
</lifecycle>
<protected-views>
<url-pattern>/admin.xhtml</url-pattern>
</protected-views>
</faces-config> Java (Trail.java) · lifecycle/src/main/java/com/example/lifecycle/Trail.java
package com.example.lifecycle;
import jakarta.faces.context.FacesContext;
import java.util.ArrayList;
import java.util.List;
/**
* The steps of the current request: the phases as they begin, and the setters and actions that run in them. The list
* lives in the request's FacesContext, which Faces creates for each request and drops at its end.
*/
public final class Trail {
private static final String KEY = Trail.class.getName();
private Trail() {
}
@SuppressWarnings("unchecked")
public static List<String> of(FacesContext context) {
return (List<String>) context.getAttributes().computeIfAbsent(KEY, key -> new ArrayList<String>());
}
public static void add(String step) {
of(FacesContext.getCurrentInstance()).add(step);
}
} Visit.java plays the browser. It loads the page, posts the form back with the page’s hidden fields and the values on
its command line, and prints the trail, the text in the fields, what the bean held while the page was rendered, and
the messages:
// Plays a browser against a page of the lifecycle application and prints what the page reports about each request.
//
// java Visit.java <page URL> loads the page with a GET
// java Visit.java <page URL> <field>=<value> ... loads the page, then posts its form back with these fields
//
// A post carries what a browser sends: the hidden fields of the page it loaded (the form's id and the
// jakarta.faces.ViewState field) and the fields given, among them the button that was pressed (reg:save=Save). A
// field given as an empty value (reg:name=) is posted empty. Options, in the place where they apply:
//
// --then posts again, from the answer of the post before, with the fields that follow
// --view-state=none leaves the view state out of the post; --view-state=<value> sends that value instead
// --tamper changes one character in the middle of the view state before posting it
// --new-session posts without the session cookie, as a browser does once its session has expired
// --state-size prints the length of the page's view state field
// --follow=<id> instead of posting, follows the link with that id, from the page, as a browser would
//
// For each answer it prints the request and the status, the steps of the page's trail, the values shown in the
// form's text fields, what the bean held while the page was rendered, and the messages.
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
static final String VIEW_STATE = "jakarta.faces.ViewState";
static final Pattern HIDDEN = Pattern.compile("<input type=\"hidden\" name=\"([^\"]+)\"[^>]*? value=\"([^\"]*)\"");
static final Pattern TEXT_FIELD = Pattern.compile("<input id=\"([^\"]+)\" type=\"text\" name=\"[^\"]+\" value=\"([^\"]*)\"");
static final Pattern STEP = Pattern.compile("<li class=\"step\">(.*?)</li>");
static final Pattern STORED = Pattern.compile("<p id=\"stored\">(.*?)</p>");
static final Pattern MESSAGES = Pattern.compile("<ul id=\"messages\">(.*?)</ul>");
static final Pattern MESSAGE = Pattern.compile("<li>\\s*(.*?)\\s*</li>");
final CookieManager cookies = new CookieManager();
HttpClient browser = HttpClient.newBuilder().cookieHandler(cookies).build();
void main(String[] args) throws Exception {
URI page = URI.create(args[0]);
HttpResponse<String> answer = browser.send(HttpRequest.newBuilder(page).build(), BodyHandlers.ofString());
List<String> rest = List.of(args).subList(1, args.length);
if (rest.stream().noneMatch(a -> a.contains("=") && !a.startsWith("--state-size"))) {
report("GET " + page.getPath(), answer, rest.contains("--state-size"));
return;
}
List<String> fields = new ArrayList<>();
for (String arg : rest) {
if (arg.equals("--then")) {
answer = post(page, answer, fields);
fields = new ArrayList<>();
} else {
fields.add(arg);
}
}
post(page, answer, fields);
}
/** Posts the form of the page in `before` back with these fields, prints the answer and returns it. */
HttpResponse<String> post(URI page, HttpResponse<String> before, List<String> fields) throws Exception {
Optional<String> follow = fields.stream().filter(f -> f.startsWith("--follow=")).findFirst();
if (follow.isPresent()) {
String id = follow.get().substring("--follow=".length());
Matcher link = Pattern.compile("<a id=\"" + Pattern.quote(id) + "\"[^>]*? href=\"([^\"]+)\"").matcher(before.body());
if (!link.find()) throw new IllegalStateException("the page has no link with the id " + id);
URI target = page.resolve(text(link.group(1)));
HttpRequest get = HttpRequest.newBuilder(target).header("Referer", page.toString()).build();
HttpResponse<String> answer = browser.send(get, BodyHandlers.ofString());
report("GET " + target.getPath() + " (link " + id + ")", answer, false);
return answer;
}
Map<String, String> form = new LinkedHashMap<>();
HIDDEN.matcher(before.body()).results().forEach(m -> form.put(m.group(1), m.group(2)));
for (String field : fields) {
if (field.equals("--view-state=none")) {
form.remove(VIEW_STATE);
} else if (field.startsWith("--view-state=")) {
form.put(VIEW_STATE, field.substring("--view-state=".length()));
} else if (field.equals("--tamper")) {
String state = form.get(VIEW_STATE);
int middle = state.length() / 2;
form.put(VIEW_STATE, state.substring(0, middle) + (state.charAt(middle) == 'A' ? 'B' : 'A') + state.substring(middle + 1));
} else if (field.equals("--new-session")) {
cookies.getCookieStore().removeAll();
} else if (!field.startsWith("--")) {
int eq = field.indexOf('=');
form.put(field.substring(0, eq), field.substring(eq + 1));
}
}
String body = form.entrySet().stream()
.map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
HttpRequest post = HttpRequest.newBuilder(page)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> answer = browser.send(post, BodyHandlers.ofString());
report("POST " + String.join(" ", fields), answer, fields.contains("--state-size"));
return answer;
}
void report(String request, HttpResponse<String> answer, boolean stateSize) {
String html = answer.body();
IO.println(request + " -> " + answer.statusCode());
STEP.matcher(html).results().forEach(m -> IO.println(" " + text(m.group(1))));
List<String> shown = TEXT_FIELD.matcher(html).results().map(m -> m.group(1) + "=" + text(m.group(2))).toList();
if (!shown.isEmpty()) IO.println("fields show: " + String.join(", ", shown));
Matcher stored = STORED.matcher(html);
if (stored.find()) IO.println("bean while rendering: " + text(stored.group(1)));
Matcher list = MESSAGES.matcher(html);
List<String> messages = list.find() ? MESSAGE.matcher(list.group(1)).results().map(m -> text(m.group(1))).toList() : List.of();
IO.println("messages: " + (messages.isEmpty() ? "none" : String.join(" | ", messages)));
if (stateSize) {
HIDDEN.matcher(html).results().filter(m -> m.group(1).equals(VIEW_STATE))
.forEach(m -> IO.println("view state field: " + m.group(2).length() + " characters"));
}
}
/** The text of an HTML fragment: the five entities Faces writes, decoded. */
String text(String html) {
return html.replace(""", "\"").replace("'", "'").replace("<", "<").replace(">", ">").replace("&", "&");
} Three requests, recorded
Read each trail phase by phase, against the table above:
from session import run
# Builds the lifecycle application, deploys it to a new Tomcat base on 127.0.0.1:28650 and sends three requests with
# Visit.java: the first GET, a post that passes validation, and a post with a semester out of range.
run(
"""
cd lifecycle
mvn -q package
cd ..
sh new-base.sh base 28650
cp lifecycle/target/lifecycle.war base/webapps/
$CATALINA_HOME/bin/startup.sh
java Visit.java http://127.0.0.1:28650/lifecycle/form.xhtml
java Visit.java http://127.0.0.1:28650/lifecycle/form.xhtml reg:name=Asha reg:semester=5 reg:save=Save
java Visit.java http://127.0.0.1:28650/lifecycle/form.xhtml reg:name=Asha reg:semester=9 reg:save=Save
$CATALINA_HOME/bin/shutdown.sh
""",
port=28650,
) Output
# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 · Apache Tomcat 11.0.26
$ cd lifecycle
$ mvn -q package
$ cd ..
$ sh new-base.sh base 28650
$ cp lifecycle/target/lifecycle.war base/webapps/
$ $CATALINA_HOME/bin/startup.sh
$ java Visit.java http://127.0.0.1:28650/lifecycle/form.xhtml
GET /lifecycle/form.xhtml -> 200
RESTORE_VIEW 1
RENDER_RESPONSE 6
fields show: reg:name=, reg:semester=
bean while rendering: name=, semester=
messages: none
$ java Visit.java http://127.0.0.1:28650/lifecycle/form.xhtml reg:name=Asha reg:semester=5 reg:save=Save
POST reg:name=Asha reg:semester=5 reg:save=Save -> 200
RESTORE_VIEW 1
APPLY_REQUEST_VALUES 2
PROCESS_VALIDATIONS 3
UPDATE_MODEL_VALUES 4
setName("Asha")
setSemester(5)
INVOKE_APPLICATION 5
save()
RENDER_RESPONSE 6
fields show: reg:name=Asha, reg:semester=5
bean while rendering: name=Asha, semester=5
messages: none
$ java Visit.java http://127.0.0.1:28650/lifecycle/form.xhtml reg:name=Asha reg:semester=9 reg:save=Save
POST reg:name=Asha reg:semester=9 reg:save=Save -> 200
RESTORE_VIEW 1
APPLY_REQUEST_VALUES 2
PROCESS_VALIDATIONS 3
RENDER_RESPONSE 6
fields show: reg:name=Asha, reg:semester=9
bean while rendering: name=, semester=
messages: Semester: Validation Error: Specified attribute is not between the expected values of 1 and 8.
$ $CATALINA_HOME/bin/shutdown.sh
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifecycle_session.py
- The first GET ran Restore View and Render Response only. Nothing had been posted, so Restore View built the tree and sent the request straight to rendering.
- The valid post ran all six phases. The setters ran in Update Model Values,
save()in Invoke Application, and the page was rendered from the new values. - Semester 9 failed
f:validateLongRangein Process Validations, and the request jumped to Render Response. The name Asha was valid, yet the bean received neither value, because Update Model Values runs for the whole form or not at all. The fields still showed Asha and 9: each component keeps the text it was sent until a post succeeds.
| Request | Phases | Bean updated | Action ran |
|---|---|---|---|
| A first GET | 1, 6 | No | No |
| A post, every value valid | 1 to 6 | Yes | Yes |
| A post with one invalid value | 1, 2, 3, 6 | No, not even the valid fields | No |
What else changes the order
Five things change the path above:
immediate="true"on a button runs its action at the end of Apply Request Values, before any validation.immediate="true"on an input field converts and validates it in Apply Request Values instead.- A post without its
jakarta.faces.ViewStatefield cannot be matched to a saved view. - Client-side state saving keeps the whole saved view in that field instead of the HTTP session.
- A protected view refuses a GET that does not carry its session’s token.
GTU 3160707, unit 5
The syllabus of Advance Java Programming names the “JSF request processing Life cycle” among the topics of unit 5. Its six phases are the same in JSF 2.0 and Faces 4.1, so the recorded trails of this lesson answer that topic for either version.
Check yourself
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
immediate, step by step: Cancel, Back and immediate fields Pro: opens with a Pro, Premium or Ultimate pass
immediate moves work from later phases into Apply Request Values. Four recorded requests show what that does to a Cancel button, to a field marked immediate, and to the Back button of a form with two steps.
In this part:
- Four requests with immediate, recorded
- Why Cancel left the typed text in the form
- A field marked immediate makes Cancel look broken
- Back without validating the current step
- Choosing a Cancel or Back that cannot be blocked: the trade-offs
2 runnable examples (Java, Python, XHTML) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
The view state: where it lives, and how posts lose it Pro: opens with a Pro, Premium or Ultimate pass
The jakarta.faces.ViewState field ties every post to a saved view. This part leaves the field out, forges it, outlives
its session and tampers with it, and records what Faces did each time.
In this part:
- Seven requests against the view state, recorded
- Reading the failures
- Server or client state saving, with numbers
- Protected views stop GETs without a token
- Making ViewExpiredException humane
2 runnable examples (Python, XML) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Exercise: predict eight requests, then check them Pro: opens with a Pro, Premium or Ultimate pass
Predicting the phases of a request before you send it is the quickest way to find a life-cycle bug. Fill in the table for eight requests to the registration form, then compare your answers with the recorded ones.
In this part:
- Eight requests to predict
- The recorded answers
- What the answers show
- Scoring your predictions: a rubric
1 runnable example (Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
-
Fresher · Warm-up
Name the six phases of the Faces life cycle in order. Which of them does the first GET of a page run, and why?
Restore View, Apply Request Values, Process Validations, Update Model Values, Invoke Application and Render Response. Restore View builds the component tree, or restores the saved one on a post; Apply Request Values gives each component its submitted text; Process Validations converts and validates it; Update Model Values copies the valid values into the beans; Invoke Application runs the action of the pressed button; Render Response writes the HTML and saves the view state. A first GET runs only Restore View and Render Response: nothing has been posted, so there is nothing to decode, validate or store, and after building a new tree Restore View sends the request straight to rendering. A phase listener registered for ANY_PHASE shows this order for any request.
-
Fresher to mid
On a form with a name and a semester, a user types a valid name and an out-of-range semester. After the error message, the debugger shows that the bean's name is still null, and a colleague files a bug against Faces. Explain what happened, and when it matters.
-
Mid
A Cancel button has immediate="true", yet on one form pressing it shows "Value is required" next to a field. On the other forms it behaves. What do you look for, and how do you make Cancel reliable everywhere?
-
Mid to senior
Users who leave a form open over lunch get an HTTP 500 page with ViewExpiredException when they press Save. Explain what happened, and compare the ways to fix it, with what each one costs.
Key takeaways
- Every request for a Faces page runs the same six phases in the same order; a first GET needs only Restore View and Render Response.
- Validation failures jump from Process Validations to Render Response, so no value reaches the bean, not even the valid ones, and no action runs.
- Setters run in Update Model Values and actions in Invoke Application; a phase listener with
ANY_PHASEshows the order for any request. immediate, the view state field, client-side state saving and protected views are the ways the order changes.
References
- Jakarta Faces 4.1 specification document, chapter 2: Request Processing Lifecycle (Eclipse Foundation)
- Jakarta Faces 4.1 API documentation (Eclipse Foundation)
- Jakarta Faces 4.1 VDL documentation: h:button (Eclipse Foundation)
- Jakarta Faces 4.1 VDL documentation: f:ajax (Eclipse Foundation)
- Jakarta Faces 4.1 (Eclipse Foundation)
- Jakarta Faces 5.0 (under development) (Eclipse Foundation)
- Mojarra 4.1 context parameters (Eclipse Foundation)
- Jakarta Servlet 6.1 specification document (Eclipse Foundation)
- Apache Tomcat 11 configuration reference: the Context container (Apache Software Foundation)
- Syllabus of Advance Java Programming (subject code 3160707) (Gujarat Technological University)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress