Performance & Production
|
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) and Java 21, checked on 2026-10-08, and every
measurement and output below comes from a scratch Spring Boot application run on that stack (Maven wrapper, JDK 26 with --release 21, an embedded
Tomcat, H2, and curl against a short-lived server). It applies to Thymeleaf with Spring MVC; the template cache, the SpringEL compiler and the
packaging notes are the same with Spring WebFlux and for standalone use (where the settings are Java calls and not properties), while the
partial-output property, the HTTP-level sections and Open Session In View are servlet-only topics. The web server examples use Tomcat, the Boot
default.
A server-rendered page is fast when four things hold: the template is parsed once and not on every request, the data the page needs is loaded in a small, known number of queries, the browser and any proxy in front of the application do not ask for what they already have, and the application starts quickly in the environment it is deployed to. Thymeleaf only owns the first point, but the other three are decided by how the template and its controller are written, so they belong on the same page.
In a hurry: the production checklist
| Concern | Do this | Section |
|---|---|---|
Parsed templates are re-read on every request |
Leave |
|
Expressions are slow on hot pages |
Try |
|
An error in the middle of a page leaves a truncated page with status 200 |
Set |
|
A collection is loaded but not always shown |
Hand the template a |
|
One query per row while the page renders |
|
|
Browsers re-download unchanged pages |
|
|
Large HTML travels uncompressed |
|
|
Shipping the application |
Executable jar, layered extraction for container images, Cloud Native Buildpacks, and native image only after a test of every template. |
The template cache
Thymeleaf parses a template file into a sequence of events and applies its processors to that sequence. By default it keeps the parsed sequence in
a cache, because cloning an in-memory sequence is much cheaper than reading and parsing a file on every request. The mechanics (the resolver’s
setCacheable, the cache patterns, the TTL, StandardCacheManager and clearing entries from code) are covered, with a runnable standalone example,
on Standalone Engine & Configuration. Spring Boot gives you the same switch as a
property, and it is the first thing to check on a production system.
| Property | Default | Effect |
|---|---|---|
|
|
Makes the default template resolver cacheable, and lets the servlet view resolver cache its views. |
|
|
|
|
|
The cache can be observed with the logger named in the tutorial’s logging section. Start the application with this property:
logging.level.org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE=TRACE
Source: Using Thymeleaf — Logging
Two requests to the same page, with the default spring.thymeleaf.cache=true, logged:
[THYMELEAF][CACHE_INITIALIZE] Initializing cache TEMPLATE_CACHE. Max size: 200. Soft references are used.
[THYMELEAF][http-nio-18740-exec-2][TEMPLATE_CACHE][CACHE_MISS] Cache miss in cache "TEMPLATE_CACHE" for key "authors/list".
[THYMELEAF][http-nio-18740-exec-2][TEMPLATE_CACHE][CACHE_ADD][1] Adding cache entry in cache "TEMPLATE_CACHE" for key "authors/list". New size is 1.
[THYMELEAF][http-nio-18740-exec-4][TEMPLATE_CACHE][CACHE_HIT] Cache hit in cache "TEMPLATE_CACHE" for key "authors/list".
and with --spring.thymeleaf.cache=false, two misses and no entry is ever added:
[THYMELEAF][CACHE_INITIALIZE] Initializing cache TEMPLATE_CACHE. Max size: 200. Soft references are used.
[THYMELEAF][http-nio-18740-exec-2][TEMPLATE_CACHE][CACHE_MISS] Cache miss in cache "TEMPLATE_CACHE" for key "authors/list".
[THYMELEAF][http-nio-18740-exec-4][TEMPLATE_CACHE][CACHE_MISS] Cache miss in cache "TEMPLATE_CACHE" for key "authors/list".
Source: Using Thymeleaf — Template Cache
The size of the difference depends on how much markup the template holds, because parsing cost grows with the markup and not with the data. A template
with 150 small sections (about 600 elements), a handful of expressions and one row of data was rendered 2000 times after a warm-up through the
SpringTemplateEngine that Boot auto-configures:
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ThymeleafAutoConfiguration.class))
.withPropertyValues("spring.thymeleaf.cache=false") // or true
.run(ctx -> {
TemplateEngine engine = ctx.getBean(TemplateEngine.class);
Context c = new Context(Locale.ENGLISH);
c.setVariable("title", "Open");
for (int i = 0; i < 300; i++) { engine.process("perf/heavy", c); } // warm-up
long t = System.nanoTime();
for (int i = 0; i < 2000; i++) { engine.process("perf/heavy", c); }
System.out.println((System.nanoTime() - t) / 1_000_000 + " ms");
});
The program printed, in two rounds, 920 ms and 910 ms with the cache off and 232 ms and 239 ms with it on: roughly four times faster for this
template. These numbers are one run on one laptop and will differ on yours, so treat them as an order of magnitude and measure your own pages. A page
dominated by iteration over a long list spends most of its time evaluating expressions and not parsing, so the cache helps it much less: the cache removes the file read and the parsing, not the processing.
Practical rules:
-
Keep
spring.thymeleaf.cache=truein production. DevTools switches it off for you in development (see Spring Boot Auto-configuration & Properties); aspring.thymeleaf.cache=falsethat reaches a production profile is one of the commonest self-inflicted slowdowns. -
The template cache holds 200 entries and the expression cache 500 by default (
StandardCacheManager), and the log above shows that entries are held through soft references, so the JVM may drop them under memory pressure. An application with more than a few hundred distinct templates, or one that builds template names dynamically, can resize the cache with a customStandardCacheManagerset on the engine, as described in Standalone Engine & Configuration. -
Template content that changes while the application runs (templates read from a database or a file system the application does not own) needs a TTL on the resolver or a call to
clearTemplateCache(); a cached template never notices that its source changed. -
Decoupled logic (
.th.xmlfiles) adds a second resource to read and merge for each template, and the cache hides that cost; see Natural Templates & Decoupled Logic.
The expression cache and the SpringEL compiler
Besides parsed templates, the cache manager holds a second cache for parsed expressions, so a ${...} or *{...} expression string is parsed once and
reused. With the Spring dialect the variable expressions are SpEL, and SpEL can additionally compile an expression that is evaluated often into
bytecode. Thymeleaf exposes the switch as SpringTemplateEngine.setEnableSpringELCompiler(boolean) and Spring Boot as a property:
spring.thymeleaf.enable-spring-el-compiler=true
The tutorial keeps it at false by default for safer backwards compatibility, because compilation can be incompatible with templates that reuse the
same expression with objects of different types. On a page with 200 table rows, each with four expressions, the same harness as above gave:
| Configuration (cache on, 2000 renders of 200 rows) | Time |
|---|---|
Compiler off, round 1 and 2 |
1909 ms, 1699 ms |
Compiler on, round 1 and 2 |
700 ms, 688 ms |
The compiler removes most of the cost of the expressions themselves, which is what dominates a page like this one. As a check of the warning in the tutorial, a
single template line, a th:text that reads item.label, was rendered five times with three instances of one class and then two of another class that
also has getLabel(). With the compiler on and with it off the output was the same (book three times, then magazine twice) and no exception was raised. That is
one simple case on 3.1.5 and Boot 4.1.1; it does not prove the compiler is safe for every expression, so the rule is the usual one for an optimisation:
enable it, run the test-suite and the key pages in a pre-production environment, and keep the property out of the way if a page fails.
Partial output while processing
On the servlet stack, ThymeleafViewResolver writes the rendered markup to the response as processing advances instead of collecting the whole page in
memory first. Boot exposes it as spring.thymeleaf.servlet.produce-partial-output-while-processing, true by default (it appears in the property table of
Spring Boot Auto-configuration & Properties). That gives a faster first byte and a smaller memory
footprint for large pages, with a cost that shows up only when a template fails half way. A page of 3000 paragraphs followed by one element whose
expression dereferences a missing variable (${missing.value}) was requested against both settings:
<p th:each="n : ${lines}" th:text="'Line ' + ${n} + ' of a large page'">Line</p>
<p th:text="${missing.value}">boom</p>
# default: produce-partial-output-while-processing=true
curl -s -o /dev/null -w "status=%{http_code} size=%{size_download} type=%{content_type}\n" localhost:18740/big
# status=200 size=97923 type=text/html;charset=UTF-8
# --spring.thymeleaf.servlet.produce-partial-output-while-processing=false
curl -s -o /dev/null -w "status=%{http_code} size=%{size_download} type=%{content_type}\n" localhost:18740/big
# status=500 size=0 type=
With the default the client received status 200 and 97,923 bytes of a page that ends after "Line 3000 of a large page", with no closing tags and no sign of the error; the failure
is only in the server log. With false Thymeleaf renders the whole page into a buffer first, so the exception happens before anything is committed and the response is the 500
status that the error handling of the application then turns into an error page. Choose false when a half-rendered page is worse than an error
page (forms, checkouts, anything a user acts on) and your pages are not so large that buffering them hurts; keep true for large reports where time to first byte matters.
The errors themselves are the topic of Error Handling & Flash Messages. The property does not exist for WebFlux, where
the reactive view resolver has its own chunked and full modes (see Reactive WebFlux).
Lazy context variables
A controller that always loads a collection the template only sometimes shows pays for the query on every request. Thymeleaf’s answer is a context
variable that implements ILazyContextVariable, usually by extending LazyContextVariable; its loadValue() runs when an expression first reads the
variable and not before, and the result is kept for the rest of that rendering. In Spring MVC it is simply a model attribute. The iteration page,
Iteration & Conditionals, explains the mechanism with a test; this is the same idea on the authors and books of the
scratch application, with a flag that decides whether the template reads the list:
@GetMapping("/authors/lazy")
public String lazy(Model model, @RequestParam(defaultValue = "false") boolean show) {
model.addAttribute("show", show);
model.addAttribute("authors", new LazyContextVariable<List<Author>>() {
@Override protected List<Author> loadValue() { return repo.findAllWithBooks(); }
});
return "authors/lazy";
}
<ul th:if="${show}"><li th:each="a : ${authors}" th:text="${a.name} + ': ' + ${a.books.size()}">Author</li></ul>
<p th:unless="${show}">Nothing to show.</p>
Counting the SQL statements Hibernate prepared during each request gave 0 for /authors/lazy and 1 for /authors/lazy?show=true. Note what this does and does
not do: it postpones the call, it does not change what the call costs, and it must still run while the data can be loaded. A lazy variable that touches the
database is evaluated inside the view, which brings the next topic.
Avoiding N+1 and Open Session In View in views
The scratch model has an Author with a lazy @OneToMany list of Book entities (each book points back with a lazy @ManyToOne). The seed data is 20 authors with three
books each. The natural way to show them in a template:
<div th:each="a : ${authors}">
<h2 th:text="${a.name}">Author</h2>
<ul><li th:each="b : ${a.books}" th:text="${b.title}">Title</li></ul>
</div>
@GetMapping("/authors")
public String list(Model model) {
model.addAttribute("authors", repo.findAll());
return "authors/list";
}
Source: Spring Data JPA — Query methods
repo.findAll() returns 20 authors whose books collection is not loaded yet. Whether the view can read it depends on one Boot property.
Open Session In View on (the default)
Spring Boot registers an interceptor that keeps the JPA EntityManager open for the whole web request, including the rendering of the view, when
spring.jpa.open-in-view is true, which is the default. Boot warns about it at start up. The line from the log of this run:
WARN ... JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
The template works, because each a.books is initialised on demand, and that is exactly the problem. With logging.level.org.hibernate.SQL=DEBUG, one request to /authors logged
21 statements: one for the authors and one per author, repeated:
select a1_0.id,a1_0.name from author a1_0
select b1_0.author_id,b1_0.id,b1_0.title from book b1_0 where b1_0.author_id=?
select b1_0.author_id,b1_0.id,b1_0.title from book b1_0 where b1_0.author_id=?
...
That is the N+1 problem: 1 query for the list plus N for the collection of each element. With 20 rows it hides in a fast local database; with 500 rows and a database on another machine it is the slow page. Open Session In View does not cause it, it only lets it happen silently in the view (and can keep a database connection busy for the length of the rendering, including slow clients).
Open Session In View off
spring.jpa.open-in-view=false
Now the same /authors request ends with status 500. The exception chain, printed by a MockMvc test, is ServletException then TemplateProcessingException then
LazyInitializationException, whose message is:
org.hibernate.LazyInitializationException: Cannot lazily initialize collection of role 'com.example.perf.Author.books' with key '1' (no session)
This is a good failure: the view can no longer trigger queries, so the controller (or a service method) has to say what the page needs. The repository method that does it:
public interface AuthorRepository extends JpaRepository<Author, Long> {
@Query("select distinct a from Author a join fetch a.books order by a.name")
List<Author> findAllWithBooks();
}
@GetMapping("/authors/fetch")
public String listFetched(Model model) {
model.addAttribute("authors", repo.findAllWithBooks());
return "authors/list";
}
Source: Spring Data JPA — Using @Query
The same template, /authors/fetch, returned status 200 with one statement, with open-in-view on or off:
select distinct a1_0.id,b1_0.author_id,b1_0.id,b1_0.title,a1_0.name from author a1_0 join book b1_0 on a1_0.id=b1_0.author_id order by a1_0.name
The same figures were checked with a @SpringBootTest and Hibernate statistics (hibernate.generate_statistics=true, getPrepareStatementCount()):
| Request | open-in-view=true | open-in-view=false |
|---|---|---|
|
21 statements |
|
|
1 statement |
1 statement |
|
0 statements |
0 statements |
|
1 statement |
1 statement |
Guidelines that follow from this:
-
Turn Open Session In View off explicitly, so that the warning disappears and the intent is in the configuration, and fix each page that then fails. The failures are a to-do list of the N+1 queries you had.
-
Load what the page needs where the transaction is:
JOIN FETCHin a query, an entity graph, or a projection that returns just the fields the template shows. A DTO or projection also stops the template from navigating an entity graph, which is a good habit for security as well (an entity exposed to a template exposes every property to an expression). -
A
JOIN FETCHof a collection combined with pagination makes Hibernate fetch everything and page in memory (and log a warning); page the owning entities first and load their collections in a second query. The details, and the alternatives (batch fetching,@BatchSize), are on Fetching and the N+1 Problem, and Hibernate & Spring Boot Integration covers the property itself. -
Do not combine the lazy context variable with a lazily initialised association unless the variable’s
loadValue()itself runs a query that fetches what the view will read. -
The pagination and sorting patterns that sit on top of this are on Displaying Data, Pagination & Sorting.
HTTP caching of rendered pages
The template cache saves the server from re-parsing. HTTP caching saves the network from carrying a response, or the server from rendering one, at all. Two kinds of response need different treatment.
Pages that are not cacheable by default
Spring Security, when present (and it normally is), adds Cache-Control: no-cache, no-store, max-age=0, must-revalidate, Pragma: no-cache and Expires: 0 to responses that do not already carry caching headers, which is the right default for pages
that show personal data. In the scratch application with a permit-all filter chain, the response for /authors carried exactly these headers, and so did the static CSS file. The details of the security headers are on Spring Security Integration.
A cacheable page therefore has to say so itself.
A page that is the same for everyone: Cache-Control and an ETag
@GetMapping("/about")
public String about(Model model, HttpServletResponse response) {
response.setHeader(HttpHeaders.CACHE_CONTROL,
CacheControl.maxAge(Duration.ofMinutes(5)).cachePublic().getHeaderValue());
model.addAttribute("lines", IntStream.rangeClosed(1, 120).boxed().toList());
return "perf/about";
}
@Bean
FilterRegistrationBean<ShallowEtagHeaderFilter> etagFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> bean =
new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
bean.addUrlPatterns("/about");
return bean;
}
ShallowEtagHeaderFilter hashes the rendered body to produce the ETag, so the page is still rendered on every request, but the response shrinks to a status line when the client already has it. With curl:
curl -s -D - -o /dev/null localhost:18740/about
# HTTP/1.1 200
# Cache-Control: max-age=300, public
# ETag: "0a5c6464efd537922c4c6eb7ad4026660"
# Content-Type: text/html;charset=UTF-8
# Content-Length: 4075
curl -s -D - -o /dev/null -H 'If-None-Match: "0a5c6464efd537922c4c6eb7ad4026660"' localhost:18740/about
# HTTP/1.1 304
# Cache-Control: max-age=300, public
# ETag: "0a5c6464efd537922c4c6eb7ad4026660"
The 304 has no body, and the caching headers set by the controller were kept because Spring Security does not overwrite headers that are already present. Use this for public, user-independent
pages only; a page that includes the user name, a CSRF token or a flash message must not be cached by shared caches. For pages that depend on data you can date (an entity with a modification
time), Spring’s WebRequest.checkNotModified(...) avoids rendering altogether, at the price of computing the date in the controller; it is documented on the same Spring Framework page.
Static resources: long lifetimes and content hashes
CSS, JavaScript and images are best cached for a long time and renamed when they change. Boot’s resource chain can add a hash of the content to the file name, and Thymeleaf’s @{...} link expressions use it
automatically, because Boot registers ResourceUrlEncodingFilter when Thymeleaf is present (see
Link URLs):
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
spring.web.resources.cache.cachecontrol.max-age=365d
spring.web.resources.cache.cachecontrol.cache-public=true
The template line <link rel="stylesheet" th:href="@{/css/app.css}"> was rendered, with these properties on, as:
<link rel="stylesheet" href="/css/app-ff974d062358212ab3c71569650f232a.css">
and that URL was served with Cache-Control: max-age=31536000, public and a weak ETag. Without the properties the same URL was /css/app.css and, behind Spring Security, was served with the no-store headers shown above. The properties
that DevTools changes for development (the chain cache, spring.web.resources.cache.period) are listed in the DevTools section of
Spring Boot Auto-configuration & Properties. A CDN in front of the application takes over the long-lived assets; see the CDN pages of the
Cloud section (for example AWS CloudFront, Azure Front Door and CDN and
Google Cloud CDN and Media CDN).
Compression
HTML is highly compressible. Boot’s embedded servers can compress responses, and compression is off by default:
server.compression.enabled=true
The reference lists text/html, text/xml, text/plain, text/css, text/javascript, application/javascript, application/json and application/xml as the default compressed types and 2048 bytes as the default
server.compression.min-response-size; server.compression.mime-types replaces the list and server.compression.additional-mime-types extends it. It is supported by Tomcat, Jetty and Reactor Netty.
With the property on, the /authors page (2010 bytes of HTML, no ETag filter) requested with Accept-Encoding: gzip came back with Content-Encoding: gzip, vary: accept-encoding and 317 bytes on the wire against
2010 plain. (It was compressed although it is below 2048 bytes because the body was streamed without a known length, so the minimum could not be applied; do not rely on the threshold for small pages.)
The interesting interaction is with the ETag filter of the previous section. The /about page of 4075 bytes, with the filter writing a strong ETag, was requested with Accept-Encoding: gzip and compression enabled and came back uncompressed
(Content-Length: 4075, no Content-Encoding). Tomcat does not compress a response that carries a strong ETag. Making the filter write a weak validator fixes it:
ShallowEtagHeaderFilter filter = new ShallowEtagHeaderFilter();
filter.setWriteWeakETag(true);
Source: Spring Framework — Shallow ETag
With that change the same request returned ETag: W/"07a952673b83b41bda6e33a199fa2ebd9", Content-Encoding: gzip, vary: accept-encoding, Transfer-Encoding: chunked and 442 bytes on the wire for 4075 bytes of HTML. When a reverse proxy, an ingress or a CDN already compresses
(see the Kubernetes ingress and the cloud pages linked below), compress in one place only.
Packaging and deployment
Thymeleaf adds no packaging rules of its own: templates are classpath resources under classpath:/templates/, so they ship inside the application jar. The points specific to a template application:
-
Executable jar.
./mvnw packageproduces a single jar (69,037,863 bytes for the scratch application with Spring Security, JPA, Hibernate and H2) that runs withjava -jar. The templates are read from inside the jar, so the cache matters even more: the file is a zip entry. Editing a template means rebuilding the jar, andspring.thymeleaf.cache=falsein production does not make editing possible, only slower. -
Layered extraction for container images.
java -Djarmode=tools -jar app.jar list-layersprinteddependencies,spring-boot-loader,snapshot-dependenciesandapplication;extract --layers --destination exproduced one directory per layer, with 66M independenciesand 20K inapplication. Copying the layers separately lets a container runtime reuse the 66M layer when only a template changes. See Spring Boot — Efficient deployments (unpacking). -
Cloud Native Buildpacks.
./mvnw spring-boot:build-imagebuilds an image without a Dockerfile; see Spring Boot — Cloud Native Buildpacks. This command was not run for this page, because it downloads the builder image; the Docker section describes both this path and a multi-stage Dockerfile for a Spring Boot service in Java and Spring Boot Images, and Docker has the rest (image best practices and security, Compose for local development, CI/CD). -
Kubernetes. A rendered-HTML application is stateless apart from the session, so the points of the Kubernetes section apply as for any web service: probes, rolling updates, autoscaling and an ingress that terminates TLS; start from Kubernetes. Keep the session either in a shared store or on sticky sessions, and remember the compression and cache headers above when the ingress or a CDN sits in front.
-
Cloud services. The container, app-service and CDN options of each provider are in the Cloud section.
-
Spring Boot in production profile. Put
spring.thymeleaf.cache=true(or simply do not set it),spring.jpa.open-in-view=falseand the compression and resource settings above inapplication.propertiesand keep development-only overrides in alocalprofile, as Spring Boot Auto-configuration & Properties describes.
GraalVM native image
Spring Boot can compile an application ahead of time to a native executable that starts in milliseconds; the Boot reference for
GraalVM Native Images describes the build, the closed-world restrictions (no lazy class loading, a classpath and bean definitions fixed at
build time, conditional properties not supported) and the fact that reflection, resources and proxies must be declared to GraalVM, partly through hints that Spring generates. What this page could verify is limited: the introduction page of the Boot
native-image reference (the only page of it that was read in full) does not mention Thymeleaf, the spring-boot-thymeleaf 4.1.1, thymeleaf 3.1.5 and thymeleaf-spring6 3.1.5 jars contain no GraalVM metadata or runtime-hints files, and no native-image toolchain was installed on the machine used,
so no native build of a Thymeleaf application was run for this page. What follows is therefore a consequence of how Thymeleaf works and not a documented guarantee:
-
Expressions such as
${book.title}are evaluated by SpEL through reflection on the model classes, and the templates are resources read at run time. Classes that are used only from templates are invisible to the static analysis that decides what to keep, so a native image may need hints for them. Boot’s advanced-topics page documents how to write them (RuntimeHintsRegistrarwith@ImportRuntimeHints, and tests withRuntimeHintsPredicates); see Spring Boot — Custom Hints. -
A missing hint typically shows up as a run-time error while a page renders and not as a build error, so render every template, with representative model data, in the native executable before shipping it. The SpringEL compiler generates classes at run time and should be treated as untested in a native image.
-
Because the application context is fixed at build time, the Thymeleaf properties that change bean creation cannot be changed at start up in a native executable; set them when you build.
Choose a native image for start-up and memory (scale-to-zero platforms, short-lived jobs), and weigh it against the longer build and the extra verification of reflection-driven templates; for a long-running web application on a normal JVM, the settings in this page matter more.
Pitfalls
| Symptom | Cause and fix |
|---|---|
Every page is slow in production, the log shows only |
|
A template change is not visible |
The template is cached. In development use DevTools or |
Response status is 200 but the page stops in the middle |
Partial output is on, and an expression failed late in the template. Set |
|
|
Dozens of identical |
N+1, hidden by Open Session In View. Count statements in a test with Hibernate statistics and fix the query. |
A cacheable page is still |
Spring Security’s default cache headers. Set |
Compression has no effect on an ETag page |
Tomcat does not compress a response with a strong ETag. Use |
A compressed response is also compressed by the proxy |
Compress in one layer only. |
Native executable renders blank values or fails on |
A model class used only from templates lacks reflection hints. Register hints and test every template in the native build. |
Where the books are outdated
The "Tips and tricks" chapter of Taming Thymeleaf (Wim Deblauwe; the consulted PDF is v1.1.1 from 2021, written for Spring Boot 2.3, Java 11 and Thymeleaf 3.0, and the current
Leanpub edition is described as updated for Spring Boot 3) starts with turning Open Session In View off and loading the data the view needs with JOIN FETCH. That advice is unchanged and is the section above, and the
same default and the same warning exist in Boot 4.1. What is different today: the persistence imports are jakarta.persistence.*; the Boot starters and test annotations moved
(spring-boot-starter-webmvc, spring-boot-starter-thymeleaf-test, org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc); Hibernate ORM 7.4 generates the
select statements shown above; and DevTools in Boot 4 gets the Thymeleaf cache default from a per-module META-INF/spring-devtools.properties file. The other tips of that chapter appear where they belong
in this section: trimming input with StringTrimmerEditor and PropertyEditor versus Formatter in
Conversion & Formatting, a global @ModelAttribute for values such as the application version in
Spring MVC Integration, file upload, linked-entity <select> and dynamic rows in
Forms & Data Binding and Fragment Rendering & Ajax, and the date picker web component in
Alpine.js & Web Components.