Skip to main content

Static-Files Mixin

The Juneau REST server ships with a StaticFilesMixin mixin in the org.apache.juneau.rest.server.staticfile package that wraps the existing BasicStaticFiles plumbing (a StaticFiles implementation, not a servlet) in a servlet-level mixin with a configurable mount point. Any @Rest-annotated resource can opt into static-file serving via @Rest(mixins=...) without subclassing BasicRestServlet just to inherit getHtdoc(...).

Need broader background on the underlying StaticFiles bean (classpath search roots, caching, exclude patterns)? See Static files for the plain-bean reference; this page covers only the mixin packaging on top.

What the mixin does​

The mixin declares two REST operation methods bound to the same handler so a single Java method serves both GET and HEAD for the mixin's mount point:

@Rest
public class StaticFilesMixin {

@RestGet(
path = "/#{pathToken(${juneau.staticfiles.path:static})}/*",
swagger = @OpSwagger(ignore = true)
)
public HttpResource getStaticFile(RestRequest req, @Path("/*") String path, Locale locale) {
return resolveStaticFile(req, path, locale);
}

@RestOp(
method = "HEAD",
path = "/#{pathToken(${juneau.staticfiles.path:static})}/*",
swagger = @OpSwagger(ignore = true)
)
public HttpResource headStaticFile(RestRequest req, @Path("/*") String path, Locale locale) {
return getStaticFile(req, path, locale);
}

public static HttpResource resolveStaticFile(RestRequest req, String path, Locale locale) throws NotFound {
return req.getStaticFiles().resolve(path, locale).orElseThrow(NotFound::new);
}
}

