This series of articles will examine Spring Boot features. This fourth
installment discusses Spring MVC, templating in Spring, and creates a
simple internationalized clock application as an example. The clock
application will allow the user to select Locale and
TimeZone.
Complete source code for the series and for this part are available on Github.
Note that this post’s details and the example source code has been updated for Spring Boot version 2.5.3 so some output may show older Spring Boot versions.
Theory of Operation
The Controller will provide methods to service GET and POST requests
at /clock/time and update the
Model
with:
| Attribute Name | Type |
|---|---|
| locale | User-selected Locale |
| zone | User-selected TimeZone |
| timestamp | Current Date |
| date | DateFormat to display date (based on Locale and TimeZone) |
| time | DateFormat to display time (based on Locale and TimeZone |
| locales | A sorted List of Locales the user may select |
| zones | A sorted List of TimeZones the user may select |
The View will use Thymeleaf technology which may be included with a reasonable configuration simply by including the corresponding “starter” in the POM. The developer’s primary responsibility is write the corresponding Thymeleaf template.
For this project, Bootstrap is used as the CSS Framework.1
The required dependencies are included in the
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
...
<dependencies verbose="true">
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
...
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.6.0-1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
</dependencies>
...
</project>
The following subsections describe the View, Controller, and Model.
View
The Controller will serve requests at /clock/time and the Thymeleaf
ViewResolver (with the default configuration) will look for the
corresponding template at
classpath:/templates/clock/time.html
(note the /templates/ superdirectory and the .html suffix). The
template with the <main/> element is shown below. The XML Namespace “th”
is defined for Thymeleaf and a number of “th:*” attributes are used. For
example, the Bootstrap artifact paths are wrapped in “th:href” and
“th:src” attributes with values expressed in Thymeleaf standard expression
syntax.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:xmlns="@{http://www.w3.org/1999/xhtml}">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/>
<link rel="stylesheet" th:href="@{/webjars/bootstrap/css/bootstrap.css}"/>
</head>
<body>
<!--[if lte IE 9]>
<p class="browserupgrade" th:utext="#{browserupgrade}"/>
<![endif]-->
<main class="container">
...
</main>
<script th:src="@{/webjars/jquery/jquery.js}" th:text="''"/>
<script th:src="@{/webjars/bootstrap/js/bootstrap.js}" th:text="''"/>
</body>
</html>
The following shows the template’s rendered HTML. The Bootstrap
artifacts’ paths have been rendered “href” and “src” attributes (with
version handling in their paths as decribed in
part 2) and with
the Thymeleaf expressions evaluated.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/>
<link rel="stylesheet" href="/webjars/bootstrap/4.4.1/css/bootstrap.css"/>
</head>
<body>
<!--[if lte IE 9]>
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="https://browsehappy.com/">upgrade your browser</a> to improve your experience and security.</p>
<![endif]-->
<main class="container">
...
</main>
<script src="/webjars/jquery/3.6.0/jquery.js"></script>
<script src="/webjars/bootstrap/4.6.0-1/js/bootstrap.js"></script>
</body>
</html>
Spring provides a message catalog facility which allows <p
class="browserupgrade" th:utext="#{browserupgrade}"/> to be evaluated from
the
message.properties.
The Tutorial: Thymeleaf + Spring provides a reference for these and other
features. Tutorial: Using Thymeleaf provides the reference for standard
expression syntax, the available “th:*” attributes and elements, and
available expression objects.
The initial implementation of the clock View is:
...
<main class="container">
<div class="jumbotron">
<h1 class="text-center" th:text="${time.format(timestamp)}"/>
<h1 class="text-center" th:text="${date.format(timestamp)}"/>
</div>
</main>
...
It output will be discussed in detail in the next section after discussing
the Controller implementation and the population of the Model but note
that the View requires the Model provide the time, date, and
timestamp attributes as laid out above.
Controller and Model
The Controller is implemented by a class annotated with
@Controller,
ClockController.
The implementation of the GET /clock/time is outlined below:
@Controller
@RequestMapping(value = { "/clock/" })
@NoArgsConstructor @ToString @Log4j2
public class ClockController {
...
@RequestMapping(method = { RequestMethod.GET }, value = { "time" })
public void get(Model model, Locale locale, TimeZone zone) {
model.addAttribute("locale", locale);
model.addAttribute("zone", zone);
DateFormat date = DateFormat.getDateInstance(DateFormat.LONG, locale);
DateFormat time = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale);
model.addAttribute("date", date);
model.addAttribute("time", time);
for (Object object : model.asMap().values()) {
if (object instanceof DateFormat) {
((DateFormat) object).setTimeZone(zone);
}
}
Date timestamp = new Date();
model.addAttribute("timestamp", timestamp);
}
...
}
The parameters are Model, Locale, and TimeZone, all injected by
Spring. A complete list of available method parameters and return types
with their respective semantics, may be found at Handler Methods.
The method updates the Model with the user Locale and TimeZone, the
current timestamp, and the time and date DateFormat to render the clock
display. Since the method returns void, the view resolves to the
Thymeleaf template at classpath:/templates/clock/time.html (as described
above). Alternatively, the method may return a String with a
name (path) of a template. Spring then evaluates the template with the
Model for the output which results in:
...
<main class="container">
<div class="jumbotron">
<h1 class="text-center">11:59:59 AM</h1>
<h1 class="text-center">December 24, 2019</h1>
</div>
</main>
...
which renders to:

