Using Jakarta MVC
This chapter describes how to use the Jakarta MVC API, an action-based framework for building web applications using the model view controller architecture pattern.
The model, or the M, refers to the underlying application data. In Jakarta EE, this is generally a Jakarta Persistence entity or its projected instance (DTOs etc).
The view or V, refers to the presentation of the model data to the user. Depending on the framework, there are different view technologies that can be used. For instance Jakarta Faces uses Facelets as its view technology, while Jakarta MVC supports both Facelets and Jakarta Server Pages (which is the default).
The controller, or C, refers to the unit that manages user input, connects that input to the model and returns output to the user. The returned output could be another view, a redirect, a file download, or anything at all, depending on the application. You can think of the controller as the central, coordinating piece of the application that liaises user actions to different parts of the application.
Introducing Jakarta MVC
Jakarta EE has action and component based frameworks for building web applications using the model view controller architecture pattern. Jakarta Faces is a component based framework while Jakarta MVC is an action based one.
An action based web framework is one in which the application code creates an explicit controller that accepts requests and maps them to actions.
Component based web frameworks on the other hand, have the controllers owned and managed by the framework itself. The controller is transparent to the developer in a component based framework.
What is Jakarta MVC?
Jakarta MVC is an action based web application development framework built on top of Jakarta REST. It is an alternative way to build traditional web applications on the Jakarta EE Platform. Jakarta MVC is an optional standalone specification that is not part of Jakarta EE by default.
As a Jakarta REST based framework, it makes all the features and options for developing REST services available for developing much more traditional web applications.
Action Vs Component Based Frameworks
An action based web framework is one in which the application code creates an explicit controller that accepts requests and maps them to actions.
Component based web frameworks on the hand, have the controllers owned and managed by the framework itself. The controller is transparent to the developer in a component based framework.
Dependencies Setup
To start with Jakarta MVC, you will need to add the specification API to your application dependencies. The current release can be obtained from the Payara API BOM. This setup is shown below.
<project>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>fish.payara.api</groupId>
<artifactId>payara-bom</artifactId>
<version>7.2024.1.Alpha3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.mvc</groupId>
<artifactId>jakarta.mvc-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
The Controller
A typical Jakarta MVC application has three parts - the view, the controller and a model. The model doesn’t have to necessarily be a database entity.
Any data that the UI displays can be considered as the model. With your above setup, a typical "Hello, World" will look as follows.
@Path("app")
@Controller
public class AppController {
@Inject
Models models;
@GET
public String sayHello() {
models.put("greet", "Hello, World! Jakarta MVC");
models.put("platform", "Jakarta EE 10 on Payara 6 Community");
models.put("date", LocalDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
models.put("message", "Your Jakarta MVC application is running!");
return "greet.xhtml";
}
}
The AppController is a Jakarta REST resource class, identified as such by the @Path
annotation.nThe @Controller
annotation is from Jakarta MVC, marking all methods in this class as controller methods.
A controller method is one that returns a view in the form of a string, as done by the sayHello()
method, or a jakarta.ws.rs.core.Response
object that wraps a string object, resolvable to a view.
The sayHello()
method creates the models for the UI by using the jakarta.mvc.Models
map.
The Models
are injected into the controller and then the method populates it accordingly with data for the UI to display. In this example, the injected Models
instance, models
, is populated with a few strings identified by their keys.
Models
is essentially a map that makes its values available to the UI through its keys. All of this is done automatically on your behalf by the Jakarta MVC runtime.
The last line of the sayHello()
method returns greet.xhtml
.
This is the UI that will be rendered when the user navigates to the sayHello()
method.
For the sample application, the full path is http://localhost:8080/jakarta-mvc/mvc/app/
, where jakarta-mvc
is the context path, mvc
is the Jakarta REST root resource path, and app
is the path to the sayHello()
method.
@ApplicationPath("mvc")
public class RestConfiguration extends Application {
}
The Models
map instance is automatically available to the view and as such it can access all the values we put into it.
The View
There are two view technology options you can use with Jakarta MVC. The first and default is the Jakarta Server Pages, or optionally Jakarta Faces.
As Jakarta Server Pages is no longer a popular technology, this chapter will use the much more popular Jakarta Faces option.
The easiest way to tell Jakarta MVC to use Jakarta Faces as the default view technology is to create an empty faces-config.xml
in the WEB-INF
folder.
<?xml version='1.0' encoding='UTF-8'?>
<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_0.xsd"
version="4.0">
</faces-config>
With that done, the greet.xhtml
facelet is shown next.
This should be created under a views
directory in the WEB-INF
folder.
<!DOCTYPE html>
<html lang="en" xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<title>Jakarta MVC</title>
</h:head>
<h:body>
<h1>#{greet}</h1>
<p>#{message}</p>
<p>This application is running on #{platform}, deployed on #{date}</p>
</h:body>
</html>
The greet.xhtml
view is a very simple facelet file that is accessing the models to display to the user. The models that were put in the Models
map instance are being accessed through the \#{}
expression, using the key of each value.
For instance the #{greet}
will return "Hello, World! Jakarta MVC", as was put in the map.
Accessing http://localhost:8080/jakarta-mvc/mvc/app/
gives us the response shown below.
Models
So far we have seen how we can pass models, or data to the view for display through the Models
map.
Another way is through the use of CDI. First let’s introduce our model, this time as a Plain Old Java Object (POJO), garnished with two CDI annotations, shown below.
@Named
@RequestScoped
public class Salutation {
private String greet;
private String platform;
private String greetingDate;
private String message;
public String getGreet() {
return greet;
}
public void setGreet(String greet) {
this.greet = greet;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getGreetingDate() {
return greetingDate;
}
public void setGreetingDate(String greetingDate) {
this.greetingDate = greetingDate;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Class Salutation
is a simple Java class with some fields.
These are the same fields we passed to the first view through the Models
map.
Salutation
is annotated @Named
and @RequestScoped
.
@Named
is a CDI qualifier that makes CDI managed instances of the class available in an Expression Language context - as used in the facelet files.
The @RequestScoped
annotation will cause a new instance of Salutation
to be created for each injection point.
With the model in place, let’s look at the amended controller and how the model is instantiated and populated.
@Path("app")
@Controller
public class AppController {
@Inject
Salutation salutation;
@GET
@Path("salute")
public String salute() {
String formattedDate = LocalDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
salutation.setGreet("Hello, World! Jakarta MVC");
salutation.setPlatform("Jakarta EE 10 on Payara 6 Community");
salutation.setGreetingDate(formattedDate);
salutation.setMessage("Your Jakarta MVC application is running!");
return "salute.xhtml";
}
}
The AppController
controller has a new method, salute()
, hosted at the path /salute
, that populates a CDI injected instance of class Salutation
.
This method returns the salute.xhtml
view to render the data. As you can see, the Models
map is not used anywhere at all.
The injected Salutation
instance is automatically available to the view thanks to the @Named
annotation.
The salute.xhtml
is shown next. This should be created under a views
directory in the WEB-INF
folder.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<title>Title</title>
</h:head>
<h:body>
<h1>#{salutation.greet}</h1>
<p>#{salutation.message}</p>
<p>This application is running on #{salutation.platform}, deployed on #{salutation.greetingDate}</p>
</h:body>
</html>
The salute.xhtml
uses the same #{}
expression to access the model.
This time around it calls the getter methods of the various fields.
The salutation instance is what is CDI makes available automatically.
This way, the view has access to the model without explicitly using the Models
map. The salute
method is hosted at http://localhost:8080/jakarta-mvc/mvc/app/salute
, which returns the following.