The op-level path is an SVL expression, so the mount defaults to /static/* but can be overridden without subclassing — see Mount path configuration below. The static-file resolution itself lives in the resolveStaticFile(...) static helper, which is also what HtdocMixin's legacy /htdocs/* handler calls into — a single implementation of the lookup logic shared by both mixins.

The handler resolves the StaticFiles bean via req.getStaticFiles() at request time — a RestRequest convenience shortcut for req.getContext().getStaticFiles() — which delegates to BeanStore.getBean(StaticFiles.class) and falls back to a default BasicStaticFiles instance that searches the importer's classpath under static/ and htdocs/ plus the working-directory static/ and htdocs/ folders. Missing paths surface as NotFound thrown from the handler, which flows through the standard exception-rendering chain (RFC 7807 problem-details when the problem-details opt-in is active; plain text otherwise).

Default mount usage​

Add the mixin to any @Rest-annotated resource — no subclassing of BasicRestServlet required — and /static/* starts serving classpath resources immediately. This is safe to add to a BasicRestServlet subclass too: BasicRestServlet already bakes in HtdocMixin for the legacy, fixed /htdocs/* mount (see Static files), and the two mixins no longer compete for the same URL by default — they share the same resolution logic but expose different mounts.

@Rest(path = "/api", mixins = StaticFilesMixin.class)
public class ApiResource extends RestServlet {

@RestGet("/items")
public List<Item> items() { ... }
}
// Serves /api/items and /api/static/<file>.

StaticFilesMixin is itself a fully-fledged @Rest-annotated resource and can also be subclassed and mounted as its own top-level servlet:

public class CdnResource extends StaticFilesMixin { }

Both deployment styles (mixin into an existing servlet vs. mount as a sibling servlet) work the same way under Spring Boot and under the Jetty microservice — see Spring Boot vs. microservice below.

Mount path configuration​

The default /static/* mount can be overridden via the ${juneau.staticfiles.path:static} SVL variable — set via a system property (-Djuneau.staticfiles.path=assets), an environment variable (JUNEAU_STATICFILES_PATH=assets), or a Config key (juneau.staticfiles.path = assets) — to change the runtime mount without subclassing. The override accepts a bare token (assets), a leading slash (/assets), a trailing slash (assets/), or a wildcard suffix (/assets/*); all resolve to the same mount. Resolution happens once at RestContext construction time.

Setting the override to htdocs reaches the same URL HtdocMixin uses — the classpath BasicStaticFiles search root already walks both static/ and htdocs/ directories by default, so the same files are reachable either way:

// -Djuneau.staticfiles.path=htdocs
@Rest(mixins = StaticFilesMixin.class)
public class ApiResource extends RestServlet {
// Now serves /htdocs/<file> via StaticFilesMixin (GET + HEAD).
}
Deliberate override onto an already-occupied mount

If you set the override to htdocs on a BasicRestServlet/BasicRestResource subclass — which already bakes in HtdocMixin's fixed /htdocs/* mount — StaticFilesMixin now competes for that same URL. This is no longer the default behavior (the two mixins default to different mounts), but it can still happen if you opt into it explicitly; test the combined behavior, or build on plain RestServlet instead so only one static-file mixin is in play at /htdocs/*.

To add a completely different mount path without touching the mixin, subclass it and override the op-level path via a Config/system-property override, or compose StaticFilesMixin alongside another mixin/handler for the additional path — the SVL expression only supports a single mount per StaticFilesMixin instance. Multiple independent mounts (e.g. /static/* and /assets/* on the same host) require either two StaticFiles-serving beans (one StaticFilesMixin instance per desired path, each with its own SVL override at the servlet-registration layer) or a custom handler for the second path.

HEAD support​

The mixin handles HEAD per RFC 7231 §4.3.2: the response carries identical headers to the corresponding GET (Content-Type, Content-Length, Cache-Control, plus any resource-supplied headers) with an empty body.

The body-suppression happens at the framework layer, not in the mixin: the shared HttpResourceProcessor response processor checks RestRequest.getMethod() and short-circuits before writing the body when it sees HEAD. The mixin's headStaticFile(...) handler delegates to getStaticFile(...) verbatim — the processor takes care of the body suppression. The improvement benefits any HttpResource-returning handler, not just the static-files mixin.

RequestResponse statusBodyHeaders
GET /static/foo.css (file present)200file bodyContent-Type, Content-Length, Cache-Control, ...
GET /static/missing.css404exception bodyper exception-rendering chain
HEAD /static/foo.css (file present)200emptyidentical to GET
HEAD /static/missing.css404emptyper exception-rendering chain

Cache-Control behavior​

The mixin returns the HttpResource from BasicStaticFiles directly, so any headers configured on the StaticFiles builder — including the default Cache-Control: max-age=86400, public — flow through to the response without modification. To customize, register your own @Bean StaticFiles factory:

@Rest(mixins = StaticFilesMixin.class)
public class MyResource extends RestServlet {

@Bean
public StaticFiles staticFiles(BeanStore bs) {
return BasicStaticFiles
.create(bs)
.cp(MyResource.class, "/assets", true)
.headers(CacheControl.of("max-age=604800, public, immutable")) // 7 days, immutable
.build();
}
}

The StaticFiles bean is resolved through the request context via req.getStaticFiles() (RestContext.getStaticFiles() under the hood), which honors the @Bean-factory walk (microservice path uses BasicBeanStore; Spring Boot path uses SpringBeanStore → ApplicationContext.getBeanProvider(...)). If you register multiple StaticFiles beans in a Spring @Configuration, mark exactly one @Primary so the bean store lookup is deterministic.

Hidden from OpenAPI​

Both @RestGet and @RestOp(method="HEAD") handlers on StaticFilesMixin are marked swagger = @OpSwagger(ignore = true). A greedy /* blob handler is not API-meaningful, and emitting it as an operation just produces noise. The exclusion applies symmetrically across both spec emitters:

  • SwaggerMixin (Swagger v2 → /api, /swagger) — skips the operation when generating the Swagger document via BasicSwaggerProviderSession.
  • OpenApiMixin (OpenAPI 3.1 → /openapi, /openapi.json, /openapi.yaml, /redoc) — same skip applies via the Swagger-to-OpenAPI transform.

The same @OpSwagger(ignore=true) mechanism is applied to the HtdocMixin.getHtdoc(...) method on BasicRestServlet, so apps that subclass BasicRestServlet directly also get a clean spec for free. See the API-Docs Mixin Pack page for the other half of the api-docs composition story.

@OpSwagger(ignore = true) is reusable on any @RestOp-annotated method — it is the recommended way to suppress greedy /* handlers, internal probes, /favicon.ico, or any operation that should not appear in the published API contract.

Spring Boot vs. microservice​

The mixin works identically under both deployment paths because mixin instances are resolved through the importing servlet's BeanStore:

  • Microservice path. BasicBeanStore looks up StaticFilesMixin.class via getBean(...) first; if no bean is registered, the framework reflects a no-arg constructor. The same lookup applies to the resolved StaticFiles bean. Verified end-to-end by StaticFilesMixin_JettyMicroservice_Test in juneau-integration-tests, which boots a real Jetty Microservice on an ephemeral port and hits the mixin's URLs over real HTTP.
  • Spring Boot path. SpringBeanStore extends BasicBeanStore and delegates unresolved lookups to ApplicationContext.getBeanProvider(beanType).getIfAvailable(). Same fallback to a no-arg constructor if no Spring bean is registered. Verified by StaticFilesMixin_Springboot_Test.

Spring Boot's META-INF/resources/ convention is also covered: an importer that wants the mixin to serve files placed under META-INF/resources/ (Spring Boot's auto-served root) can add it as a classpath search root via BasicStaticFiles.create(bs).cp(...,"/META-INF/resources",true):

@Bean
public StaticFiles staticFiles(BeanStore bs) {
return BasicStaticFiles
.create(bs)
.cp(MyResource.class, "/META-INF/resources", true)
.build();
}

This is pinned by StaticFilesMixin_SpringbootMetaInf_Test — a file at src/main/resources/META-INF/resources/spring-fixture.txt is reachable through /static/spring-fixture.txt under the mixin's default mount (and also through /htdocs/spring-fixture.txt via the always-on HtdocMixin mount when the host extends BasicRestServlet/BasicSpringRestServlet, since both mixins share the same StaticFiles bean). Spring Boot's own static-resource handler also continues to serve the same file at /spring-fixture.txt (the root mount), so the handlers coexist without shadowing each other.

See also​

  • Static files — the underlying StaticFiles and BasicStaticFiles reference; classpath search roots, caching, exclude patterns.
  • REST Server — Mixins and Multi-Mount Paths — the @Rest(mixins=...) primitive this mixin builds on.
  • API-Docs Mixin Pack — the four-class api-docs surface that also uses the @OpSwagger(ignore=true) exclusion mechanism for its /* handlers.
  • Response Processors — how HttpResourceProcessor handles HEAD body suppression and what other response-processor stages run for HttpResource returns.