Spring Boot Auto-configuration & Properties

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, Spring Boot 4.1.1 (Spring Framework 7.0.9), Java 21 and the Layout Dialect 4.0.1, all checked on 2026-10-07. It applies to Thymeleaf with Spring MVC and with Spring WebFlux: the auto-configuration builds a servlet or a reactive view resolver depending on the type of web application, and the sections below say which property or bean belongs to which stack. It does not apply to standalone Thymeleaf, which has no auto-configuration (see the Standalone Engine & Configuration page). Boot 4 moved Thymeleaf support out of the old single spring-boot-autoconfigure jar into its own spring-boot-thymeleaf module, so code and blog posts written for Boot 3 mention package names and test annotations that moved; where that matters the text says so.

In the Bookshelf application that grows across this section, almost everything you never write in Java is done here: the resolver that turns the view name books/list into classpath:/templates/books/list.html, the engine that is shared by every request, the layout and Spring Security dialects and the view resolver that renders the page. This page opens the box, lists every switch Boot gives you, shows how to add an SVG icon resolver and a custom dialect, how to replace the view resolver, how to test it all, and what DevTools changes during development.

In a hurry: what goes where

Need Do this Section

Thymeleaf in a Boot web application

Add spring-boot-starter-thymeleaf next to spring-boot-starter-webmvc (or spring-boot-starter-webflux). Nothing else is required.

The starters

Know what exists in the context

One SpringResourceTemplateResolver, one SpringTemplateEngine, one ThymeleafViewResolver, plus the dialects whose jars are on the classpath.

ThymeleafAutoConfiguration: what it creates

Change a default

Set a spring.thymeleaf.* property in application.properties; every one is listed with its default.

The spring.thymeleaf properties

A second template location (icons, mail, text)

Declare an additional ITemplateResolver bean and give it an order and resolvablePatterns.

Extra template resolvers: an SVG icon resolver

A custom dialect

Declare it as a bean; every IDialect bean is added to the engine.

Extra dialect beans

Replace the view resolver

Declare a bean named thymeleafViewResolver (MVC) or thymeleafReactiveViewResolver (WebFlux).

Overriding the view resolver

Test it

spring-boot-starter-thymeleaf-test; for slice tests import your own resolver and dialect configuration.

Testing: spring-boot-starter-thymeleaf-test

Template edits not showing up

DevTools sets spring.thymeleaf.cache=false; otherwise set it in a local profile; fix the IDE classpath.

DevTools defaults, IDE classpath pitfalls

The starters

Boot 4 splits the integration into three artifacts, all managed by the Spring Boot BOM so that none of them needs a version number in a Boot project:

Artifact (org.springframework.boot) What it is

spring-boot-starter-thymeleaf

The application starter. It depends on spring-boot-starter and spring-boot-thymeleaf.

spring-boot-thymeleaf

The module that holds ThymeleafAutoConfiguration, ThymeleafProperties and the DevTools default. It depends on thymeleaf-spring6, which brings Thymeleaf itself (there is no thymeleaf-spring7; Spring Framework 7 uses the -spring6 integration).

spring-boot-starter-thymeleaf-test

The test starter. It depends on spring-boot-starter-thymeleaf and spring-boot-starter-test. It does not include a web stack: add spring-boot-starter-webmvc-test or spring-boot-starter-webflux-test for MockMvc or WebTestClient.

Neither the starter nor the module brings a web stack; a Thymeleaf-only application is not a web application, so the view resolvers below are only created when spring-boot-starter-webmvc or spring-boot-starter-webflux is present. The Layout Dialect, the Spring Security dialect and the data-attribute dialect are optional: Boot registers each one only if its jar is on the classpath.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!-- optional dialects: versions are managed by the Boot BOM -->
    <dependency>
        <groupId>nz.net.ultraq.thymeleaf</groupId>
        <artifactId>thymeleaf-layout-dialect</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity6</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webmvc-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

ThymeleafAutoConfiguration: what it creates

