Spring Boot
Static Resources
Static resources are files served directly to the browser without server-side processing — CSS, JavaScript, images, fonts, and other assets. Spring Boot auto-configures ResourceHttpRequestHandler to serve static files from well-known classpath locations and supports content versioning, caching headers, WebJars, and custom resource chains for production optimisation.
Default Static Resource Locations
Spring Boot auto-configures four classpath locations as static resource roots. Any file placed in these directories is served at the URL path matching the file's path relative to the root.
Shell
# ── Four default static resource locations (evaluated in order): ───────
# classpath:/META-INF/resources/
# classpath:/resources/
# classpath:/static/ ← most commonly used
# classpath:/public/
# ── File layout → URL mapping: ────────────────────────────────────────
# src/main/resources/static/css/main.css → GET /css/main.css
# src/main/resources/static/js/app.js → GET /js/app.js
# src/main/resources/static/images/logo.png → GET /images/logo.png
# src/main/resources/public/favicon.ico → GET /favicon.ico
# src/main/resources/static/index.html → GET / (welcome file)
# ── Typical static directory layout: ──────────────────────────────────
# src/main/resources/
# └── static/
# ├── css/
# │ ├── main.css
# │ └── vendor/bootstrap.min.css
# ├── js/
# │ ├── app.js
# │ └── vendor/htmx.min.js
# ├── images/
# │ ├── logo.png
# │ └── icons/
# └── fonts/
# └── inter.woff2
# ── Referencing in Thymeleaf (always use @{} for context-path safety): ─
# <link rel="stylesheet" th:href="@{/css/main.css}" />
# <script th:src="@{/js/app.js}"></script>
# <img th:src="@{/images/logo.png}" />Configuring Static Resources
Spring Boot exposes static resource configuration through application.yml properties and through WebMvcConfigurer.addResourceHandlers() for advanced scenarios — custom locations, additional URL patterns, and cache control.
yaml
# ── application.yml — common static resource settings: ───────────────
spring:
mvc:
static-path-pattern: /static/** # serve static files under /static/** only
# default: /**
web:
resources:
static-locations:
- classpath:/static/ # default locations
- classpath:/public/
- file:/opt/myapp/uploads/ # serve files from the filesystem
add-mappings: true # set false to disable all static serving
# (pure REST API — no static files)
cache:
period: 3600 # Cache-Control: max-age=3600 (seconds)
cachecontrol:
max-age: 365d # override with a Duration
cache-public: true # Cache-Control: public
no-cache: false
# ── WebMvcConfigurer — custom resource handlers: ──────────────────────
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Custom URL pattern → custom classpath location:
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/assets/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic());
// Serve uploaded files from the filesystem:
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:/opt/myapp/uploads/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS));
// Disable caching for a specific path (dev tools etc.):
registry.addResourceHandler("/dev/**")
.addResourceLocations("classpath:/dev/")
.setCacheControl(CacheControl.noStore());
}
}WebJars — Dependency-Managed Client Libraries
WebJars packages client-side libraries (Bootstrap, jQuery, HTMX, Alpine.js) as Maven/Gradle dependencies. Spring Boot auto-configures a resource handler for /webjars/** that serves the JAR contents. The webjars-locator-core dependency removes version numbers from the URL path.
XML
<!-- pom.xml — add Bootstrap and HTMX as WebJars: -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.2</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>htmx.org</artifactId>
<version>1.9.10</version>
</dependency>
<!-- Remove version from URL path (version-agnostic references): -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<!-- ── Without webjars-locator — version in URL: ─────────────────────── -->
<link rel="stylesheet"
href="/webjars/bootstrap/5.3.2/css/bootstrap.min.css" />
<script src="/webjars/htmx.org/1.9.10/dist/htmx.min.js"></script>
<!-- ── With webjars-locator — version-free URL: ─────────────────────── -->
<link rel="stylesheet"
th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" />
<script th:src="@{/webjars/htmx.org/dist/htmx.min.js}"></script>
<!-- Update the JAR version in pom.xml → URL stays the same.
webjars-locator resolves the actual path at runtime. -->Content Versioning for Cache Busting
Spring's ResourceChainRegistration and VersionResourceResolver append a content hash to resource URLs automatically. This enables permanent caching headers (max-age: 1 year) — the URL changes whenever the file content changes, forcing browsers to fetch the new version.
Java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)
.cachePublic().immutable())
.resourceChain(true) // enable resource chain
.addResolver(new VersionResourceResolver()
.addContentVersionStrategy("/**")); // hash appended to URL
}
}
<!-- ── Thymeleaf — use @{} to resolve versioned URLs: ────────────────── -->
<!-- Without versioning: /css/main.css -->
<!-- With versioning: /css/main-d41d8cd98f00b204e9800998ecf8427e.css -->
<link rel="stylesheet" th:href="@{/static/css/main.css}" />
<script th:src="@{/static/js/app.js}"></script>
<!-- Thymeleaf resolves @{} through the ResourceUrlProvider,
which applies the VersionResourceResolver and returns the hashed URL. -->
# ── application.yml — enable versioning via properties (simpler): ─────
spring:
web:
resources:
chain:
enabled: true
strategy:
content:
enabled: true
paths: /** # apply to all resources
# Or use a fixed version string instead of content hash:
# chain:
# strategy:
# fixed:
# enabled: true
# version: v2.1.0
# paths: /js/**,/css/**Caching Headers and ETags
Correct HTTP caching headers are essential for production performance. Spring Boot's ResourceHttpRequestHandler sets Last-Modified automatically. Add CacheControl and ETag support for fine-grained cache control.
Java
# ── Cache-Control strategies: ────────────────────────────────────────
# Immutable assets (versioned URLs — change URL when content changes):
CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic().immutable()
# Cache-Control: max-age=31536000, public, immutable
# Browser caches forever — safe because the URL contains a content hash.
# Short-lived assets (change frequently, CDN cacheable):
CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()
# Cache-Control: max-age=3600, public
# Private assets (user-specific — no shared cache):
CacheControl.maxAge(10, TimeUnit.MINUTES).cachePrivate()
# Cache-Control: max-age=600, private
# No caching (admin pages, sensitive data):
CacheControl.noStore()
# Cache-Control: no-store
# ── ETag support — enable in application.yml: ────────────────────────
server:
servlet:
session:
tracking-modes: cookie
spring:
web:
resources:
cache:
use-last-modified: true # Last-Modified header (default: true)
# ── ShallowEtagHeaderFilter — add ETag to all responses: ─────────────
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> etagFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> bean =
new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
bean.addUrlPatterns("/static/*");
return bean;
}
}
# ShallowEtagHeaderFilter computes an MD5 hash of the response body
# and sets ETag. Subsequent requests with If-None-Match return 304
# without re-sending the body — saves bandwidth, not server computation.Disabling Static Resources for REST APIs
A pure REST API does not serve static files. Disabling the auto-configured static resource handler removes an unnecessary component and prevents accidental file serving.
yaml
# ── Disable static resource handling entirely: ────────────────────────
spring:
web:
resources:
add-mappings: false # disables ResourceHttpRequestHandler
# ── Or configure via WebMvcConfigurer: ────────────────────────────────
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
// Do NOT call configurer.enable() — default servlet not needed for REST APIs
}
}
# ── Effect on 404 handling: ───────────────────────────────────────────
# With add-mappings: false, requests to unmapped URLs are not intercepted
# by the static resource handler. Spring MVC returns 404 through the
# normal exception resolver chain — controlled by @RestControllerAdvice.
# Ensure NoHandlerFoundException is thrown (not swallowed):
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<ErrorResponse> handleNoHandler(
NoHandlerFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(404, "Not Found",
ex.getHttpMethod() + " " + ex.getRequestURL() + " not found"));
}
}