Spring WebFlux provides comprehensive support for serving static resources (CSS, JavaScript, images, etc.) with features including caching, content negotiation, resource transformation, versioning strategies, and WebJars support. The resource handling chain allows for flexible processing of resources before they're served to clients.
The ResourceWebHandler class handles requests for static resources.
public class ResourceWebHandler implements WebHandler, InitializingBean {
// Set resource locations (file system paths or classpath locations)
public void setLocations(List<Resource> locations) { ... }
// Get resource locations
public List<Resource> getLocations() { ... }
// Set resource resolvers
public void setResourceResolvers(List<ResourceResolver> resourceResolvers) { ... }
// Get resource resolvers
public List<ResourceResolver> getResourceResolvers() { ... }
// Set resource transformers
public void setResourceTransformers(List<ResourceTransformer> resourceTransformers) { ... }
// Get resource transformers
public List<ResourceTransformer> getResourceTransformers() { ... }
// Set cache control
public void setCacheControl(CacheControl cacheControl) { ... }
// Get cache control
public CacheControl getCacheControl() { ... }
// Set whether to use last modified
public void setUseLastModified(boolean useLastModified) { ... }
// Check if using last modified
public boolean isUseLastModified() { ... }
// Set whether to optimize locations (check if locations exist at startup)
public void setOptimizeLocations(boolean optimizeLocations) { ... }
// Check if optimizing locations
public boolean isOptimizeLocations() { ... }
// Set supported media types
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) { ... }
// Get supported media types
public List<MediaType> getSupportedMediaTypes() { ... }
@Override
public Mono<Void> handle(ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.CacheControl;
import org.springframework.web.reactive.resource.ResourceWebHandler;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Configuration
public class ResourceConfig {
@Bean
public ResourceWebHandler resourceWebHandler() {
ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocations(List.of(
new ClassPathResource("static/"),
new ClassPathResource("public/")
));
handler.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
handler.setUseLastModified(true);
handler.setOptimizeLocations(true);
return handler;
}
}Resource resolvers locate and resolve resources from configured locations.
public interface ResourceResolver {
// Resolve resource for the given request
Mono<Resource> resolveResource(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations,
ResourceResolverChain chain);
// Resolve URL path for the given resource path
Mono<String> resolveUrlPath(String resourcePath,
List<? extends Resource> locations,
ResourceResolverChain chain);
}public interface ResourceResolverChain {
// Resolve resource using remaining resolvers in the chain
Mono<Resource> resolveResource(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations);
// Resolve URL path using remaining resolvers in the chain
Mono<String> resolveUrlPath(String resourcePath,
List<? extends Resource> locations);
}The PathResourceResolver resolves resources under configured locations.
public class PathResourceResolver extends AbstractResourceResolver {
public PathResourceResolver() { ... }
// Set allowed locations
public void setAllowedLocations(Resource... allowedLocations) { ... }
// Get allowed locations
public Resource[] getAllowedLocations() { ... }
// Set URL path helper
public void setUrlPathHelper(UrlPathHelper urlPathHelper) { ... }
// Get URL path helper
public UrlPathHelper getUrlPathHelper() { ... }
@Override
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
@Override
protected Mono<String> resolveUrlPathInternal(String resourcePath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
// Check if resource is accessible
protected boolean isResourceUnderLocation(Resource resource, Resource location) { ... }
// Get resource
protected Mono<Resource> getResource(String resourcePath, Resource location) { ... }
}The CachingResourceResolver adds caching to resource resolution.
public class CachingResourceResolver extends AbstractResourceResolver {
// Default cache name
public static final String RESOLVED_RESOURCE_CACHE_KEY_PREFIX = "resolvedResource:";
public static final String RESOLVED_URL_PATH_CACHE_KEY_PREFIX = "resolvedUrlPath:";
public CachingResourceResolver(Cache cache) { ... }
public CachingResourceResolver(org.springframework.cache.CacheManager cacheManager, String cacheName) { ... }
// Get cache
public Cache getCache() { ... }
@Override
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
@Override
protected Mono<String> resolveUrlPathInternal(String resourcePath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
}Usage:
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.web.reactive.resource.CachingResourceResolver;
CachingResourceResolver cachingResolver = new CachingResourceResolver(
new ConcurrentMapCache("resourceCache")
);The EncodedResourceResolver resolves pre-encoded variants of resources (gzip, brotli).
public class EncodedResourceResolver extends AbstractResourceResolver {
// Default encodings
public static final String[] DEFAULT_CODINGS = {"br", "gzip"};
public EncodedResourceResolver() { ... }
@Override
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
@Override
protected Mono<String> resolveUrlPathInternal(String resourcePath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
}The VersionResourceResolver resolves resources with version information in the URL.
public class VersionResourceResolver extends AbstractResourceResolver {
public VersionResourceResolver() { ... }
// Add content-based versioning for path patterns
public VersionResourceResolver addContentVersionStrategy(String... pathPatterns) { ... }
// Add fixed version strategy for path patterns
public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) { ... }
// Add custom version strategy for path patterns
public VersionResourceResolver addVersionStrategy(VersionStrategy strategy, String... pathPatterns) { ... }
// Get strategy map
public Map<String, VersionStrategy> getStrategyMap() { ... }
@Override
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
@Override
protected Mono<String> resolveUrlPathInternal(String resourcePath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
}Usage:
import org.springframework.web.reactive.resource.VersionResourceResolver;
VersionResourceResolver versionResolver = new VersionResourceResolver()
.addContentVersionStrategy("/**/*.js", "/**/*.css")
.addFixedVersionStrategy("v1.0.0", "/**/*.png", "/**/*.jpg");The LiteWebJarsResourceResolver resolves WebJars resources without version numbers in the path.
public class LiteWebJarsResourceResolver extends AbstractResourceResolver {
public LiteWebJarsResourceResolver() { ... }
@Override
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
@Override
protected Mono<String> resolveUrlPathInternal(String resourcePath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
}Resource transformers modify resource content before serving.
public interface ResourceTransformer {
// Transform the given resource
Mono<Resource> transform(ServerWebExchange exchange,
Resource resource,
ResourceTransformerChain chain);
}public interface ResourceTransformerChain {
// Transform using remaining transformers in the chain
Mono<Resource> transform(ServerWebExchange exchange, Resource resource);
}The CssLinkResourceTransformer updates links in CSS files to versioned URLs.
public class CssLinkResourceTransformer extends ResourceTransformerSupport {
public CssLinkResourceTransformer() { ... }
@Override
public Mono<Resource> transform(ServerWebExchange exchange,
Resource resource,
ResourceTransformerChain chain) { ... }
}Usage:
import org.springframework.web.reactive.resource.CssLinkResourceTransformer;
CssLinkResourceTransformer cssTransformer = new CssLinkResourceTransformer();
// This transformer will rewrite URLs in CSS files like:
// background-image: url('/images/logo.png')
// to include version:
// background-image: url('/images/logo-abc123.png')The CachingResourceTransformer adds caching to resource transformation.
public class CachingResourceTransformer extends ResourceTransformerSupport {
public CachingResourceTransformer(Cache cache) { ... }
public CachingResourceTransformer(org.springframework.cache.CacheManager cacheManager, String cacheName) { ... }
// Get cache
public Cache getCache() { ... }
@Override
public Mono<Resource> transform(ServerWebExchange exchange,
Resource resource,
ResourceTransformerChain chain) { ... }
}Version strategies determine how version information is extracted and embedded in resource URLs.
public interface VersionStrategy {
// Get the resource version
String getResourceVersion(Resource resource);
// Extract version from request path
String extractVersion(String requestPath);
// Remove version from request path
String removeVersion(String requestPath, String version);
// Add version to request path
String addVersion(String requestPath, String version);
}The ContentVersionStrategy uses MD5 hash of resource content as version.
public class ContentVersionStrategy extends AbstractFileNameVersionStrategy {
public ContentVersionStrategy() { ... }
@Override
public String getResourceVersion(Resource resource) { ... }
}Usage:
import org.springframework.web.reactive.resource.ContentVersionStrategy;
ContentVersionStrategy strategy = new ContentVersionStrategy();
// For a file like "app.js", this generates:
// app-abc123def456.js
// where "abc123def456" is the MD5 hash of the contentThe FixedVersionStrategy uses a fixed version string.
public class FixedVersionStrategy extends AbstractPrefixVersionStrategy {
public FixedVersionStrategy(String version) { ... }
@Override
public String getResourceVersion(Resource resource) { ... }
}Usage:
import org.springframework.web.reactive.resource.FixedVersionStrategy;
FixedVersionStrategy strategy = new FixedVersionStrategy("v1.0.0");
// For a file like "app.js", this generates:
// v1.0.0/app.jsThe TransformedResource represents a transformed resource with modified content.
public class TransformedResource implements HttpResource {
public TransformedResource(Resource original, byte[] transformedContent) { ... }
// Get original resource
public Resource getOriginal() { ... }
@Override
public InputStream getInputStream() throws IOException { ... }
@Override
public long contentLength() throws IOException { ... }
@Override
public long lastModified() throws IOException { ... }
@Override
public Resource createRelative(String relativePath) throws IOException { ... }
@Override
public String getFilename() { ... }
@Override
public URL getURL() throws IOException { ... }
@Override
public URI getURI() throws IOException { ... }
@Override
public File getFile() throws IOException { ... }
@Override
public boolean exists() { ... }
@Override
public boolean isReadable() { ... }
@Override
public boolean isOpen() { ... }
@Override
public String getDescription() { ... }
@Override
public HttpHeaders getResponseHeaders() { ... }
}The HttpResource interface extends Resource with HTTP-specific metadata.
public interface HttpResource extends Resource {
// Get HTTP response headers for this resource
HttpHeaders getResponseHeaders();
}The ResourceUrlProvider generates public URLs for static resources.
public class ResourceUrlProvider implements ApplicationContextAware {
// Set resource handler registry
public void setResourceHandlers(Map<String, ResourceWebHandler> resourceHandlers) { ... }
// Get resource handlers
public Map<String, ResourceWebHandler> getResourceHandlers() { ... }
// Get resource URL for path
public Mono<String> getForUriString(String uriString, ServerWebExchange exchange) { ... }
// Get resource URL for lookup path
public Mono<String> getForLookupPath(String lookupPath, ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.web.reactive.resource.ResourceUrlProvider;
@RestController
public class ResourceController {
private final ResourceUrlProvider resourceUrlProvider;
public ResourceController(ResourceUrlProvider resourceUrlProvider) {
this.resourceUrlProvider = resourceUrlProvider;
}
@GetMapping("/page")
public Mono<String> getPage(ServerWebExchange exchange) {
// Get versioned URL for a resource
return resourceUrlProvider.getForLookupPath("/css/main.css", exchange)
.map(versionedUrl -> {
// Returns something like: /css/main-abc123.css
return buildHtmlPage(versionedUrl);
});
}
}Utility methods for resource handling.
public class ResourceHandlerUtils {
// Check if request is a resource request
public static boolean isResourceRequest(ServerWebExchange exchange) { ... }
// Get media type for resource
public static MediaType getMediaType(Resource resource) { ... }
// Set headers for resource
public static void setHeaders(ServerWebExchange exchange,
Resource resource,
MediaType mediaType) { ... }
}Base class for resource resolver implementations.
public abstract class AbstractResourceResolver implements ResourceResolver {
protected final Log logger = LogFactory.getLog(getClass());
@Override
public Mono<Resource> resolveResource(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
@Override
public Mono<String> resolveUrlPath(String resourcePath,
List<? extends Resource> locations,
ResourceResolverChain chain) { ... }
protected abstract Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,
String requestPath,
List<? extends Resource> locations,
ResourceResolverChain chain);
protected abstract Mono<String> resolveUrlPathInternal(String resourcePath,
List<? extends Resource> locations,
ResourceResolverChain chain);
}Base class for resource transformer implementations.
public abstract class ResourceTransformerSupport implements ResourceTransformer {
protected final Log logger = LogFactory.getLog(getClass());
// Set resource URL provider
public void setResourceUrlProvider(ResourceUrlProvider resourceUrlProvider) { ... }
// Get resource URL provider
public ResourceUrlProvider getResourceUrlProvider() { ... }
// Resolve URL path
protected Mono<String> resolveUrlPath(String resourcePath,
ServerWebExchange exchange,
Resource resource,
ResourceTransformerChain chain) { ... }
// Convert to public resource URL
protected Mono<String> toAbsolutePath(String path, ServerWebExchange exchange) { ... }
}Base class for version strategies that add version to file name.
public abstract class AbstractFileNameVersionStrategy implements VersionStrategy {
protected final Log logger = LogFactory.getLog(getClass());
@Override
public String extractVersion(String requestPath) { ... }
@Override
public String removeVersion(String requestPath, String version) { ... }
@Override
public String addVersion(String requestPath, String version) { ... }
// File name pattern: name-version.extension
protected String extractVersionFromFilename(String filename) { ... }
}Base class for version strategies that add version as URL prefix.
public abstract class AbstractPrefixVersionStrategy implements VersionStrategy {
protected final Log logger = LogFactory.getLog(getClass());
@Override
public String extractVersion(String requestPath) { ... }
@Override
public String removeVersion(String requestPath, String version) { ... }
@Override
public String addVersion(String requestPath, String version) { ... }
// Path pattern: version/path/to/resource
}Exception thrown when a resource cannot be found.
public class NoResourceFoundException extends ResponseStatusException {
public NoResourceFoundException(String resourcePath) { ... }
public NoResourceFoundException(HttpMethod method, String resourcePath) { ... }
public String getResourcePath() { ... }
}Usage:
public Mono<Resource> findResource(String path) {
return resourceService.find(path)
.switchIfEmpty(Mono.error(new NoResourceFoundException(path)));
}import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.CacheControl;
import org.springframework.web.reactive.resource.*;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@Configuration
public class ResourceHandlingConfig {
@Bean
public ResourceWebHandler resourceWebHandler() {
ResourceWebHandler handler = new ResourceWebHandler();
// Configure locations
handler.setLocations(Arrays.asList(
new ClassPathResource("static/"),
new ClassPathResource("public/")
));
// Configure cache control
handler.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic());
handler.setUseLastModified(true);
handler.setOptimizeLocations(true);
// Configure resource resolvers
VersionResourceResolver versionResolver = new VersionResourceResolver()
.addContentVersionStrategy("/**/*.js", "/**/*.css")
.addFixedVersionStrategy("v1.0", "/**/*.png", "/**/*.jpg");
EncodedResourceResolver encodedResolver = new EncodedResourceResolver();
CachingResourceResolver cachingResolver = new CachingResourceResolver(
new ConcurrentMapCache("resourceCache")
);
PathResourceResolver pathResolver = new PathResourceResolver();
handler.setResourceResolvers(Arrays.asList(
cachingResolver,
encodedResolver,
versionResolver,
pathResolver
));
// Configure resource transformers
CssLinkResourceTransformer cssTransformer = new CssLinkResourceTransformer();
CachingResourceTransformer cachingTransformer = new CachingResourceTransformer(
new ConcurrentMapCache("transformerCache")
);
handler.setResourceTransformers(Arrays.asList(
cachingTransformer,
cssTransformer
));
return handler;
}
}