org.springframework.boot.thymeleaf.autoconfigure.ThymeleafAutoConfiguration is registered through the META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file of the spring-boot-thymeleaf module. It is a @AutoConfiguration ordered after the WebMvc and WebFlux auto-configurations, active when TemplateMode and SpringTemplateEngine are on the classpath, and it binds ThymeleafProperties to the spring.thymeleaf prefix. Everything it declares is a plain @Bean with a back-off condition, so you replace a piece by declaring your own, not by editing configuration.

Bean (name) Created when Notes

defaultTemplateResolver (SpringResourceTemplateResolver)

No bean named defaultTemplateResolver exists.

Built from prefix, suffix, mode, encoding, cache, template-resolver-order and check-template. At startup it logs a warning if check-template-location is true and the prefix location does not exist.

templateEngine (SpringTemplateEngine)

No ISpringTemplateEngine bean exists (MVC and the default case).

Sets the SpringEL compiler and hidden-checkbox-marker flags from properties, then adds every ITemplateResolver bean and every IDialect bean in @Order order.

templateEngine (SpringWebFluxTemplateEngine)

The application is reactive and no ISpringWebFluxTemplateEngine bean exists.

The same assembly as above, for WebFlux.

thymeleafViewResolver (ThymeleafViewResolver)

Servlet application, AbstractCachingViewResolver on the classpath and no bean named thymeleafViewResolver.

Character encoding and Content-Type come from encoding and servlet.content-type; partial output, view-names, excluded-view-names and cache from their properties. Order Ordered.LOWEST_PRECEDENCE - 5, so it is a fallback after more specific resolvers.

thymeleafViewResolver (ThymeleafReactiveViewResolver)

Reactive application and no bean named thymeleafReactiveViewResolver (note the different name).

Media types, chunk size, full-mode and chunked-mode view names from reactive.*. Same order as the servlet one.

resourceUrlEncodingFilter (FilterRegistrationBean)

Servlet application, a resource chain is enabled and no other filter bean of that type exists.

Registers ResourceUrlEncodingFilter for the REQUEST and ERROR dispatcher types so that @{/css/site.css} is rewritten to the versioned URL.

layoutDialect (LayoutDialect)

nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect is on the classpath and no LayoutDialect bean exists.

Enables layout:decorate and friends without any configuration.

dialect (DataAttributeDialect)

thymeleaf-extras-data-attribute is on the classpath and no such bean exists.

Enables the data: attribute processors. The bean method is named dialect.

securityDialect (SpringSecurityDialect)

thymeleaf-extras-springsecurity6 and Spring Security’s web classes (CsrfToken) are on the classpath.

Enables the sec: attributes and the #authentication/#authorization objects.

The engine is assembled from collections, not from fixed beans. Because SpringTemplateEngine receives ObjectProvider<ITemplateResolver> and ObjectProvider<IDialect>, any resolver or dialect bean in the context is picked up, including those that come from third-party starters; the htmx integration htmx-spring-boot-thymeleaf works this way by declaring an IDialect bean (named htmxDialect in the scratch application). The SpringStandardDialect is the engine’s default dialect and is always present.

flowchart TD A["Application starts"] --> B{"thymeleaf-spring6 on the classpath?"} B -- no --> Z["ThymeleafAutoConfiguration inactive"] B -- yes --> P["Bind spring.thymeleaf.* to ThymeleafProperties"] P --> R["defaultTemplateResolver: SpringResourceTemplateResolver"] P --> D{"Dialect jars present?"} D -- "layout dialect" --> D1["layoutDialect bean"] D -- "data-attribute" --> D2["dialect bean"] D -- "springsecurity6" --> D3["securityDialect bean"] R --> E["templateEngine: collects every ITemplateResolver and IDialect bean"] D1 --> E D2 --> E D3 --> E E --> W{"Web application type?"} W -- servlet --> V1["thymeleafViewResolver + ResourceUrlEncodingFilter"] W -- reactive --> V2["ThymeleafReactiveViewResolver"] W -- none --> N["Engine only, no view resolver"]

The next test prints what the engine really contains in the Bookshelf application (Layout Dialect, Spring Security and the htmx integration are on its classpath, plus the two extras added later on this page):