Of course, this implementation does not yet allow the user to customize
their Locale or TimeZone. The next section adds this functionality.
Adding User Customization
To allow user customization, first a form allowing the user to select
Locale and TimeZone must be added.
...
<main class="container">
...
<form class="row" method="post" th:action="${#request.servletPath}">
<select class="col-lg" name="languageTag">
<option th:each="option : ${locales}"
th:with="selected = ${option.equals(locale)},
value = ${option.toLanguageTag()},
display = ${option.getDisplayName(locale)},
text = ${value + ' - ' + display}"
th:selected="${selected}" th:value="${value}" th:text="${text}"/>
</select>
<select class="col-lg" name="zoneID">
<option th:each="option : ${zones}"
th:with="selected = ${option.equals(zone)},
value = ${option.ID},
display = ${option.getDisplayName(locale)},
text = ${value + ' - ' + display}"
th:selected="${selected}" th:value="${value}" th:text="${text}"/>
</select>
<button class="col-sm-1" type="submit" th:text="'↻'"/>
</form>
</main>
...
Two attributes must be added to the Model by the GET /clock/time
method, the Lists of Locales and TimeZones from which the user may
select.2
private static final List<Locale> LOCALES =
Stream.of(Locale.getAvailableLocales())
.filter(t -> (! t.toString().equals("")))
.collect(Collectors.toList());
private static final List<TimeZone> ZONES =
Stream.of(TimeZone.getAvailableIDs())
.map(t -> TimeZone.getTimeZone(t))
.collect(Collectors.toList());
@RequestMapping(method = { RequestMethod.GET }, value = { "time" })
public void get(Model model, Locale locale, TimeZone zone, ...) {
...
Collator collator = Collator.getInstance(locale);
List<Locale> locales =
LOCALES.stream()
.sorted(Comparator.comparing(Locale::toLanguageTag, collator))
.collect(Collectors.toList());
List<TimeZone> zones =
ZONES.stream()
.sorted(Comparator
.comparingInt(TimeZone::getRawOffset)
.thenComparingInt(TimeZone::getDSTSavings)
.thenComparing(TimeZone::getID, collator))
.collect(Collectors.toList());
model.addAttribute("locales", locales);
model.addAttribute("zones", zones);
...
}
Key to the implementation is the use of the “th:each” attribute where the
node is evaluated each member of the List. The “th:with”
attribute allows variables to be defined and referenced within the scope of
the corresponding node. Partial output is shown below.
...
<main class="container">
...
<form class="row" method="post" action="/clock/time">
<select class="col-lg" name="languageTag">
<option value="ar">ar - Arabic</option>
<option value="ar-AE">ar-AE - Arabic (United Arab Emirates)</option>
...
<option value="en-US" selected="selected">en-US - English (United States)</option>
...
<option value="zh-TW">zh-TW - Chinese (Taiwan)</option>
</select>
<select class="col-lg" name="zoneID">
<option value="Etc/GMT+12">Etc/GMT+12 - GMT-12:00</option>
<option value="Etc/GMT+11">Etc/GMT+11 - GMT-11:00</option>
...
<option value="PST8PDT" selected="selected">PST8PDT - Pacific Standard Time</option>
...
<option value="Pacific/Kiritimati">Pacific/Kiritimati - Line Is. Time</option>
</select>
<button class="col-sm-1" type="submit">↻</button>
</form>
</main>
...
The updated View provides to selection lists and a form POST button:



A new method is added to the Controller to handle the POST /clock/time
request. Note the HttpServletRequest and
HttpSession parameters.
@RequestMapping(method = { RequestMethod.POST }, value = { "time" })
public String post(HttpServletRequest request, HttpSession session,
@RequestParam Map<String,String> form) {
for (Map.Entry<String,String> entry : form.entrySet()) {
session.setAttribute(entry.getKey(), entry.getValue());
}
return "redirect:" + request.getServletPath();
}
The selected Locale languageTag and TimeZone zoneID are written to
the @RequestParam-annotated Map with keys
languageTag and zoneID, respectively. The Map key-value pairs are
written into the HttpSession (automatically managed by Spring)
attributes.3 Adding the prefix
“redirect:” instructs Spring to respond with a 302 to cause the browser
to make a request to the new URL: GET /clock/time. That method must be
modified to set Locale based on the session languageTag and/or
TimeZone based on zoneID if specified (accessed via
@SessionAttribute).4
@RequestMapping(method = { RequestMethod.GET }, value = { "time" })
public void get(Model model, Locale locale, TimeZone zone,
@SessionAttribute Optional<String> languageTag,
@SessionAttribute Optional<String> zoneID) {
if (languageTag.isPresent()) {
locale = Locale.forLanguageTag(languageTag.get());
}
if (zoneID.isPresent()) {
zone = TimeZone.getTimeZone(zoneID.get());
}
model.addAttribute("locale", locale);
model.addAttribute("zone", zone);
...
}
A couple of alternative Locales and TimeZones:


Summary
This article demonstrates Spring MVC with Thymeleaf templates by implementing a simple, but internationalized, clock web application.
[1] Part 2 of this series demonstrated the inclusion of Bulma artifacts and the use of Bootstrap here is to provide contrast. ↩
[2] Arguably, the sorting of the lists should be part of the View and included in the template but that would overly complicate the implementation. ↩
[3]
An alternative strategy would be to include the POST @RequestParams as
query parameters in the redirected URL.
↩
[4]
To avoid specifying the @SessionAttribute name attribute, the Java code
must be compiled with the javac -parameters option so the method
parameter names are available through reflection. Please see the
configuration of the maven-compiler-plugin plug-in in the
pom.xml.
↩