Spring MVC Integration
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Thymeleaf 3.1.5.RELEASE (2026-04-21), Spring Boot 4.1.1 (Spring Framework 7.0.9) and Java 21, checked on 2026-10-08, and the
examples were compiled and run on that stack in a scratch Bookshelf application (Maven wrapper, JDK 26 with --release 21, MockMvc and a plain
Spring MVC context without Boot); the rendered output shown is the real output of those runs. The page applies to Thymeleaf with Spring MVC
(the servlet stack). The Spring dialect and the SpEL expressions it enables are shared with Spring WebFlux, but the view classes shown here are
the servlet ones; WebFlux has its own reactive view resolver (see the Spring Boot Auto-configuration & Properties page).
Thymeleaf standalone has no controllers and no view resolvers; see Standalone Engine & Configuration.
Thymeleaf was designed as a template engine, not as a web framework, and Spring MVC already decides which handler runs, which model it fills and
which view name it returns. The integration is therefore the thin layer that lets a Spring MVC ViewResolver hand a view name to a Thymeleaf
TemplateEngine, and that lets the templates use Spring’s own facilities (SpEL, MessageSource, conversion, validation results,
RequestDataValueProcessor). This page covers that layer and the controller conventions that feed it; forms, validation, conversion and
internationalization have their own pages. The running scenario is the Bookshelf introduced on the Getting Started page.
What thymeleaf-spring6 provides
Thymeleaf ships its Spring integration as separate artifacts per Spring generation: thymeleaf-spring5 and thymeleaf-spring6. There is no
thymeleaf-spring7: Spring Framework 7 applications use org.thymeleaf:thymeleaf-spring6 3.1.5.RELEASE, which is also what the Spring Boot 4.1 BOM
manages. The tutorial states the migration rule for older code in one sentence: replace the org.thymeleaf.spring6 package with org.thymeleaf.spring5
if the application still runs on Spring 5.
The integration lets Spring MVC controllers forward to Thymeleaf templates exactly as they would to JSPs, replaces OGNL with the Spring Expression
Language (SpEL), connects forms to form-backing beans and binding results, shows messages from the Spring MessageSource, and resolves templates
through Spring’s resource abstraction. In a Boot application the starter brings everything:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
The SpringStandard dialect
The dialect that does the work is org.thymeleaf.spring6.dialect.SpringStandardDialect, a subclass of org.thymeleaf.standard.StandardDialect. Everything
from the Standard Dialect is inherited (the th:* attributes, the five expression types), and the Spring dialect adds:
-
SpEL as the expression language: every
${...}and*{...}expression is evaluated by Spring’s engine instead of OGNL, which gives safe navigation (?.),${@beanName.method()}access to any bean of the application context and the SpEL compiler -
the form attributes
th:field,th:errorsandth:errorclass, and ath:objectthat selects a form command object (see the the Forms & Data Binding page) -
the expression object
#themes(the counterpart of thespring:themeJSP tag) and#mvcwith itsurl(...)method (the counterpart ofspring:mvcUrl(...), see Link URLs) -
th:method, which lets a form declarePUT,PATCHorDELETE(see Hidden HTTP methods below)
The tutorial advises against registering this dialect on a plain TemplateEngine. Use org.thymeleaf.spring6.SpringTemplateEngine, which applies
the dialect and wires Spring’s MessageSource in for you.
The engine, the resolver and the view resolver
Three beans connect Spring MVC to Thymeleaf, from the template up to the web layer: a template resolver finds the template file, the template engine parses and processes it, and a view resolver turns a controller’s view name into a view that calls the engine.
SpringStandardDialect"] eng --> res["SpringResourceTemplateResolver
prefix, suffix, mode, cache"] res --> file["classpath:/templates/books.html"]
A manual configuration (no Spring Boot)
Without Boot you declare the three beans yourself. The resolver is the Spring variant, SpringResourceTemplateResolver, which uses Spring’s own
Resource loading (so a prefix may be classpath:/templates/, file:... or a servlet-context path) and needs the ApplicationContext.
@Configuration
@EnableWebMvc
class WebConfig implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(applicationContext);
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML); // HTML is the default
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(true); // false while developing
return resolver;
}
@Bean
SpringTemplateEngine templateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(templateResolver());
engine.setEnableSpringELCompiler(true); // see the SpEL compiler below
return engine;
}
@Bean
ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(1);
resolver.setViewNames(new String[] {"manual/*"});
return resolver;
}
}
The configuration was exercised in a plain Spring MVC web context built with AnnotationConfigWebApplicationContext and MockMvc, with a controller
that returns the view name manual/hello:
<p th:text="'Hello, ' + ${name}">Hello</p>
<p>Hello, Bookshelf</p>
The test also asserted that engine.getEnableSpringELCompiler() returns true. The prefix and suffix live on the template resolver, never on the
view resolver; that is the main difference from a JSP InternalResourceViewResolver.
What Spring Boot configures
In a Boot application none of the three beans is written by hand. ThymeleafAutoConfiguration (module spring-boot-thymeleaf in Boot 4) registers a
SpringResourceTemplateResolver, a SpringTemplateEngine and a ThymeleafViewResolver from the spring.thymeleaf.* properties, and each
is backed off when you declare your own (the view resolver is conditional on a bean named thymeleafViewResolver). The default view
resolver gets the order Ordered.LOWEST_PRECEDENCE - 5, so that other resolvers, such as Boot’s BeanNameViewResolver, are consulted first. The
property-to-bean mapping was verified with a test:
spring.thymeleaf.enable-spring-el-compiler=true
spring.thymeleaf.excluded-view-names=legacy/*
@SpringBootTest(properties = {
"spring.thymeleaf.enable-spring-el-compiler=true",
"spring.thymeleaf.excluded-view-names=legacy/*"})
class BootViewResolverTest {
@Autowired SpringTemplateEngine engine;
@Autowired @Qualifier("thymeleafViewResolver") ThymeleafViewResolver resolver;
@Test
void propertiesReachTheBeans() {
assertThat(engine.getEnableSpringELCompiler()).isTrue();
assertThat(resolver.getExcludedViewNames()).containsExactly("legacy/*");
assertThat(resolver.getOrder()).isEqualTo(Integer.MAX_VALUE - 5);
}
}
Printing the same values gave:
order=2147483642 excluded=[legacy/*] dialects=[SpringStandardDialect, BookshelfDialect, HtmxDialect, SpringSecurityDialect, LayoutDialect]
The dialect list is that of the scratch application: SpringStandardDialect is always first, and every IDialect bean (the Layout Dialect, the Spring
Security dialect, the htmx dialect and the application’s own dialect) is added by the auto-configuration. The complete property table, including the
cache, encoding, template-check and reactive keys, is on Spring Boot Auto-configuration & Properties.
Source: Spring Boot — Template Engines
The SpEL compiler
SpEL can compile frequently evaluated expressions to bytecode. Thymeleaf exposes the switch as SpringTemplateEngine.setEnableSpringELCompiler(boolean), and Boot as
spring.thymeleaf.enable-spring-el-compiler. It defaults to false, as the tutorial explains, because compilation can break templates in which the same
expression text is evaluated against objects of different types (for example a fragment reused with a Book in one place and a Magazine in another). Turn it on
for hot, type-stable pages, measure, and keep the exception in mind: a compiled expression that meets a different type fails at run time and not at
parse time. The complete caching and performance discussion is on the Performance & Production page.
Views and view resolvers
Spring MVC’s templating core is two interfaces, org.springframework.web.servlet.View and org.springframework.web.servlet.ViewResolver. A View renders
a page (usually by running a template engine); a ViewResolver maps the view name returned by a controller, plus the locale, to a View. The resolvers
form an ordered chain, and the first one that can resolve the name wins. Thymeleaf supplies one class for each side:
-
org.thymeleaf.spring6.view.ThymeleafView -
org.thymeleaf.spring6.view.ThymeleafViewResolver
Most pages do not need a View bean: when no bean has the view name, the resolver creates a ThymeleafView on the fly for each name it resolves.
ThymeleafViewResolver properties
| Setter / Boot property | Meaning |
|---|---|
|
Position in the resolver chain. Lower values are asked first. Boot uses |
|
Patterns (with |
|
Patterns it must not handle, for names that another resolver (a JSP resolver, a PDF view) should serve |
|
The engine that renders the views; required |
|
Response encoding and content type (Boot: |
|
Whether output is flushed while the template is still being processed (Boot: |
|
How a |
|
Whether |
|
The |
Redirect and forward view names
A view name that starts with redirect: or forward: is not a template name. The ThymeleafViewResolver recognizes the two prefixes (they are public
constants, REDIRECT_URL_PREFIX and FORWARD_URL_PREFIX) and returns Spring’s RedirectView or an InternalResourceView instead of a ThymeleafView. The path after
a redirect: prefix that starts with / is relative to the application’s context path, unless setRedirectContextRelative(false) was called. The two behave very differently for
the browser:
-
redirect:/mvc/booksanswers302 Foundwith aLocationheader, so the browser issues a second request and its address bar changes -
forward:/mvc/booksis handled inside the server by the servlet container: one request, one response, the URL does not change, and the request attributes are kept
@GetMapping("/home")
String home() { return "redirect:/mvc/books"; }
@GetMapping("/catalog")
String catalog() { return "forward:/mvc/books"; }
dump("home", mvc.perform(get("/mvc/home")).andExpect(status().isFound()).andExpect(redirectedUrl("/mvc/books")).andReturn());
dump("catalog", mvc.perform(get("/mvc/catalog")).andExpect(forwardedUrl("/mvc/books")).andReturn());
GET /mvc/home -> status=302 location=/mvc/books
GET /mvc/catalog -> status=200 forwardedUrl=/mvc/books (the container performs the forward)
Another check confirmed that /mvc/home produced a Location without a query string, although the request had model attributes (appName,
activeMenu) at the time. Do not rely on that when you add your own attributes to a redirecting request: pass what the target needs explicitly, as
flash attributes or as redirectAttributes.addAttribute(...) (which become query parameters).
A ThymeleafView bean with static variables
When one page needs a special configuration, you can declare its ThymeleafView as a prototype bean. The tutorial adds static variables this way, which become
available as ordinary context variables to the template:
@Bean
@Scope("prototype")
ThymeleafView mainView() {
ThymeleafView view = new ThymeleafView("mvc/main"); // the template name
view.setStaticVariables(Collections.singletonMap("footer", "Bookshelf Ltd."));
return view;
}
@GetMapping("/main")
String main() { return "mainView"; } // a View bean name, not a template name
<main>Main page</main>
<footer th:text="${footer}">footer</footer>
<main>Main page</main>
<footer>Bookshelf Ltd.</footer>
The tutorial’s recipe works as printed with a single ThymeleafViewResolver. In a Boot application it does not: Boot registers a BeanNameViewResolver that comes
first in the chain and returns the raw ThymeleafView bean without giving it the template engine or the locale, so the first request fails with
IllegalArgumentException: Property 'locale' is required. The working variant used above is a second ThymeleafViewResolver that runs earlier and claims only that name:
@Bean
ThymeleafViewResolver viewBeanResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine);
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(1);
resolver.setViewNames(new String[] {"mainView"});
return resolver;
}
If all you need is a footer text on every page, a @ModelAttribute method (next section) is simpler than a view bean; reserve view beans for the rare page that
needs its own markupSelector or a custom View subclass. Declaring a second ThymeleafViewResolver also means that autowiring a single one by type fails with
NoUniqueBeanDefinitionException, as one of the scratch tests found; inject by name or qualifier.
Controllers, models and model attributes
A Thymeleaf template never talks to the controller. It receives a context, a map of variables, and Spring MVC’s model becomes that map: every attribute in the
Model (or ModelAndView, or ModelMap) is a Thymeleaf context variable of the same name. Spring calls them model attributes; the Thymeleaf term is context
variables. Request-mapping annotations and argument resolution are Spring’s business and the Spring Framework reference is the authority; this section shows the
parts that matter for templates.
Mapping, model and return types
@Controller
@RequestMapping("/mvc")
public class MvcController {
private final ShelfService shelf;
public MvcController(ShelfService shelf) { this.shelf = shelf; }
@GetMapping("/books")
String list(Model model, BookForm bookForm) {
model.addAttribute("books", shelf.findAll());
return "mvc/books";
}
@GetMapping("/books/{id}")
ModelAndView detail(@PathVariable long id, @RequestParam(defaultValue = "false") boolean full) {
BookInfo book = shelf.find(id);
if (book == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
ModelAndView mav = new ModelAndView("mvc/book");
mav.addObject("book", book);
mav.addObject("full", full);
return mav;
}
// ...
}
<h1 th:text="${book.title}">Title</h1>
<p th:text="${book.author}">Author</p>
<p th:if="${full}">Full details requested</p>
Requesting /mvc/books/1?full=true rendered:
<h1>Dune</h1>
<p>Frank Herbert</p>
<p>Full details requested</p>
Requesting /mvc/books/99 answered 404 through the ResponseStatusException. The pieces:
-
@GetMapping,@PostMapping,@PutMapping,@PatchMappingand@DeleteMappingmap one HTTP method each;@RequestMappingon the class adds a path prefix. Two methods that map the same method and path make the application fail at startup with anIllegalStateExceptionabout an ambiguous mapping, so a typo cannot silently shadow a page -
a
Stringreturn value is a view name; aModelAndViewcarries the name and the model together; aModelorModelMapargument is how aString-returning method fills the model -
@PathVariablebinds a segment of the path (/books/{id}) and@RequestParama query or form parameter, converted to the parameter type by the conversion service (see the Conversion & Formatting page) -
a form object as an argument (the
BookForm bookForminlist(...)) is added to the model under its decapitalized class name, which is whatth:object="${bookForm}"binds to
A handler may also return a view name computed from request input, for example return "pages/" + page;. Do not do this. The view name decides which template
Thymeleaf resolves, so request-controlled names let a visitor render templates you never meant to expose, and a name such as pages/x :: secret also selects fragments. Map
fixed paths to fixed names, or look the value up in a whitelist (see the Template Security & XSS page for the written-up attack).
Model attributes shared by many handlers
A method annotated with @ModelAttribute runs before every handler method of its controller and adds its return value to the model. This is the idiom for lists a
page always needs (a drop-down of genres, the authors) and for marking the active menu item:
// Runs before every handler method of this controller; the value is a constant, never request input
@ModelAttribute("activeMenu")
String activeMenu() { return "books"; }
<nav>
<a href="books.html" th:href="@{/mvc/books}" th:classappend="${activeMenu == 'books'} ? 'active'">Books</a>
</nav>
<nav>
<a href="/mvc/books" class="active">Books</a>
</nav>
The same annotation in a @ControllerAdvice class is global: it applies to the handlers of every controller the advice covers. Without arguments the advice covers all controllers; the
selectors basePackageClasses, basePackages, assignableTypes and annotations narrow it. The Bookshelf advice below is limited to the controllers of one package:
@ControllerAdvice(basePackageClasses = MvcController.class)
public class MvcAdvice {
@ModelAttribute("appName")
String appName() { return "Bookshelf"; }
}
<head><title th:text="${appName}">Bookshelf</title></head>
The test asserted model().attribute("appName", "Bookshelf") and model().attribute("activeMenu", "books"), and the rendered <head> was <head><title>Bookshelf</title></head>.
The Thymeleaf and Spring documents both treat this as the sanctioned way to expose common data; the other two are Model.addAttribute and ModelAndView.addObject.
Post/Redirect/Get
A browser that receives HTML as the answer to a POST and is then reloaded asks the user whether to submit the form again. The cure is the Post/Redirect/Get pattern: the POST
handler does its work and answers with a redirect, and the browser then loads the result with an ordinary GET. Reloading repeats only the GET. A POST with
validation errors is the exception: it must not redirect, because the binding result with the errors would be lost; it renders the form again with status 200.
@PostMapping("/books")
String create(@Valid BookForm bookForm, BindingResult result, Model model, RedirectAttributes redirect) {
if (result.hasErrors()) {
model.addAttribute("books", shelf.findAll());
return "mvc/books"; // 200, no redirect: the errors must survive
}
shelf.add(bookForm.getTitle(), bookForm.getAuthor());
redirect.addFlashAttribute("message", "Book saved");
return "redirect:/mvc/books"; // 302: the browser then issues a GET
}
<p th:if="${message}" th:text="${message}">Flash message</p>
RedirectAttributes.addFlashAttribute keeps the value in the session just long enough for the redirected request, then removes it; the template reads it as a normal
model attribute. The test drove the whole round trip with MockMvc:
MvcResult post = mvc.perform(post("/mvc/books").with(csrf()).param("title", "Ulysses").param("author", "James Joyce"))
.andExpect(status().isFound()).andExpect(redirectedUrl("/mvc/books")).andReturn();
MvcResult get = mvc.perform(get("/mvc/books").flashAttrs(post.getFlashMap())).andExpect(status().isOk())
.andExpect(content().string(containsString("Book saved"))).andReturn();
// a refresh of the GET shows no flash message and does not add a second book
mvc.perform(get("/mvc/books")).andExpect(content().string(not(containsString("Book saved"))));
POST /mvc/books -> status=302 location=/mvc/books flash={message=Book saved}
GET /mvc/books -> status=200, body contains <p>Book saved</p> and the new Ulysses entry
GET /mvc/books -> status=200, no flash message
The invalid submission (title empty) answered 200 with the view mvc/books and the message of the @NotBlank constraint next to the field:
<input type="text" id="title" name="title" value="">
<span>must not be blank</span>
<input type="text" id="author" name="author" value="">
Flash messages and error pages are developed further on the Error Handling & Flash Messages page, and the form itself on the Forms & Data Binding page.
Hidden HTTP methods
An HTML form can only submit GET or POST. REST-style controllers use PUT, PATCH and DELETE, and Spring bridges the gap with HiddenHttpMethodFilter, which reads a request parameter
named _method from a POST and wraps the request as if it had that method. Thymeleaf’s side is th:method: when its value is a method other than get or post, such as delete, the processor
writes method="post" and adds the hidden _method field. In Boot the filter is off by default:
spring.mvc.hiddenmethod.filter.enabled=true
@DeleteMapping("/books/{id}")
String delete(@PathVariable long id, RedirectAttributes redirect) {
shelf.remove(id);
redirect.addFlashAttribute("message", "Book deleted");
return "redirect:/mvc/books";
}
<form th:action="@{/mvc/books/{id}(id=${b.id})}" th:method="delete">
<button type="submit">Delete</button>
</form>
The rendered form (the CSRF token is shortened here):
<form action="/mvc/books/1" method="post"><input type="hidden" name="_csrf" value="6wbaCJ1m..."/><input type="hidden" name="_method" value="delete"/>
<button type="submit">Delete</button>
</form>
Two details deserve attention. First, the _csrf field appears because the form uses th:action: Thymeleaf asks Spring’s RequestDataValueProcessor for extra hidden fields,
and Spring Security’s implementation contributes the CSRF token. A form written with a plain action attribute would get no token and Spring Security would reject the submission
with 403. Second, the state change is a POST carrying a CSRF token, never a GET link, which is why the test requests below behave as shown:
mvc.perform(post("/mvc/books/2").with(csrf()).param("_method", "delete"))
.andExpect(status().isFound()).andExpect(redirectedUrl("/mvc/books"));
mvc.perform(post("/mvc/books/2").param("_method", "delete")).andExpect(status().isForbidden());
POST /mvc/books/2 _method=delete + CSRF token -> 302 Location /mvc/books, flash {message=Book deleted}
POST /mvc/books/2 _method=delete, no token -> 403
POST /mvc/books/2 _method=delete, filter off -> 405, HttpRequestMethodNotSupportedException: Request method 'POST' is not supported
The last row comes from a run with spring.mvc.hiddenmethod.filter.enabled=false. The filter accepts the methods PUT, DELETE and PATCH. An alternative that
needs no filter is a dedicated @PostMapping("/books/{id}/delete"); both are fine. When JavaScript chooses the form action (for example a confirmation dialog), include the
CSRF token in the form yourself; the Spring Security Integration page shows how.
Source: Spring Framework — Filters
Request, session and application data
The tutorial section on STSM shows the model route; the thymeleaf.org article Spring MVC and Thymeleaf: how to access data from templates lists the rest. All of it is
read with ${...}; the dialect decides what each name means:
| Expression | Reads |
|---|---|
|
A model attribute (a context variable); this includes request attributes |
|
The request parameter |
|
An |
|
A |
|
A Spring bean by name, a SpEL feature |
@GetMapping("/data")
String data(HttpSession session, Model model) {
session.setAttribute("lastSearch", "dune");
model.addAttribute("total", shelf.count());
return "mvc/data";
}
<p id="model" th:text="${appName} + ': ' + ${total} + ' books'">model</p>
<p id="param" th:text="${param.q}">request parameter</p>
<p id="params" th:if="${param.tag != null}" th:text="${param.tag[0] + ' / ' + param.tag[1]}">several values</p>
<p id="session" th:text="${session.lastSearch}">session attribute</p>
<p id="app" th:text="${application.catalogVersion}">servlet context attribute</p>
<p id="bean" th:text="${@shelfService.count()}">bean call</p>
The servlet context attribute catalogVersion was set by the configuration class through ServletContextAware. Requesting /mvc/data?q=Thymeleaf+is+great&tag=a&tag=b produced:
<p id="model">Bookshelf: 2 books</p>
<p id="param">Thymeleaf is great</p>
<p id="params">a / b</p>
<p id="session">dune</p>
<p id="app">2026.10</p>
<p id="bean">2</p>
Version 3.1 removed the old expression objects #request, #response, #session and #servletContext, because the engine no longer assumes the servlet API: the
article above still shows param, session and application, which remain supported, and these cover the cases the removed objects were used for. If a value of the request is needed for
the page, add it to the model in the controller (for example the current path) instead of reaching into the request from the template. The utility objects and the removal are discussed on
Expression Utility Objects and Standard Expression Syntax.
Beans are reachable from every template, which is powerful and a reason for discipline: expose a small, read-only bean for the template (a ShelfService.count() style query), never a
repository or a service with side effects. Restricted mode forbids some expression forms in places where user input could reach them; see the Template Security & XSS page.
The Spring Thyme Seed Starter Manager
The thymeleaf.org tutorial builds its Spring chapters around a sample application, the Spring Thyme Seed Starter Manager (STSM): one page that lists seed starters and a form to add new
ones, with a SeedStarterMngController whose @ModelAttribute methods populate the drop-downs and whose two mapped methods show the form (GET) and process it (POST, then redirect:).
Its Spring MVC configuration class declares exactly the three beans described above, a ResourceBundleMessageSource, and two formatters registered through
WebMvcConfigurer.addFormatters. The same controller pattern, a GET that shows the form and a POST that validates, saves and redirects, is what the Bookshelf controller above follows,
and the form, validation and conversion chapters of the tutorial are covered on the Forms, Validation and Conversion pages of this section. The application is on GitHub as
thymeleafexamples-stsm (a Spring 5 and a Spring 6 version); like the tutorial it uses a hand-written @EnableWebMvc configuration, whereas a Boot application replaces that class with the starter and the properties shown earlier.
It is still a prototype
The tutorial’s section 8 makes the point of the whole engine: the STSM template still opens in a browser from the file system, because every th:* attribute is added to markup
that already works. The Bookshelf templates above do the same; for instance <a href="books.html" th:href="@{/mvc/books}" ...> keeps a clickable link in the static prototype. Only the Spring-specific attributes
(th:field, th:errors) show their placeholder values in a prototype, and the page on Natural Templates & Decoupled Logic shows how to keep those
natural as well.
Spring WebFlow (legacy)
The Spring integration packages include an adapter for Spring WebFlow, the older XML-driven conversation framework. It is a legacy note here: new applications use plain controllers, redirects and flash attributes (for the AJAX-style partial updates it was used for, see the Fragment Rendering & AJAX page). It matters only when an existing WebFlow application is being upgraded. The tutorial states that Spring 6 needs Spring WebFlow 3.0 or later (and Spring 5 needs 2.5), and that the integration replaces the view resolver:
@Bean
public ViewResolver thymeleafViewResolver() {
AjaxThymeleafViewResolver viewResolver = new AjaxThymeleafViewResolver();
// A special ThymeleafView implementation is needed
viewResolver.setViewClass(FlowAjaxThymeleafView.class);
viewResolver.setTemplateEngine(templateEngine());
return viewResolver;
}
AjaxThymeleafViewResolver and FlowAjaxThymeleafView live in the package org.thymeleaf.spring6.webflow.view; the other beans (flow registry and executor, FlowBuilderServices, an
MvcViewFactoryCreator that receives the view resolver) are configured as the Spring WebFlow reference describes, and a view-state then names a Thymeleaf template. The AJAX fragments of a
transition are listed in a <render fragments="..."/> element and must be elements that carry an id. This snippet is the tutorial’s, quoted from its description; it was not run, because the
scratch application has no WebFlow dependency.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
In Boot, |
Add an earlier |
|
|
Set |
|
The form used |
Use |
The same form is submitted again when the page is reloaded |
The |
Post/Redirect/Get: return |
The flash message never appears |
It was added with |
Use |
A template under |
The controller builds the view name from request data |
Return fixed view names, or look the value up in a whitelist |
|
A second resolver bean was declared and another bean injects the type |
Inject with |
|
The objects were removed in Thymeleaf 3.1 |
Use |
A compiled SpEL expression fails with a |
|
Turn the compiler off for that application, or keep each expression’s types stable |
Changes to a template do not show up |
|
Set |
Version notes
-
The Spring dialect and view classes live in
org.thymeleaf.spring6for Spring 6 and 7; the Thymeleaf + Spring tutorial for 3.1 states that its examples use Spring 6.x andthymeleaf-spring6, and replaces nothing for Spring 7 (there is nothymeleaf-spring7). -
In Boot 4 the Thymeleaf auto-configuration moved to the
spring-boot-thymeleafmodule, with the packageorg.springframework.boot.thymeleaf.autoconfigure. The MockMvc test support moved too (org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc). -
spring.thymeleaf.enabledhas been deprecated at error level since Boot 4.0.0 and no longer disables the auto-configuration; depend onthymeleaf-spring6directly if you do not want the auto-configuration. -
th:includeand the unwrapped fragment syntax are deprecated in 3.1, andth:substitutebywas removed; the#request,#response,#sessionand#servletContextobjects were removed; see the What’s Changed & Migration page. -
The consulted edition of Taming Thymeleaf (Wim Deblauwe, PDF v1.1.1, 2021) teaches controllers, Post/Redirect/Get,
HiddenHttpMethodFilterand the active menu item with Spring Boot 2.3, Java 11 and Thymeleaf 3.0; the ideas are unchanged, but usejakarta.*imports,spring-boot-starter-webmvcand the packages listed above.
References
-
Tutorial: Thymeleaf + Spring 3.1 — Integrating Thymeleaf with Spring
-
Tutorial: Thymeleaf + Spring 3.1 — The SpringStandard Dialect
-
Tutorial: Thymeleaf + Spring 3.1 — Spring Thyme Seed Starter Manager
-
Tutorial: Thymeleaf + Spring 3.1 — Spring WebFlow integration
-
Thymeleaf — Spring MVC and Thymeleaf: how to access data from templates
-
GitHub — thymeleafexamples-stsm (Spring Thyme Seed Starter Manager)