Chaining Thymeleaf template resolvers
12 Aug 2022 - Tobias ErdleTL;DR
To chain template resolvers in Thymeleaf, you need to set the order via AbstractTemplateResolver#setOrder
for each template resolver and check for existence with AbstractTemplateResolver#setCheckExistence
in the leading resolvers to allow a fallthrough to following ones, handling the not resolvable template. Then add them to the
TemplateEngine
with TemplateEngine#addTemplateResolver
.
Initial situation
Today I tried to fix an issue in the Krazo Thymeleaf Extension which drives my crazy everytime I use
this extension, namely that using fragments forces me to add the whole path of the template located in /WEB-INF/views
(or wherever the Jakarta MVC application is configured
to search for views). So I started to debug the issue using this easy templates where index.html
is the view and fragment.html
contains a simple HTML fragment to include.
<!-- index.html -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Thymeleaf default view folder test</title>
</head>
<body>
<div th:replace="fragment :: test"></div>
</body>
</html>
<!-- fragment.html -->
<div th:fragment="test" id="from-fragment">I'm from the fragment</div>
The currently used template engine was produced with a single org.thymeleaf.templateresolver.WebApplicationTemplateResolver
using its default config. Now I started to debug
and recognized, that the view returned by the controller could be resolved because it contained the full path when fetched from the jakarta.mvc.engine.ViewEngineContext
. On the
other hand, the fragment was only resolved as /fragment
which surely can't be found in this location when it is really located in /WEB-INF/views/
. So I tried a few things until I came to a positive result.
Using prefixes / suffixes in default template resolver
My first approach was to use the setPrefix
and setSuffix
method of WebApplicationTemplateResolver
to set the required values. My expectation was, that the resolver recognizes
that /WEB-INF/views/index.html
has the correct format and does nothing, whereas fragments
does not and needs them applied. Unfortunately, with this approach the template resolver added prefix and suffix even to the /WEB-INF/views/index.html
so the path got wrong and not resolvable. After this failure I tried another approach, testing if a second template resolver may help.
Using a second template resolver
So after I couldn't implement my desired behavior with a single template resolver, I added a second one which got configured like this:
final String mvcViewPath = //...
final WebApplicationTemplateResolver secondResolver = new WebApplicationTemplateResolver(servletApplication);
secondResolver.setPrefix(mvcViewPath);
secondResolver.setSuffix(FILE_SUFFIX);
secondResolver.setTemplateMode(TemplateMode.HTML);
secondResolver.setCharacterEncoding(StandardCharsets.UTF_8.name());
secondResolver.setOrder(2);
//...
Here I set the views directory as a prefix and .html
as suffix, so fragment
is, assumed that mvcViewPath
evaluates to /WEB-INF/views
, transformed to /WEB-INF/views/fragment.html
. Also I set the order of the template resolver to ensure, that the default one runs first, which got the order 1
. Afterwards I added the initialized engine also to the TemplateEngine
via TemplateEngine#addTemplateResolver
and tried again. Unfortunately, it failed again.
The solution: additionally use WebApplicationTemplateResolver#setCheckExistence
I checked the available methods a few times until I came upon setCheckExistence
which contains this interesting sentence in its JavaDoc:
This allows resolvers to pass control to the next ITemplateResolver in the chain
So what I simply missed is to set this flag at the first template resolver, so in case the original fragment
template is not found, the name will be checked in the next
template resolver which adds the required prefix and suffix and will be able to find the template.
Attention: I just used two template resolvers here. In case you have more of them, all but the last one need this flag set to true
.