REST Server — Mixins and Multi-Mount Paths
Juneau REST servers ship two complementary composition / mounting primitives on the class-level
@Rest annotation:
@Rest(mixins=Class<?>[])— inline composition with per-mixin sub-contexts. Graft every@RestOp-group method (@RestGet,@RestPost,@RestPut,@RestPatch,@RestDelete,@RestOptions,@RestOp) from a listed class into the importing resource's operation tree. Local methods on the importing resource win on path/method collisions. Starting in 10.0.0 each mixin owns its ownRestContextparent-linked to the host's, so the mixin can declare serializers / guards / hooks / call-logger / messages that apply only to its endpoints — see Mixin Sub-Contexts for the inheritance model.@Rest(mixinDefs=@Mixin[])(10.0.0) — host-side rich mixin composition. The override form ofmixins: each@Mixin(type=X.class, ...)names a mixin and lets the host override selected@Restsettings (guards, serializers, default headers, mountpath, …) for that mixin's endpoints without editing or subclassing the mixin. Coexists with baremixins=; a@Mixin(type=X.class)with no overrides equals a baremixins=X.classentry. See Host-side overrides with@Mixin.@Rest(paths=String[])— multi-mount for top-level servlets. Mount a singleRestServletinstance under multiple exact URL patterns. Primarily intended for back-compat- friendly add-on endpoints (probes, well-known paths) that need to live at fixed URLs without fighting an app's rootpath="/"mapping.
Both attributes are purely additive — resources that don't declare them keep their pre-10.0.0
behavior unchanged. They are also not mutually exclusive: nothing stops a single class from
declaring both mixins and paths at once. The built-in health-probe family, however, keeps
the three deployment flavors as three separate classes — HealthResource (child mount),
HealthServlet (standalone paths= mount), and HealthMixin (mixins= composition) — rather
than collapsing them into one dual-purpose class; see
Standalone vs Mixin Resources for the rationale.
Motivation
Why composition?
Java single-inheritance forces a binary choice when you want to pull in a built-in resource such
as HealthMixin:
- Subclass it — and lose the ability to subclass your own framework type (
BasicRestServlet,BasicRestResourceGroup, etc.). - Wire it as a separate child resource — and pay the cost of an extra servlet mount, a separate
RestContext, a separate bean store, and so on.
@Rest(mixins=...) is the third option: keep your existing inheritance chain, but borrow the
@RestOp methods from one (or several) addon classes into your own operation tree. The mixin's
methods run inside your RestContext, share your bean store, and surface at your
configured path. The cost of pulling in an addon drops to a single class literal on @Rest.
Why multi-mount?
Servlet path is conventionally a single prefix (/*-style). Add-on endpoints whose URLs are
fixed by external convention — Kubernetes probes (/healthz, /readyz, /livez), well-known
URIs (/.well-known/...), bootstrap endpoints, etc. — don't fit that mold. Mounting them under
a single path="/" collides with whatever the app's own root resource owns; mounting them under
a long path prefix breaks the contract clients depend on.
@Rest(paths=...) solves this: one servlet instance, multiple exact-match URL patterns, no
prefix prefix-stripping. The JettyServerComponent auto-mount path honors paths when present
and falls back to path otherwise.
@Rest(mixins=...)
What it does
When the importing resource's RestContext is built, the mixin walk:
- Collects the transitive closure of all mixin classes declared on the importing class
(across
noInherit-aware@Restchains), in parent-to-child order, with a cycle guard. - Instantiates each mixin class through the importing resource's bean store and caches the instance in the bean store (one mixin instance per importing resource).
- Walks each mixin class's
@RestOp-group methods as if they were declared on the importing resource, registering them in the operation tree against the cached mixin instance as the invocation target. - Walks the importing resource's local
@RestOp-group methods last. Local methods win on path/method collisions — by registration order — so a resource can selectively override a single mixin operation without losing the others.
Example: mixing in HealthMixin
The recommended way to wire Kubernetes probes into an existing root servlet:
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.rest.server.*;
import org.apache.juneau.rest.server.health.*;
import org.apache.juneau.rest.server.servlet.*;
@Rest(
path="/",
mixins=HealthMixin.class
)
public class RootResources extends BasicRestServlet {
@Bean(name="dbHealth")
HealthIndicator dbHealth() {
return () -> Health.up("db").detail("validationQueryMs", 12).build();
}
}
After construction, the importing servlet exposes (in addition to its own routes):
GET /healthz— aggregated probe response (200/503).GET /readyz— readiness-only aggregation.GET /livez— liveness-only aggregation.
No subclassing, no extra ServletHolder, no separate context. The mixin's three @RestGet
methods run inside RootResources' own RestContext and share its bean store — which is why the
@Bean HealthIndicator dbHealth() factory above is picked up by the mixin's aggregation logic
without any extra registration.
Importer-wins on collision
If the importing resource declares a route at the same (path, method) tuple as a mixin route,
the importing resource's method wins:
@Rest
public class SampleMixin {
@RestGet(path="/same")
public String fromMixin() { return "mixin"; }
}
@Rest(mixins=SampleMixin.class)
public class RootResources extends BasicRestServlet {
@RestGet(path="/same")
public String fromResource() { return "resource"; }
}
A GET /same request resolves to RootResources.fromResource() and returns "resource". The
mixin's fromMixin() is reachable only if the importer removes its own /same route.
This is the standard "decorator pattern but with importer precedence" — pull in everything by default, override per-route as needed.
Transitive mixins and cycle handling
Mixins are walked transitively: if A mixes in B, and B mixes in C, then A ends up with
the operations declared on B and on C. The walk is breadth-first, parent-to-child, and uses
a visited set keyed by mixin class so each mixin contributes at most once even when reachable
via multiple paths:
@Rest
public class C {
@RestGet(path="/c") public String c() { return "c"; }
}
@Rest(mixins=C.class)
public class B {
@RestGet(path="/b") public String b() { return "b"; }
}
@Rest(mixins=B.class)
public class A extends BasicRestServlet {
@RestGet(path="/root") public String root() { return "root"; }
}
A exposes /root, /b, and /c. Multiple mixins on the same importer compose the same way:
@Rest(mixins={A_MixinA.class, B_MixinC.class}) pulls in everything from both walks, deduped by
visited-set so a diamond doesn't double-register.
Cycles are tolerated — the visited set short-circuits any back-edge — so a mixin class that transitively mixes in its own importer doesn't recurse infinitely.
Bean-store-resolved mixin instances
Mixin instances are instantiated via the importing resource's bean store, so a mixin class with a constructor that takes injected dependencies wires up the same way a local resource class does. The instance is cached on first use and stored back into the bean store, so:
- A subsequent
bs.getBean(MyMixin.class)from elsewhere in the resource graph returns the cached instance. - Subsequent calls to the same mixin route reuse that instance (no per-request instantiation).
- A
@Beanfactory method on the importing resource for the mixin type, if present, takes precedence over the default constructor walk.
Mixin invocation target
RestOpInvoker ordinarily invokes the operation method on RestSession.getResource() — the
importing resource instance. For mixin operations, the invocation target is explicitly the cached
mixin instance (not the importing resource), so the mixin's method body can rely on this
referring to the mixin and on the mixin's own private state being available. Both the regular
and RRPC dispatch paths honor this distinction.
@Rest(paths=...)
What it does
paths is an alternative to path for top-level servlet mounting. Where path declares a
single prefix (typically /*-style — path="/foo" matches /foo/*), paths declares an array
of exact-match URL patterns that all map to the same ServletHolder:
@Rest(paths={"/healthz","/readyz","/livez"})
public class HealthServlet extends BasicRestServlet { ... }
The Jetty auto-mount logic (in JettyServerComponent) sees the paths attribute and registers
one servlet instance with the ServletContextHandler under three separate path-spec
mappings (/healthz, /readyz, /livez). The same HealthServlet instance handles
every request that lands on any of the three URLs.
Path-spec semantics differ from path:
| Attribute | Pattern shape | Servlet container interpretation |
|---|---|---|
path="/foo" | Prefix (/foo/*-style) | Matches /foo, /foo/bar, /foo/baz/qux, … |
paths={"/foo"} | Exact (/foo) | Matches only /foo (no subpath wildcard). |
Use path when the resource owns a URL subtree (/orders/*). Use paths when the resource owns
one (or several) specific URLs and you don't want a wildcard absorbing sibling routes.
Example: probe URLs without a root collision
The motivating use case. HealthServlet ships with @Rest(paths={"/healthz","/readyz","/livez"})
and is wired in via HealthProbeConfiguration:
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.microservice.*;
import org.apache.juneau.microservice.jetty.*;
import org.apache.juneau.rest.server.health.*;
import jakarta.servlet.*;
@Configuration
public class AppConfig {
@Bean
Servlet root() {
return new RootResources(); // @Rest(path="/")
}
@Bean
HealthIndicator dbHealth() {
return () -> Health.up("db").detail("validationQueryMs", 12).build();
}
}
public class App {
public static void main(String[] args) throws Exception {
Microservice.create()
.args(args)
.configurations(JettyConfiguration.class, HealthProbeConfiguration.class, AppConfig.class)
.build()
.start()
.join();
}
}
After startup, the Jetty container has two top-level mounts:
| Path spec | Servlet | Source |
|---|---|---|
/* | RootResources | @Bean Servlet root() + @Rest(path="/") |
/healthz | HealthServlet | @Bean Servlet healthProbeServlet() + @Rest(paths=...) |
/readyz | HealthServlet | (same instance, second mapping) |
/livez | HealthServlet | (same instance, third mapping) |
The three probe paths are exact-match, so they're matched by the servlet container before
falling through to RootResources's /* prefix mount. No collision, no order-of-registration
fragility — just three explicit URLs that always land on HealthServlet.
Single ServletHolder, multiple mappings
Multi-mount uses one ServletHolder instance and registers it under each path-spec:
var sh = new ServletHolder(servlet);
for (var pathSpec : pathSpecs)
getServletContextHandler().addServlet(sh, pathSpec);
Consequences:
- One servlet lifecycle (
init/destroyare called once). - One backing
RestContext(shared across all mount points). - Servlet-container
getServletPath()reflects whichever path the request hit, butRestRequestroute matching runs against the resource's own operation tree regardless of which mount fired.
Collision detection
Each auto-mount path-spec is tracked against its declaring source, and the registration loop fails fast on collisions:
java.lang.RuntimeException: Servlet mount path collision: '/same' is already mounted by
@Bean org.example.FirstServlet[first]; refused by @Bean org.example.SecondServlet[second].
Both path and paths mounts flow through the same collision check, so:
- Two resources declaring
@Rest(paths={"/healthz"})fail at startup. - A resource declaring
@Rest(paths={"/healthz"})and another declaring@Rest(path="/healthz")do not collide — they map to different path-specs (/healthzexact vs/healthz/*prefix). - A
pathsarray internally is just a list of exact-match path-specs; a duplicate inside the array is still a collision (paths={"/h","/h"}fails fast).
The collision message names both the prior mount source and the refused mount source, so multi-bean apps can find the conflict without digging through Jetty internals.
Interaction with path
When both path and paths are present on the same resource, the Jetty auto-mount logic
prefers paths for top-level servlet registration. The path attribute is still meaningful
for child-resource composition — i.e. when the resource is referenced via @Rest(children=...)
on a parent, the child's path is used as the subpath under the parent.
This split lets a resource ship with sensible defaults for both deployment styles:
- Top-level mount (
@Bean Servlet):pathswins, the resource lands at its declared exact URLs. - Child mount (
@Rest(children=...)):pathwins, the resource lands under the parent's subtree.
When to choose which
| You want… | Use |
|---|---|
Add an addon's @RestOp methods to an existing root servlet. | mixins |
| Mount a separate addon servlet at fixed URLs without prefixes. | paths |
| Override one route on a mixin while keeping the rest. | mixins + local method on importer (importer-wins). |
| Run an addon in isolation (its own context, bean store, etc.). | paths — declare the addon as its own @Bean Servlet. |
| Keep the option open and let downstream pick. | Declare both attributes on the same class, or ship separate mixin/standalone flavors and let the caller choose. |
The two attributes are not mutually exclusive at the framework level — nothing stops a single
class from declaring both mixins-compatible operations and a paths array. The built-in
health-probe family instead keeps the flavors as separate classes: HealthServlet declares
@Rest(paths={"/healthz","/readyz","/livez"}) so it can be auto-mounted standalone (Option B in
the probe docs), while HealthMixin is the bare-@Rest flavor apps pull in via
@Rest(mixins=HealthMixin.class) on their own root resource (Option A). Both delegate to the
same HealthAggregator worker bean, so the two forms can't drift. The mixin path
ignores the paths array — mixin operations are grafted at the importer's URL space, not the
mixin's own.
Caveats
A few intentional limitations to be aware of.
Mixin scope and per-mixin RestContext
The mixin walk grafts @RestOp-group methods from the mixin class. 10.0.0 promotes each
mixin class to its own RestContext parent-linked to the host's, so the following mixin-class
declarations now apply specifically to that mixin's endpoints (host endpoints are unaffected):
@Rest(serializers=..., parsers=..., encoders=...)— append to the host's chain for mixin endpoints only.@Rest(converters=..., responseProcessors=..., restOpArgs=..., guards=...)— same: appended to the host's chain for mixin endpoints. Guards inherit by default — see the Mixin Sub-Contexts topic for the security rationale.@Rest(messages=..., partSerializer=..., partParser=...)— chained / overridden per mixin.@RestStartCall/@RestEndCall/@RestPreCall/@RestPostCall/@RestDestroy— dual-fire, host-then-mixin for mixin-endpoint requests; host-only for host-endpoint requests.
@Rest(noInherit={...}) on a mixin class cuts off inheritance per property using the same token
set as the host's @Rest(noInherit={...}) machinery. The full inheritance model — including the
flat-inheritance rule for transitive mixins, hook ordering, and the Messages.chain(...) seam
that makes message-bundle inheritance work — is documented in the
Mixin Sub-Contexts topic.
The mixin walk still does not inherit:
- The mixin's class-level
@HtmlDocConfig/@JsonConfig/ other config annotations. - The mixin's
@Rest(children=...)child resources. Mixins don't compose child trees. - The mixin's
@Rest(path=...)or@Rest(paths=...)— the importing resource's path / paths configuration is what controls mounting. - The mixin class's superclass methods (
extends BasicRestServlet, etc.). Only the mixin class's own declared@RestOpmethods are grafted; the mixin's inheritance chain isn't re-traversed by the walk. (The mixin class itself can still subclass anything it likes — the superclass methods just don't pile into the importer.) - Static fields, instance fields, or per-mixin private state — unless the mixin's
@RestOpmethods explicitly read them. The cached mixin instance is the invocation target, so any per-instance state on the mixin is honored; it just isn't visible to the importing resource's local methods or to other mixins.
If you need the addon to mount under its own URL subtree with full resolution isolation from the
host, the right tool is @Rest(children=...), not mixins. Children are deliberately isolated
from the parent's resolution chain — see
Mixin Sub-Contexts § Mixin-vs-child divergence
for the rationale.
paths is exact-match only
paths entries are normalized to exact-match servlet path-specs. There's no equivalent of
path's prefix-extension behavior. If you want /healthz/extra/segment to also resolve to the
probe resource, declare it explicitly (paths={"/healthz","/healthz/extra/segment"}) or use
path="/healthz" for the whole subtree.
paths is honored by the auto-mount path
paths is interpreted by JettyServerComponent's @Bean Servlet auto-mount loop. Manual
addServlet(...) calls and externally-configured Jetty/servlets / Jetty/servletMap entries
take whatever path-spec the caller hands them, regardless of the paths attribute on the
servlet class. The auto-mount honor is opt-in — @Bean Servlet is the trigger.
Runtime-overridable paths
The paths array on @Rest is a default — apps frequently need to substitute it at deploy
time without recompiling the resource (e.g. probe URLs that vary by Kubernetes namespace, internal
endpoints whose paths differ between the AWS and GCP clusters, etc.). Juneau resolves the
top-level mount paths through a three-rung runtime-override resolution chain, walked once
during RestContext construction.
Precedence (highest first)
| Rung | Source | Notes |
|---|---|---|
| 1 | Programmatic — RestContext.Args.paths() / RestContext.Builder.paths(String...) | Highest precedence. null = inherit; new String[0] = explicit clear. |
| 2 | RestServlet.getPaths() / RestObject.getPaths() virtual getter | null = inherit lower rung; non-null (incl. empty) wins. |
| 3 | @Rest(paths={"…"}) annotation default | Lowest rung. Each element runs through SVL, then is split on , (trim, drop empties). |
The resolved array is exposed via RestContext.getPaths() (and consumed at mount time by
JettyServerComponent and the public mount-time helper
RestContext.resolveTopLevelPaths(Class, Object, BeanStore) for Spring Boot's
ServletRegistrationBean flow).
Worked examples
Rung 1 — programmatic override (test fixtures, custom mounts):
var args = new RestContext.Args(MyResource.class, null, null, MyResource::new, "", null, null,
new String[]{"/api/v2/healthz", "/api/v2/readyz"});
var ctx = new RestContext(args);
// ctx.getPaths() → {"/api/v2/healthz", "/api/v2/readyz"}
Pass new String[0] (not null) to explicitly clear all lower rungs and end up with no
top-level mounts.
Rung 2 — getPaths() getter override on a subclass:
@Rest(paths={"/healthz","/readyz"})
public class TenantHealthServlet extends HealthServlet {
@Override
public String[] getPaths() {
var prefix = "/" + tenantId() + "/health";
return new String[]{ prefix + "/live", prefix + "/ready" };
}
}
The getter runs at construction time (before the servlet container hits the resource), so the
mounted URLs reflect the per-deployment tenant id without recompiling HealthServlet.
Rung 3 — @Rest(paths={"…"}) with SVL + comma-split:
Each element of the paths array is a template. The resolver:
- Runs the element through SVL substitution (using the bootstrap
VarResolveron the bean store, so$C{key}consults the registered JuneauConfig). - Splits the post-SVL value on
,, trims each piece, and drops empties.
A single element can therefore expand to zero, one, or many mount paths. Examples:
// 1. Single Config key whose value is a comma-separated list.
@Rest(paths={"$C{health.paths}"})
public class HealthResource extends BasicRestServlet { ... }
# my-app.cfg
health.paths = /probe/live, /probe/ready
// 2. Mix literal and resolved elements.
@Rest(paths={"/api", "$C{extra.paths}"})
public class ApiResource extends BasicRestServlet { ... }
// 3. Env var with comma-separated default baked in.
@Rest(paths={"$E{HEALTH_PATHS,/healthz,/readyz}"})
public class HealthResource extends BasicRestServlet { ... }
# At deploy time:
HEALTH_PATHS="/probe/live,/probe/ready" java -jar my-app.jar
The same SVL surface as any other @Rest string member is available: $C{key[,default]} for
Juneau Config, $E{NAME[,default]} for environment variables, $S{prop[,default]} for
system properties, and so on.
Resolution-miss behavior is uniform across SVL: when a key resolves to nothing and no default is
provided, the substitution is empty, and the comma-split then drops the empty piece. The
annotation's own array still represents the floor — an element like "/literal" is unaffected
by adjacent unresolved SVL elements.
Spring Boot integration
Under juneau-rest-server-springboot, paths still resolve through the same three-rung chain.
Spring Boot's manual ServletRegistrationBean flow uses the public mount-time helper:
@Configuration
class HealthBoot {
@Bean
ServletRegistrationBean<HealthResource> healthServletRegistration(
HealthResource servlet, ApplicationContext appContext) {
var store = new SpringBeanStore(appContext, null);
var paths = RestContext.resolveTopLevelPaths(HealthResource.class, servlet, store);
return new ServletRegistrationBean<>(servlet, paths);
}
}
The same chain runs whether the app is wired via Juneau auto-discovery (JettyServerComponent)
or Spring Boot's manual registration — the only difference is that the programmatic Builder rung
is N/A in the manual flow (no RestContext exists at registration time; users substitute via
the getter, by registering a Config bean whose contents drive $C{...} elements, or by
providing system-property / env-var overrides for $S{...} / $E{...} elements).
Spring Boot
application.yamlintegration is planned for a future release. Once available,$C{key}in@Rest(paths=...)elements will transparently consult Spring'sEnvironmentunder Spring Boot via a Juneau-Config-to-Environmentbridge — no API change here, just an additional resolution source registered with theConfigbean.
Null vs. empty-array semantics
| Value | Programmatic (Rung 1) | Getter (Rung 2) |
|---|---|---|
null | No override; lower rungs resolve. | Inherit lower rung. |
new String[0] | Explicit clear; no top-level mounts. | Explicit clear; no top-level mounts. |
Picking the right sentinel matters: null lets the annotation default still resolve beneath
you; new String[0] short-circuits and produces a resource with no mounts (which surfaces a
clear "no mounts" error from the hosting runtime when nothing is reachable).
Mixin operations don't surface in Swagger as "imported"
The Swagger / OpenAPI emission walks the importing resource's full operation tree, so mixin
operations appear in the generated spec as ordinary operations of the importing resource. There
is no per-operation "imported from X" tag in the spec. If you need that distinction for API
consumers, declare the addon as a separate child resource (@Rest(children=...)) so it gets its
own Swagger / OpenAPI scope.
Parent-chain mixin aggregation
@Rest is @Inherited, so a mixin set declared on a parent class flows through to subclasses
just like the rest of the @Rest attributes. The aggregator collects every @Rest(mixins=...)
in the resource class hierarchy in parent-to-child order, with the parent's mixins inserted
first and the child's appended after. The resulting list is funneled through a
LinkedHashSet keyed by class identity, so:
- Order is preserved in the order Juneau encountered each mixin (parent's mixins first, then the child's). The mixin walk visits them in that order, so registration order — and therefore importer-wins precedence for any collisions between two mixins — is deterministic across rebuilds.
- Duplicates are deduped silently. Declaring the same mixin class on both parent and child
is harmless —
LinkedHashSetonly keeps the first occurrence, so the mixin is constructed once and grafted once.
Example: parent declares a mixin, child inherits
@Rest(mixins = RedocMixin.class)
public abstract class CommonApi extends RestServlet {
@RestGet("/health") public String health() { return "ok"; }
}
@Rest // No mixins declared — inherits parent's by Java @Inherited.
public class TenantApi extends CommonApi {
@RestGet("/items") public List<Item> items() { ... }
}
TenantApi resolves to mixin set [RedocMixin, OpenApiMixin] — the parent's
RedocMixin flows in via @Inherited, and transitive resolution then walks
RedocMixin's own @Rest(mixins=OpenApiMixin.class) to bring in the spec
mixin. Result: TenantApi serves /health, /items, /openapi, /openapi.json,
/openapi.yaml, and /redoc.
Example: parent + child both declare mixins (union)
@Rest(mixins = SwaggerMixin.class)
public abstract class CommonApi extends RestServlet { ... }
@Rest(mixins = HealthMixin.class)
public class TenantApi extends CommonApi { ... }
TenantApi resolves to mixin set [SwaggerMixin, HealthMixin] — parent-first
ordering preserved, both mixins live, both grafted. The child does not need to redeclare the
parent's mixin to keep it.
Example: parent + child declare the same mixin (dedupe)
@Rest(mixins = HealthMixin.class)
public abstract class CommonApi extends RestServlet { ... }
@Rest(mixins = {HealthMixin.class, SwaggerMixin.class})
public class TenantApi extends CommonApi { ... }
TenantApi resolves to mixin set [HealthMixin, SwaggerMixin]. The
HealthMixin declaration on the child is harmless — LinkedHashSet dedupes by class
identity, so the mixin is constructed once and grafted once. The child can list the parent's
mixin alongside its own without worrying about double-registration.
Opt-out: @Rest(noInherit={"mixins"})
Sometimes a child needs to drop the parent's mixins entirely — e.g. a subclass that wants to
substitute a different api-docs surface, or a custom subclass of BasicRestServlet that wants
only the OpenAPI half:
@Rest(noInherit = {"mixins"}, mixins = RedocMixin.class)
public class OpenApiOnly extends BasicRestServlet {
@RestGet("/items") public List<Item> items() { ... }
}
noInherit = {"mixins"} tells the aggregator to skip the parent's @Rest(mixins=...) when
walking the inheritance chain. OpenApiOnly therefore mounts /openapi, /openapi.json,
/openapi.yaml, /redoc, and /items — but not /api, /swagger, or any of
BasicRestServlet's default Swagger surface.
noInherit accepts the same property names that @Rest exposes (e.g. "converters",
"guards", "mixins", "responseProcessors"), so you can cut off inheritance per-property
without dropping everything else.
Interaction with transitive mixin resolution
Parent-chain aggregation and transitive mixin resolution are orthogonal — both apply, and the order is well-defined:
- The aggregator walks the resource class hierarchy parent-to-child, collecting every
@Rest(mixins=...)declaration into the initial mixin set. - The transitive walk then visits each mixin in that set and recursively pulls in any
mixins the mixin itself declares via its own
@Rest(mixins=...). LinkedHashSetdeduping runs on the union, so a mixin reachable via multiple paths (parent-direct, child-direct, transitive-from-parent's-mixin, transitive-from-child's-mixin) is still constructed once.
Worked example:
@Rest(mixins = OpenApiMixin.class)
public abstract class CommonApi extends RestServlet { ... }
@Rest(mixins = RedocMixin.class)
public class TenantApi extends CommonApi { ... }
TenantApi resolves to mixin set [OpenApiMixin, RedocMixin]:
- Parent contributes
OpenApiMixindirectly. - Child contributes
RedocMixindirectly. - Transitive walk visits
RedocMixinand follows its@Rest(mixins=OpenApiMixin.class)— butOpenApiMixinis already in the set, so the dedupe drops the second occurrence on the floor. Net effect: the spec mixin is grafted once, mounted at/openapi/*, and shared between both UI mounts.
This is the pattern the api-docs mixin pack relies on to make
@Rest(mixins={SwaggerUiMixin.class, RedocMixin.class}) on BasicRestServlet
work correctly. See API-Docs Mixin Pack for the full worked
example, including the noInherit={"mixins"} opt-out for users who want to drop one half of
the inherited pack.
See also
- REST Server — Programmatic Fluent Builder — building and configuring
resources entirely in code via
RestBuilder<SELF>/AbstractRestBuilder<R, SELF>, without@Restannotation attributes. - API-Docs Mixin Pack — the canonical worked example of mixin
composition: four single-purpose api-docs mixins (
SwaggerMixin,SwaggerUiMixin,OpenApiMixin,RedocMixin) composed intoBasicRestServletwith transitive resolution + parent-chain aggregation. - Mixin Sub-Contexts — the per-mixin
RestContextinheritance model,noInheritopt-out, hook dual-firing, flat-inheritance rule, and mixin-vs-child divergence. - Health / Readiness / Liveness Probes — the canonical consumer of both primitives, with full Option A (mixin) and Option B (standalone auto-mount) examples.
- @Rest-Annotated Class Basics — the broader
@Restattribute reference. - Child Resources — the alternative composition primitive when
you want a separate
RestContextand bean store per addon, isolated from the host's resolution chain. - Path Patterns — operation-level path matching (the layer mixin routes are grafted into).
- REST Server Overview — where these primitives sit in the overall request-routing pipeline.