@SpringBootTest
class AutoConfigurationTest {

    @Autowired ApplicationContext ctx;
    @Autowired SpringTemplateEngine engine;

    @Test
    void listsBeans() {
        System.out.println(Arrays.toString(ctx.getBeanNamesForType(ITemplateResolver.class)));
        System.out.println(engine.getDialects().stream().map(IDialect::getName).toList());
        System.out.println(engine.getTemplateResolvers().stream()
                .map(r -> r.getName() + ":" + r.getOrder()).toList());
    }
}
[iconTemplateResolver, defaultTemplateResolver]
[SpringStandard, Bookshelf, Htmx, SpringSecurity, Layout]
[org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver:2, org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver:null]

The spring.thymeleaf properties

All properties live under the spring.thymeleaf prefix and are bound to ThymeleafProperties. The defaults below were read from the spring-configuration-metadata.json of spring-boot-thymeleaf 4.1.1 and from its source. The reactive-only and servlet-only keys are marked.

Property Default Effect

spring.thymeleaf.cache

true

Whether the template resolver caches parsed templates and whether the servlet view resolver caches its views. DevTools turns it off (see DevTools defaults).

spring.thymeleaf.check-template

true

Sets checkExistence on the default resolver: the resolver verifies the template exists before claiming it, so a later resolver in the chain gets a chance.

spring.thymeleaf.check-template-location

true

At startup, warns if the prefix location does not exist. It never fails the application.

spring.thymeleaf.enable-spring-el-compiler

false

Turns on the SpringEL compiler for expressions in templates, a performance option for hot templates.

spring.thymeleaf.encoding

UTF-8

Character encoding of the template files and of the response (servlet view resolver) or the default charset (reactive).

spring.thymeleaf.excluded-view-names

none

View names, patterns allowed, that the Thymeleaf view resolver must not resolve.

spring.thymeleaf.mode

HTML

The TemplateMode of the default resolver (HTML, XML, TEXT, JAVASCRIPT, CSS, RAW).

spring.thymeleaf.prefix

classpath:/templates/

Prepended to the view name to build the resource location.

spring.thymeleaf.suffix

.html

Appended to the view name.

spring.thymeleaf.template-resolver-order

none

Order of the default resolver in the chain. Needed only if you declare additional resolver beans (see Extra template resolvers: an SVG icon resolver).

spring.thymeleaf.view-names

none

View names, patterns allowed, that can be resolved; everything else is left to other view resolvers.

spring.thymeleaf.render-hidden-markers-before-checkboxes

false

Renders the hidden marker input of a checkbox before the checkbox itself instead of after it.

spring.thymeleaf.servlet.content-type (servlet)

text/html

The Content-Type written to responses; Boot appends ;charset= from encoding if the value has none.

spring.thymeleaf.servlet.produce-partial-output-while-processing (servlet)

true

Start writing output as early as possible; set it to false to buffer until processing ends (an error then yields a clean error page, not a half-written one).

spring.thymeleaf.reactive.chunked-mode-view-names (reactive)

none

Views run in CHUNKED mode when a maximum chunk size is set; if this list is given, only these.

spring.thymeleaf.reactive.full-mode-view-names (reactive)

none

Views run in FULL mode even when a maximum chunk size is set.

spring.thymeleaf.reactive.max-chunk-size (reactive)

0B

Maximum size of the data buffers written to the response; only values above zero are applied, and templates then run in CHUNKED mode by default.

spring.thymeleaf.reactive.media-types (reactive)

text/html, application/xhtml+xml, application/xml, text/xml, application/rss+xml, application/atom+xml, application/javascript, application/ecmascript, text/javascript, text/ecmascript, application/json, text/css, text/plain, text/event-stream

The media types the reactive view resolver supports.

spring.thymeleaf.enabled

removed

Deprecated with level error since 4.0.0 ("no longer required"), so it is no longer supported and setting it does not switch the auto-configuration off. To use Thymeleaf without auto-configuration, depend on thymeleaf (or thymeleaf-spring6) directly instead of spring-boot-thymeleaf.

