Skip to main content

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 own RestContext parent-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 of mixins: each @Mixin(type=X.class, ...) names a mixin and lets the host override selected @Rest settings (guards, serializers, default headers, mount path, …) for that mixin's endpoints without editing or subclassing the mixin. Coexists with bare mixins=; a @Mixin(type=X.class) with no overrides equals a bare mixins=X.class entry. See Host-side overrides with @Mixin.
  • @Rest(paths=String[]) — multi-mount for top-level servlets. Mount a single RestServlet instance 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 root path="/" 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:

  1. Collects the transitive closure of all mixin classes declared on the importing class (across noInherit-aware @Rest chains), in parent-to-child order, with a cycle guard.
  2. 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).
  3. 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.
  4. 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 @Bean factory 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:

AttributePattern shapeServlet 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 specServletSource
/*RootResources@Bean Servlet root() + @Rest(path="/")
/healthzHealthServlet@Bean Servlet healthProbeServlet() + @Rest(paths=...)
/readyzHealthServlet(same instance, second mapping)
/livezHealthServlet(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 / destroy are called once).
  • One backing RestContext (shared across all mount points).
  • Servlet-container getServletPath() reflects whichever path the request hit, but RestRequest route 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 (/healthz exact vs /healthz/* prefix).
  • A paths array 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): paths wins, the resource lands at its declared exact URLs.
  • Child mount (@Rest(children=...)): path wins, 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 @RestOp methods 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 @RestOp methods 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)​

RungSourceNotes
1Programmatic — RestContext.Args.paths() / RestContext.Builder.paths(String...)Highest precedence. null = inherit; new String[0] = explicit clear.
2RestServlet.getPaths() / RestObject.getPaths() virtual getternull = inherit lower rung; non-null (incl. empty) wins.
3@Rest(paths={"…"}) annotation defaultLowest 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:

  1. Runs the element through SVL substitution (using the bootstrap VarResolver on the bean store, so $C{key} consults the registered Juneau Config).
  2. 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.yaml integration is planned for a future release. Once available, $C{key} in @Rest(paths=...) elements will transparently consult Spring's Environment under Spring Boot via a Juneau-Config-to-Environment bridge — no API change here, just an additional resolution source registered with the Config bean.

Null vs. empty-array semantics​

ValueProgrammatic (Rung 1)Getter (Rung 2)
nullNo 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 — LinkedHashSet only 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:

  1. The aggregator walks the resource class hierarchy parent-to-child, collecting every @Rest(mixins=...) declaration into the initial mixin set.
  2. 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=...).
  3. LinkedHashSet deduping 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 OpenApiMixin directly.
  • Child contributes RedocMixin directly.
  • Transitive walk visits RedocMixin and follows its @Rest(mixins=OpenApiMixin.class) — but OpenApiMixin is 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 @Rest annotation attributes.
  • API-Docs Mixin Pack — the canonical worked example of mixin composition: four single-purpose api-docs mixins (SwaggerMixin, SwaggerUiMixin, OpenApiMixin, RedocMixin) composed into BasicRestServlet with transitive resolution + parent-chain aggregation.
  • Mixin Sub-Contexts — the per-mixin RestContext inheritance model, noInherit opt-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 @Rest attribute reference.
  • Child Resources — the alternative composition primitive when you want a separate RestContext and 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.