Three things the table does not show. First, check-template and check-template-location look alike but do different jobs: the first is a per-view check with an effect on resolver chaining, the second a one-time startup warning. Second, the servlet-only keys are not honoured by a WebFlux application; the reactive keys do nothing in an MVC one. Third, the properties configure only the default resolver and the view resolver Boot creates: a resolver or view resolver you declare yourself ignores them.

# application.properties -- Bookshelf production defaults, written out explicitly
spring.thymeleaf.cache=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html

# application-local.properties -- loaded with --spring.profiles.active=local
spring.thymeleaf.cache=false

A test that sets several of them shows how they map onto the beans (output from the scratch application):

@SpringBootTest(properties = {
        "spring.thymeleaf.cache=true",
        "spring.thymeleaf.template-resolver-order=1",
        "spring.thymeleaf.view-names=books/*,fragments/*",
        "spring.thymeleaf.excluded-view-names=books/icons",
        "spring.thymeleaf.servlet.content-type=application/xhtml+xml",
        "spring.thymeleaf.servlet.produce-partial-output-while-processing=false"})
class PropertiesEffectTest { /* reads thymeleafViewResolver and defaultTemplateResolver */ }
application/xhtml+xml;charset=UTF-8 cache=true partial=false viewNames=[books/*, fragments/*] excluded=[books/icons]
| resolver order=1 cacheable=true mode=HTML checkExistence=true enc=UTF-8

For a WebFlux application the same mapping happens on ThymeleafReactiveViewResolver: with spring.thymeleaf.reactive.max-chunk-size=8KB and spring.thymeleaf.reactive.full-mode-view-names=hello the scratch reactive application reported a response maximum chunk size of 8192 bytes and a full-mode view list of hello.

Extra template resolvers: an SVG icon resolver

The default resolver reads HTML from classpath:/templates/. Anything else, such as inline SVG icons, plain-text mail bodies or XML, needs a second resolver with its own prefix, suffix and template mode. Declare it as a bean: the engine collects every ITemplateResolver bean in the context. Three settings make a second resolver safe:

  • resolvablePatterns restricts it to the template names it owns (icons/*), so it never swallows view names.

  • checkExistence makes it decline a missing resource, so the chain moves on instead of failing.

  • order fixes who is asked first. Resolvers are consulted in ascending order, and in the scratch application a resolver with no order was asked after one with order 2. Because spring.thymeleaf.template-resolver-order is unset by default, set it (or the order of yours) explicitly as soon as you add a second resolver.

@Configuration(proxyBeanMethods = false)
class ThymeleafExtrasConfig {

    @Bean
    SpringResourceTemplateResolver iconTemplateResolver(ApplicationContext context) {
        var resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(context);
        resolver.setPrefix("classpath:/");
        resolver.setSuffix(".svg");
        resolver.setResolvablePatterns(Set.of("icons/*"));
        resolver.setTemplateMode(TemplateMode.XML);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setCheckExistence(true);
        resolver.setOrder(2);
        return resolver;
    }
}

The SVG file is src/main/resources/icons/book.svg, outside templates/ and static/. Note the prefix: the template name is icons/book, so the prefix is classpath:/, not classpath:/icons/. With the longer prefix Thymeleaf looks for icons/icons/book.svg and fails with Error resolving template [icons/book], template might not exist or might not be accessible by any of the configured Template Resolvers.

<p><span th:replace="~{icons/book}"></span> <span th:text="${#isbn.format('9780134685991')}">isbn</span></p>

Rendered by the scratch application (the second part comes from the dialect of the next section):

<p><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
  <path d="M4 4h12a4 4 0 0 1 4 4v12H8a4 4 0 0 1-4-4z" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
 <span>978-0134685991</span></p>

Extra dialect beans

A dialect is registered the same way: declare a bean. Every IDialect bean is added to the engine in @Order order, so a dialect in your own configuration, in a library or from a starter needs no wiring. The following minimal dialect adds a #isbn expression object, which is the lightest kind of extension (the full extension mechanisms, processors and dialects, are the subject of the Extending Thymeleaf page):

@Bean
BookshelfDialect bookshelfDialect() {
    return new BookshelfDialect();
}

static class BookshelfDialect extends AbstractDialect implements IExpressionObjectDialect {

    BookshelfDialect() {
        super("Bookshelf");
    }

    @Override
    public IExpressionObjectFactory getExpressionObjectFactory() {
        return new IExpressionObjectFactory() {
            @Override public Set<String> getAllExpressionObjectNames() { return Set.of("isbn"); }
            @Override public Object buildObject(IExpressionContext context, String name) { return new Isbn(); }
            @Override public boolean isCacheable(String name) { return true; }
        };
    }
}

public static class Isbn {
    public String format(String digits) {
        return digits.length() == 13 ? digits.substring(0, 3) + "-" + digits.substring(3) : digits;
    }
}

The three dialects Boot registers by itself follow the same rule, with a twist: each one is @ConditionalOnMissingBean, so declaring your own LayoutDialect bean (for example one built with non-default options) replaces the default one rather than adding a second.

Overriding the view resolver

The view resolver backs off on a bean name, not on a bean type. That makes replacing it simple and also explains the first pitfall of this page: a resolver declared under a different name does not replace the auto-configured one, it joins it.

For Spring MVC declare a bean named thymeleafViewResolver. The test below replaces the default one with a resolver that only handles books/* and runs first; afterwards the context holds exactly one ThymeleafViewResolver, with order 1.

@SpringBootTest
@Import(ViewResolverOverrideTest.CustomViewResolver.class)
class ViewResolverOverrideTest {

    @TestConfiguration(proxyBeanMethods = false)
    static class CustomViewResolver {
        @Bean
        ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine engine) {
            var resolver = new ThymeleafViewResolver();
            resolver.setTemplateEngine(engine);
            resolver.setCharacterEncoding("UTF-8");
            resolver.setContentType("text/html;charset=UTF-8");
            resolver.setViewNames(new String[] {"books/*"});
            resolver.setOrder(1);
            return resolver;
        }
    }

    @Autowired ApplicationContext ctx;

    @Test
    void customResolverWins() {
        var resolvers = ctx.getBeansOfType(ThymeleafViewResolver.class);
        assertThat(resolvers).hasSize(1);
        assertThat(resolvers.get("thymeleafViewResolver").getOrder()).isEqualTo(1);
    }
}

For WebFlux the auto-configured bean is named thymeleafViewResolver but its back-off condition is on the name thymeleafReactiveViewResolver. In the scratch reactive application the default bean was listed as [thymeleafViewResolver], and declaring a ThymeleafReactiveViewResolver bean named thymeleafReactiveViewResolver left a single resolver, yours:

@Bean
ThymeleafReactiveViewResolver thymeleafReactiveViewResolver(ISpringWebFluxTemplateEngine engine) {
    var resolver = new ThymeleafReactiveViewResolver();
    resolver.setTemplateEngine(engine);
    resolver.setOrder(1);
    return resolver;
}

Most of the reasons to override the resolver have a property: view-names and excluded-view-names to share the application with another view technology, servlet.content-type for XHTML, produce-partial-output-while-processing to buffer, the reactive.* keys for chunking. Declare the bean only when a property does not exist. A resolver you declare no longer reads those properties; you set everything on it yourself.

Versioned static resources and the ResourceUrlEncodingFilter

@{/css/site.css} becomes a content-hashed URL only when two things hold: a Spring resource chain with a version strategy is enabled, and ResourceUrlEncodingFilter wraps the response so that encodeURL can rewrite links. Boot registers that filter for you when a chain is enabled; without one, no filter bean exists, which is exactly what the scratch application showed with default settings. With the chain enabled:

spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
<link rel="stylesheet" th:href="@{/css/site.css}">
<link rel="stylesheet" href="/css/site-3c7253c0b17971959285c18f6eea6509.css">

The filter bean resourceUrlEncodingFilter appeared in the context only after the properties were added. DevTools sets spring.web.resources.chain.cache=false (see below) but does not enable the chain. The link syntax itself is covered on the Link URLs page.

Testing: spring-boot-starter-thymeleaf-test

spring-boot-starter-thymeleaf-test brings the Thymeleaf starter and spring-boot-starter-test, and the spring-boot-thymeleaf module contributes to the test slices. Its META-INF/spring descriptors list ThymeleafAutoConfiguration for the @AutoConfigureWebMvc and @AutoConfigureWebFlux imports, and list org.thymeleaf.dialect.IDialect in the include filters of @WebMvcTest and @WebFluxTest. In practice:

  • a @WebMvcTest renders real templates through the real engine; MockMvc returns the HTML.

  • an IDialect bean that is itself a scanned component is included in the slice. A dialect or a resolver declared in a @Configuration class is not: slices exclude @Configuration classes you do not name, so the icon resolver above needs an explicit @Import.

  • in Boot 4 @WebMvcTest lives in org.springframework.boot.webmvc.test.autoconfigure and @AutoConfigureMockMvc in the same package; the old org.springframework.boot.test.autoconfigure.web.servlet imports from Boot 3 no longer compile.

@WebMvcTest(IconController.class)
@Import(ThymeleafExtrasConfig.class)
@WithMockUser
class IconWebMvcTest {

    @Autowired MockMvc mvc;

    @Test
    void rendersIconAndFormatsIsbn() throws Exception {
        mvc.perform(get("/icons-demo"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("<svg")))
            .andExpect(content().string(containsString("978-0134685991")));
    }
}

Removing the @Import line makes the same test fail at render time with Error resolving template [icons/book], because the slice then has no icon resolver. The test starter does not add Spring Security’s test support; the example uses spring-boot-starter-security-test because the Bookshelf application has Spring Security.

DevTools defaults

spring-boot-devtools applies a list of development-time property defaults. For Thymeleaf the entry that matters is spring.thymeleaf.cache=false: since Boot 4 each module contributes its own default through a META-INF/spring-devtools.properties file (spring-boot-thymeleaf ships defaults.spring.thymeleaf.cache=false). The same list also switches off the caches of the other template engines and the static resource chain, sets spring.web.resources.cache.period=0, enables the H2 console and makes error responses include the message and stack trace. A property you set yourself always wins.

The Bookshelf application with DevTools on the classpath and no spring.thymeleaf.cache anywhere, then with the defaults disabled (values printed by a throwaway runner from the scratch application):

./mvnw spring-boot:run
# cache=false viewResolverCache=false h2=true chain=false

./mvnw spring-boot:run -Dspring-boot.run.arguments="--spring.devtools.add-properties=false"
# cache=null viewResolverCache=true h2=null chain=null

Three consequences follow. DevTools is not on the classpath of a packaged jar or in tests, so production keeps cache=true and a @SpringBootTest ignores the DevTools default; if you do not use DevTools, set the property in a local profile as in the properties example above. Only changes that DevTools can see reach the browser, so a template edit in src/main/resources/templates needs a rebuild of the classpath resources (next section). And LiveReload is deprecated as of Spring Boot 4.1.0 with no replacement: the embedded LiveReload server still exists and is controlled by spring.devtools.livereload.enabled, but do not build a workflow on it; changes under /templates, /static and /public do not trigger a restart by default (they trigger a live reload), so a template edit reaches the next request through the disabled cache, not through a restart.

IDE classpath pitfalls

A template that is found by ./mvnw spring-boot:run or a packaged jar but not when the application starts from the IDE is almost always a classpath ordering or staleness problem, not a Thymeleaf one. The Boot reference states that running from the IDE’s main method orders the classpath differently from Maven, Gradle or a packaged jar, which can make Boot fail to find the expected template, and recommends reordering the classpath so that the module’s own classes and resources come first. Related traps:

  • target/classes (or build/resources/main) is what the application reads, not src/main/resources. If the IDE does not copy changed resources there, an edited template looks unchanged even with cache=false. Build the project (IntelliJ IDEA: Build Project; Eclipse and Spring Tools: save) so that the classpath changes and DevTools reacts.

  • The DevTools restart works by watching classpath directories and needs the build tool’s fork enabled: a Maven or Gradle run that disables forking has no restart class loader.

  • A default prefix of classpath:/templates/ finds templates in every jar on the classpath. Two modules that both contain templates/index.html shadow each other according to classpath order.

  • On a case-insensitive file system (macOS, Windows) books/List renders locally and fails in a Linux container. View names are paths: match the case.

  • The startup warning Cannot find template location: classpath:/templates/ means the directory is missing from the classpath, not that a template is wrong; silence it with spring.thymeleaf.check-template-location=false only for a deliberately template-less module.

Worked example: Bookshelf configuration end to end

The pieces above compose into the configuration of the Bookshelf application: one profile switches the cache, one configuration class adds the icon resolver and the ISBN dialect, and one test proves the result.

# src/main/resources/application.properties
spring.application.name=bookshelf
spring.thymeleaf.template-resolver-order=1

# src/main/resources/application-local.properties
spring.thymeleaf.cache=false
<!-- templates/books/list.html (excerpt) -->
<li th:each="b : ${books}">
  <span th:replace="~{icons/book}"></span>
  <span th:text="${b.title}">Title</span>
  <small th:text="${#isbn.format(b.isbn)}">978-0-13468599-1</small>
</li>

With template-resolver-order=1 the default resolver is asked first for every name; for icons/book it finds no templates/icons/book.html (check-template=true makes it decline), so the icon resolver with resolvablePatterns takes over. The cost of the extra resolver is one failed resource lookup per uncached template, which the template cache removes in production. The profile is activated with --spring.profiles.active=local. A @SpringBootTest that saved a book with ISBN 9780134685991 and requested /books rendered the SVG followed by 978-0134685991 for each row, on Spring Boot 4.1.1.

Pitfalls

Symptom Cause Fix

A second ThymeleafViewResolver appears, templates render twice or in the wrong order.

The bean has a name other than thymeleafViewResolver (MVC) or thymeleafReactiveViewResolver (WebFlux), so the auto-configured one did not back off.

Name the bean exactly as the condition expects, or use the properties.

Error resolving template [icons/book].

The extra resolver’s prefix repeats the directory that is already part of the template name.

Prefix classpath:/, pattern icons/*, suffix .svg.

The extra resolver never gets used, or it hides regular views.

No order, no resolvablePatterns and no checkExistence; the default resolver has no order unless template-resolver-order is set.

Set an order on both resolvers and restrict the new one with resolvablePatterns.

spring.thymeleaf.enabled=false is reported as a removed property and does not disable Thymeleaf.

The property is removed (error-level deprecation since 4.0.0).

Depend on thymeleaf directly instead of spring-boot-thymeleaf, or exclude the auto-configuration.

sec: attributes or layout:decorate are ignored.

The dialect jar is not on the classpath; Boot registers each dialect only when its jar is present.

Add the dependency; check engine.getDialects() as in the first test.

Static resources are not versioned.

ResourceUrlEncodingFilter is registered only when a resource chain is enabled.

Set spring.web.resources.chain.strategy.content.enabled=true and paths.

Template edits are not visible.

Caching is on, DevTools is absent, or the IDE did not update the classpath.

Use a local profile with spring.thymeleaf.cache=false, build the project, check the classpath order.

@WebMvcTest fails with Error resolving template for an extra resolver or dialect.

The slice does not scan @Configuration classes.

@Import the configuration class in the test.

spring.thymeleaf.servlet.* ignored in a WebFlux application, or reactive.* in MVC.

Each group configures only its own view resolver.

Use the keys of the stack in use (see the properties table).

Boot 3 imports such as org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest do not compile.

Boot 4 moved the test annotations next to their modules.

Use org.springframework.boot.webmvc.test.autoconfigure.

Your own resolver ignores cache, view-names and the other properties.

Properties configure only the beans Boot creates.

Set the value on your bean, or drop the bean.