This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new c80e9b9fe9 docs: add 10.07b mixin sub-RestContext topic, cross-link
from 10.07a, and 9.5.0 release notes
c80e9b9fe9 is described below
commit c80e9b9fe9298d95821c9493d4850d45ab5b197f
Author: James Bognar <[email protected]>
AuthorDate: Sun May 24 13:24:16 2026 -0400
docs: add 10.07b mixin sub-RestContext topic, cross-link from 10.07a, and
9.5.0 release notes
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/9.5.0.md | 39 +++
pages/topics/10.07a.RestServerComposition.md | 50 ++-
pages/topics/10.07b.RestServerMixinSubContexts.md | 404 ++++++++++++++++++++++
3 files changed, 482 insertions(+), 11 deletions(-)
diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 1776792120..df8ea7cfc1 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2056,6 +2056,45 @@ Users can now choose either:
- **Recommended**: `@Rest(mixins=BasicHealthResource.class)` on the root
resource (single servlet).
- **Fallback**: standalone `HealthProbeConfiguration` auto-mount (separate
servlet, explicit paths).
+#### Mixin Sub-Context Inheritance
+
+`@Rest(mixins=…)` mixin classes are now each elevated into a dedicated
`RestContext` sub-context
+rather than being treated as bare method libraries grafted into the host's
context. Each mixin
+gets its own bean store, hook chain, and contribution lists; the host's
`RestContext` becomes the
+mixin's `parentContext`, and the mixin inherits configuration via the same
parent-walk used for
+the rest of the `@Rest` annotation chain — with `@Rest(noInherit={…})`
available as the per-property
+opt-out (semantics aligned with the rest of the framework).
+
+- **Inheritance walk.** `RestContext.getRestAnnotationsForProperty(name)`
prepends the host's
+ `@Rest` chain onto the mixin's own so the reduce-last / append-all walks all
the contribution
+ lists transparently. Verified by a per-list regression matrix covering
serializers, parsers,
+ encoders, converters, response processors, REST op args, guards (default
inherit + `noInherit`
+ opt-out), callLogger, debugEnablement, debugDefault, partSerializer, and
partParser. None of
+ these lists needed bespoke wiring — the walk drives them all.
+- **Hook dual-firing.** `@RestStartCall` / `@RestEndCall` / `@RestPreCall` /
`@RestPostCall` /
+ `@RestDestroy` now fire host-first-then-mixin for mixin-endpoint requests
and host-only for
+ host-endpoint requests. Hook execution order is the standard host→mixin
chain regardless of
+ which mixin class declared the operation that's being dispatched, so
per-request
+ observability (logging, metrics, request-id injection) stays uniform across
host and mixin
+ endpoints.
+- **Messages chaining.** `Messages` doesn't ride the `@Rest` walk (it has its
own
+ parent-resource-bundle chain semantics), so the new `Messages.chain(child,
parent)` static
+ factory composes two pre-built bundles into a single parent-chained bundle
without mutating
+ either input. The `messages` memoizer uses it to inherit the host's bundle
on mixin sub-contexts
+ (mixin-keys-first, host-keys-as-fallback). `@Rest(noInherit={"messages"})`
cuts the chain off
+ for callers that want isolation.
+- **`DefaultConfig` synthesis is host-only.** The framework's synthesized
`DefaultConfig` entry on
+ a bare `@Rest` resource is now skipped for mixin sub-contexts, because the
host's chain already
+ supplies it. Without this, a mixin without an explicit per-property
declaration would silently
+ pick up the framework's `DefaultConfig` default *after* the host's explicit
value in the
+ combined parent-walk — i.e. mixins would shadow host overrides for any
property they didn't
+ declare themselves. The skip is invisible to user code; it just makes "no
declaration on the
+ mixin" actually inherit from the host.
+- **New canonical `RestContext.Args` constructor.** The legacy 8-arg
back-compat constructor was
+ removed; the canonical 9-arg form now takes an explicit `mixinContext` flag
(host=`false`,
+ mixin sub-context=`true`). No external callers exist; all internal call
sites in
+ `juneau-rest-server`, `juneau-rest-mock`, and `juneau-commons` were updated.
+
#### Runtime-Overridable `@Rest(paths=...)` Resolution Chain (TODO-73)
The `paths` array on `@Rest` is now a *default* in a three-rung
**runtime-override resolution
diff --git a/pages/topics/10.07a.RestServerComposition.md
b/pages/topics/10.07a.RestServerComposition.md
index c049be2e72..d409774938 100644
--- a/pages/topics/10.07a.RestServerComposition.md
+++ b/pages/topics/10.07a.RestServerComposition.md
@@ -6,10 +6,13 @@ slug: RestServerCompositionMixinsAndPaths
Juneau REST servers ship two complementary composition / mounting primitives
on the class-level
[`@Rest`](/site/apidocs/org/apache/juneau/rest/annotation/Rest.html)
annotation:
-- **`@Rest(mixins=Class<?>[])`** — *composition without inheritance.* Graft
every
+- **`@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.
+ Local methods on the importing resource win on path/method collisions.
Starting in 9.5.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](/docs/topics/RestServerMixinSubContexts) for the
inheritance model.
- **`@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
@@ -329,14 +332,32 @@ mixin's own.
A few intentional limitations to be aware of.
-### Mixin scope is `@RestOp` methods only
+### Mixin scope and per-mixin RestContext (since 9.5.0)
-The mixin walk grafts `@RestOp`-group **methods** from the mixin class. It
does **not** inherit:
+The mixin walk grafts `@RestOp`-group **methods** from the mixin class. 9.5.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](/docs/topics/RestServerMixinSubContexts#guard-inheritance-security-note)
+ topic for the security rationale.
+- `@Rest(callLogger=..., debugEnablement=..., debugDefault=...)` — per-mixin
override that wins
+ over the host's value for mixin endpoints.
+- `@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](/docs/topics/RestServerMixinSubContexts) topic.
+
+The mixin walk still does **not** inherit:
-- The mixin's class-level `@Rest(serializers=..., parsers=..., encoders=...,
guards=..., converters=..., ...)`
- configuration. The importing resource's `@Rest` config applies to mixin
operations — including
- serializers, parsers, content negotiation, response processors, error
handling, guards,
- converters, and so on.
- 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
@@ -350,8 +371,11 @@ The mixin walk grafts `@RestOp`-group **methods** from the
mixin class. It does
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 mixin's full config, the right tool is `@Rest(children=...)`
(or an explicit
-servlet mount via `@Rest(paths=...)`), not `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](/docs/topics/RestServerMixinSubContexts#mixin-vs-child-divergence)
+for the rationale.
### `paths` is exact-match only
@@ -515,12 +539,16 @@ own Swagger / OpenAPI scope.
## See also
+- [Mixin Sub-Contexts](/docs/topics/RestServerMixinSubContexts) — the
per-mixin `RestContext`
+ inheritance model, `noInherit` opt-out, hook dual-firing, flat-inheritance
rule, and
+ mixin-vs-child divergence.
- [Health / Readiness / Liveness Probes](/docs/topics/HealthProbes) — the
canonical consumer of
both primitives, with full Option A (mixin) and Option B (standalone
auto-mount) examples.
- [@Rest-Annotated Class Basics](/docs/topics/RestAnnotatedClassBasics) — the
broader `@Rest`
attribute reference.
- [Child Resources](/docs/topics/ChildResources) — the alternative composition
primitive when
- you want a separate `RestContext` and bean store per addon.
+ you want a separate `RestContext` and bean store per addon, isolated from
the host's
+ resolution chain.
- [Path Patterns](/docs/topics/PathPatterns) — operation-level path matching
(the layer mixin
routes are grafted *into*).
- [REST Server Overview](/docs/topics/RestServerOverview) — where these
primitives sit in the
diff --git a/pages/topics/10.07b.RestServerMixinSubContexts.md
b/pages/topics/10.07b.RestServerMixinSubContexts.md
new file mode 100644
index 0000000000..e6f5bce39e
--- /dev/null
+++ b/pages/topics/10.07b.RestServerMixinSubContexts.md
@@ -0,0 +1,404 @@
+---
+title: "REST Server — Mixin Sub-Contexts"
+slug: RestServerMixinSubContexts
+---
+
+`@Rest(mixins=...)` got a structural upgrade in 9.5.0. The user-facing API is
unchanged
+(`@Rest(mixins=Foo.class, Bar.class)` still grafts the listed classes'
`@RestOp` methods into the
+importing resource's operation tree), but the *implementation* now elevates
each mixin from a
+"method library absorbed into the host's `RestContext`" to an "embedded
sub-resource with its own
+`RestContext` parent-linked to the host's."
+
+Pre-9.5.0, mixin endpoints resolved serializers, parsers, guards, hooks, etc.
through the host's
+single `RestContext`. There was no place for mixin-scoped configuration — the
mixin's
+class-level `@Rest(serializers=..., guards=..., ...)` declarations were
silently ignored. Starting
+in 9.5.0, each mixin owns its own `RestContext` whose `parentContext` points
at the host's, and
+inheritance walks parent → mixin per property. The mixin can append to or
replace each
+contribution list, and the framework's `@Rest(noInherit={...})` opt-out blocks
inheritance per
+property.
+
+> This is the *opposite* design choice from `@Rest(children=...)`. Children are
+> deliberately isolated from the parent's resolution chain. Mixins inherit.
The divergence is
+> intentional — see [Mixin-vs-child divergence](#mixin-vs-child-divergence)
below.
+
+This topic covers the inheritance model, the `noInherit` opt-out, hook
dual-firing, the
+flat-inheritance rule for transitive mixins, per-mixin overrides, the new
+`Messages.chain(...)` seam, and the guard inheritance security default.
+
+## What changed and why
+
+Pre-9.5.0 mixins were "method libraries absorbed into the host's
`RestContext`." Mixin classes
+that declared `@Rest(serializers=..., guards=..., hooks=..., callLogger=...,
...)` got those
+contributions silently ignored — every mixin endpoint resolved through the
host's resolver chain
+with no place for mixin-scoped overrides. 9.5.0 promotes each mixin to "an
embedded sub-resource,
+each with their own `RestContext` parent-linked to the host." Mixin endpoints
now resolve through
+the mixin's `RestContext`, which inherits the host's contributions (parent
walk) and optionally
+appends or replaces them.
+
+The rationale, quoted from the resolved-decisions log on the work item that
drove this redesign:
+
+> "Children are completely independent from parents — they're
externally-mounted at their own URL
+> namespace, often have their own deployment lifecycle (factored into separate
jars, etc.), and
+> conflating their resolution with the host's would surprise authors who treat
children as
+> standalone REST endpoints that happen to be discovered through a parent.
Mixins are different:
+> they're explicitly *inline*, share the host's URL namespace, and exist to
compose with the
+> host. Inheritance is the right model for mixins; isolation is the right
model for children."
+
+That divergence is the heart of the 9.5.0 mixin model. The rest of this page
is the user-facing
+reference for what inherits, what doesn't, and how to opt out.
+
+## The host-to-mixin inheritance model
+
+The canonical motivating use case is YAML serialization for OpenAPI documents.
A REST app's
+`/items` endpoint should not advertise `application/yaml` in its
`Content-Type` negotiation, but
+its `/openapi.yaml` endpoint must serve YAML. With mixin sub-context
inheritance, the YAML
+serializer can live on the mixin alone:
+
+```java
+// Mixin: declares a YamlSerializer that applies only to mixin endpoints.
+@Rest(
+ paths = {"/openapi", "/openapi.json", "/openapi.yaml"},
+ serializers = {YamlSerializer.class}
+)
+public class OpenApiResource { // (illustrative — full BasicOpenApiResource
lands via TODO-74)
+ @RestGet(path="/openapi.yaml")
+ public OpenApi yaml(RestRequest req) { return req.getOpenApi(); }
+ // ... /openapi and /openapi.json handlers ...
+}
+
+// Host: a vanilla RestServlet, no YAML declared.
+@Rest(mixins = OpenApiResource.class)
+public class ApiResource extends RestServlet {
+ @RestGet("/items")
+ public List<Item> items() { ... }
+}
+```
+
+After construction, the resolved serializer sets look like this:
+
+| Context | Resolved serializers
|
+|----------------------------------|----------------------------------------------|
+| `ApiResource` `RestContext` | host defaults (JSON, XML, HTML, …)
|
+| `OpenApiResource` mixin sub-ctx | host defaults (JSON, XML, HTML, …) +
`YamlSerializer` |
+
+Concrete request dispatch:
+
+| Request | Resolves through
| Result |
+|------------------------------------------------------|-------------------------|---------------------------------------|
+| `GET /items` | host context
| JSON/XML/HTML negotiation as normal |
+| `GET /items` `Accept: application/yaml` | host context
| 406 Not Acceptable (host has no YAML) |
+| `GET /openapi.yaml` | mixin sub-context
| 200, YAML body |
+| `GET /openapi` `Accept: application/yaml` | mixin sub-context
| 200, YAML body |
+
+The mixin's `@Rest(serializers={YamlSerializer.class})` declaration *appends*
to the inherited
+host set, so the mixin sub-context sees the full host serializer chain plus
YAML. The host's
+chain is unaffected — `/items` doesn't grow a YAML serializer just because
YAML appears on a
+mixin.
+
+## The contribution lists that inherit
+
+The Phase 2 inheritance walk extends
`RestContext.getRestAnnotationsForProperty(name)` to prepend
+the parent's `@Rest` chain onto the mixin's own. That single change cascades
to every
+contribution list because the `*Builder` memoizers and configuration accessors
on `RestContext`
+all read through `getRestAnnotationsForProperty(...)`. The framework verifies
inheritance for the
+following thirteen contribution lists in
`juneau-utest/src/test/java/org/apache/juneau/rest/mixin/`
+(one regression test class per list):
+
+| `@Rest` property | Inheritance behavior
| `noInherit` token |
+|--------------------------|---------------------------------------------------------------------------------|-------------------------|
+| `serializers` | Host first, then mixin appended. Mixin endpoint
sees union. | `"serializers"` |
+| `parsers` | Host first, then mixin appended. Mixin endpoint
sees union. | `"parsers"` |
+| `encoders` | Host first, then mixin appended. Mixin endpoint
sees union. | `"encoders"` |
+| `partSerializer` | Mixin overrides host if declared; else inherits
host's resolved value. | `"partSerializer"` |
+| `partParser` | Mixin overrides host if declared; else inherits
host's resolved value. | `"partParser"` |
+| `converters` | Host first, then mixin appended. Mixin endpoint
runs both chains. | `"converters"` |
+| `responseProcessors` | Host first, then mixin appended. Mixin endpoint
runs both chains. | `"responseProcessors"` |
+| `restOpArgs` | Host first, then mixin appended. Mixin can add
custom arg resolvers. | `"restOpArgs"` |
+| `guards` | Host first, then mixin appended.
**Inherit-by-default for security.** | `"guards"` |
+| `callLogger` | Mixin overrides host if declared; else inherits
host's resolved value. | `"callLogger"` |
+| `debugEnablement` | Mixin overrides host if declared; else inherits
host's resolved value. | `"debugEnablement"` |
+| `debugDefault` | Mixin overrides host if declared; else inherits
host's resolved value. | `"debugDefault"` |
+| `messages` | Mixin's bundle chained as child of host's via
`Messages.chain(...)`. | `"messages"` |
+
+"Host first, then mixin appended" means the mixin endpoint's resolver chain is
the concatenation
+of the host's and the mixin's, in that order. For *list-shaped* properties
(serializers,
+parsers, encoders, converters, response processors, REST op args, guards), the
chain produced is
+the union — useful for accumulating contributions. For *replace-shaped*
properties (partSerializer,
+partParser, callLogger, debugEnablement, debugDefault), the mixin's value
(when present) wins
+over the host's because the walk uses last-write-wins on the property's
annotation chain.
+
+Tokens in the `noInherit` column are the exact case-insensitive strings to put
in
+`@Rest(noInherit={...})` on the mixin to cut off inheritance for that property
— semantics
+identical to the framework's existing `@RestOp(noInherit={...})` and
`@Rest(noInherit={...})`
+machinery on the host's own class chain.
+
+## Hook dual-firing
+
+Lifecycle hooks (`@RestStartCall`, `@RestEndCall`, `@RestPreCall`,
`@RestPostCall`, and
+`@RestDestroy`) participate in the mixin model with a strict ordering contract:
+
+1. **Host-endpoint request.** Only host hooks fire. The mixin's hook chains
are not visited.
+2. **Mixin-endpoint request.** Host hooks fire first; then the mixin's hooks
fire. Hooks declared
+ on the host's class always run before hooks declared on the mixin's class,
even for requests
+ that land on mixin endpoints. The dispatcher walks the parent context's
invoker list before
+ the mixin's.
+3. **Application shutdown.** `@RestDestroy` fires on every context — host
first, then each
+ mixin's destroy chain in mixin-discovery order.
+
+The "host first" rule is the natural consequence of the parent-walk model:
from a mixin
+sub-context's point of view, the host context is its parent, and parent hooks
run before the
+local class's hooks (consistent with how the framework has always handled
`@RestStartCall` on a
+parent class vs. a subclass).
+
+Putting it together, a `GET /openapi.yaml` request against the example above
traces this
+sequence:
+
+1. `ApiResource`'s `@RestStartCall` methods (host's start-call invoker list).
+2. `OpenApiResource`'s `@RestStartCall` methods (mixin's start-call invoker
list).
+3. Host's `@RestPreCall` methods.
+4. Mixin's `@RestPreCall` methods.
+5. The matched `@RestGet` operation method on `OpenApiResource`.
+6. Mixin's `@RestPostCall` methods.
+7. Host's `@RestPostCall` methods.
+8. Mixin's `@RestEndCall` methods.
+9. Host's `@RestEndCall` methods.
+
+(`@RestPreCall` is host-then-mixin to match the start-call ordering.
`@RestEndCall` and
+`@RestPostCall` reverse the order — mixin-then-host — so unwinds match
initialization, like
+nested try/finally blocks.)
+
+## The `noInherit` opt-out per property
+
+Every contribution list inherits by default. `@Rest(noInherit={...})` on the
mixin's class is the
+opt-out — the token set is the same as the property names in the table above,
and the framework's
+existing `noInherit` machinery handles them uniformly. Three worked examples:
+
+**`noInherit={"serializers"}`** — the mixin owns its serializer chain, no host
inheritance.
+
+```java
+// Octet-stream-only "raw blob" mixin: only mixin's serializers, no host
JSON/XML/HTML.
+@Rest(
+ noInherit = {"serializers", "parsers"},
+ serializers = {OctetStreamSerializer.class},
+ parsers = {OctetStreamParser.class}
+)
+public class RawBlobResource {
+ @RestPut(path="/blob") public void put(byte[] body) { ... }
+ @RestGet(path="/blob") public byte[] get() { ... }
+}
+```
+
+Mixin endpoints emit and accept `application/octet-stream` exclusively. Host
endpoints retain
+whatever serializer chain they had.
+
+**`noInherit={"guards"}`** — the mixin opts out of host guard protection.
+
+```java
+// Health probe mixin: explicitly unguarded even if host has BearerTokenGuard.
+@Rest(
+ paths = {"/healthz", "/readyz", "/livez"},
+ noInherit = {"guards"}
+)
+public class HealthResource extends BasicHealthResource { ... }
+```
+
+Without `noInherit={"guards"}`, the host's `BearerTokenGuard` would also
protect `/healthz` — the
+inherit-by-default behavior is the security-conservative choice (see [Guard
inheritance security
+note](#guard-inheritance-security-note) below).
+
+**`noInherit={"messages"}`** — the mixin owns its message bundle, no host
fallthrough.
+
+```java
+@Rest(
+ messages = "MixinMessages",
+ noInherit = {"messages"}
+)
+public class MixinWithIsolatedMessages { ... }
+```
+
+Mixin's `$L{key}` lookups resolve from `MixinMessages.properties` only — they
do not fall through
+to the host's bundle. Without `noInherit`, the mixin's bundle is chained as a
child of the host's
+via `Messages.chain(...)` (see [Messages.chain(...) as the public
seam](#messageschain-as-the-public-seam)
+below) so any key absent from the mixin's bundle resolves through the host's.
+
+## Flat inheritance for transitive mixins
+
+A mixin can declare `@Rest(mixins=...)` of its own. The framework discovers
the transitive
+closure and registers every reachable mixin against the host. **All discovered
mixins
+parent-link directly to the host** — never to a transitive predecessor. If `A`
mixes in `B`,
+both `A` and `B` end up with `parentContext = host`. They do *not* form an `A
→ B` inheritance
+chain.
+
+```java
+@Rest(serializers=YamlSerializer.class)
+public class B {
+ @RestGet(path="/b") public String b() { return "b"; }
+}
+
+@Rest(mixins=B.class, serializers=ProtobufSerializer.class)
+public class A {
+ @RestGet(path="/a") public String a() { return "a"; }
+}
+
+@Rest(mixins=A.class, serializers=XmlSerializer.class)
+public class HostResource extends BasicRestServlet { ... }
+```
+
+Resolved serializer sets:
+
+| Context | Parent | Resolved serializer chain
|
+|------------------------------|-------------|------------------------------------------------------|
+| `HostResource` (host) | (none) | `XmlSerializer`
|
+| `A` mixin sub-ctx | host | `XmlSerializer`,
`ProtobufSerializer` |
+| `B` mixin sub-ctx | host | `XmlSerializer`,
`YamlSerializer` |
+
+Notice the flat structure: `B`'s sub-context inherits from the host
(`XmlSerializer`), not from
+`A` (no `ProtobufSerializer`). The rationale, again from the
resolved-decisions log:
+
+> "Predictable: a mixin always inherits from 'the host,' period.
Order-independent: the discovery
+> order of A and B doesn't matter for B's resolution. Composable: a mixin can
be added or removed
+> without rearranging inheritance chains."
+
+If a use case for cross-mixin inheritance ever emerges, it'll be modeled as an
explicit
+mechanism (some new annotation member) rather than by reshaping the discovery
walk. The
+flat-inheritance rule is load-bearing for the v1 model.
+
+## Per-mixin overrides for host settings
+
+Because each mixin owns its own `RestContext`, it can declare any host setting
— including
+typically host-wide concerns like `callLogger`, `debugEnablement`, and
`guards` — and the
+override applies *only* to that mixin's endpoints. The host's resolution chain
is unaffected.
+
+```java
+// Host: vanilla call-logging, no admin protection.
+@Rest(
+ mixins = AdminResource.class,
+ callLogger = HostCallLogger.class
+)
+public class ApiResource extends BasicRestServlet {
+ @RestGet("/items") public List<Item> items() { ... }
+}
+
+// Mixin: structured JSON logging, BearerToken-only.
+@Rest(
+ paths = {"/admin/threads", "/admin/heap"},
+ callLogger = StructuredJsonLogger.class,
+ debugEnablement = AdminDebugEnablement.class,
+ guards = AdminBearerGuard.class
+)
+public class AdminResource {
+ @RestGet(path="/admin/threads") public String threads() { ... }
+ @RestGet(path="/admin/heap") public String heap() { ... }
+}
+```
+
+Concrete observability:
+
+| Request | callLogger | debugEnablement
| Guard chain |
+|------------------------|-------------------------|--------------------------|-------------------------------------------------------|
+| `GET /items` | `HostCallLogger` | host's default
| host's (none in this example) |
+| `GET /admin/threads` | `StructuredJsonLogger` | `AdminDebugEnablement`
| host's (none) + `AdminBearerGuard` (inherited+appended) |
+
+The admin mixin's override is scoped to admin endpoints — it doesn't bleed
back into the host's
+`/items` logging or debugging. If the mixin wanted *only* its own logger (no
inheritance), it
+would add `noInherit={"callLogger"}` (though for callLogger and the other
replace-shaped
+properties, "declare an explicit value on the mixin" already wins over the
parent walk — `noInherit`
+is mostly meaningful for the list-shaped properties where inheritance produces
a union).
+
+## `Messages.chain(...)` as the public seam
+
+`@Rest(messages=...)` doesn't ride the standard
`getRestAnnotationsForProperty(...)` walk —
+`Messages` has its own ResourceBundle-based parent-chain mechanism that
pre-dates this work item.
+To keep mixin-Messages inheritance working without a special-case in
`Messages.Builder`, 9.5.0
+introduces a new static factory on `Messages`:
+
+```java
+public static Messages chain(Messages child, Messages parent)
+```
+
+`Messages.chain(child, parent)` returns a new parent-chained bundle whose
lookups read `child`
+first and fall through to `parent` for any key not found in `child`'s chain.
Neither input is
+mutated: the factory deep-copies `child`'s existing `parent2` chain, splicing
`parent` in at the
+deepest position. The new chain shares the inner `ResourceBundle` instances
with the inputs
+(read-only, so safe).
+
+The mixin's `RestContext.messages` memoizer uses it like this:
+
+```java
+var local = build the mixin's own Messages bundle from @Rest(messages=...);
+return Messages.chain(local, hostContext.getMessages());
+```
+
+That single line makes the mixin's bundle inherit from the host's.
`@Rest(noInherit={"messages"})`
+on the mixin skips the wrap so the mixin sees its bundle in isolation.
+
+The factory is a general-purpose composition primitive — user code that needs
to thread one
+pre-built `Messages` bundle through another (a CMS-style overlay of per-tenant
strings on top of
+app-wide strings, for example) can call `Messages.chain(...)` directly without
going through the
+mixin machinery.
+
+## Mixin-vs-child divergence
+
+**Mixins inherit from the host. Children do not.** This is the central design
decision of the
+9.5.0 mixin model, and it's the one to internalize before reaching for either
primitive. From
+the resolved-decisions log:
+
+> "Child resources tend to be completely independent from parents — they're
externally-mounted at
+> their own URL namespace, often have their own deployment lifecycle (factored
into separate
+> jars, etc.), and conflating their resolution with the host's would surprise
authors who treat
+> children as standalone REST endpoints that happen to be discovered through a
parent. Mixins
+> are different: they're explicitly *inline*, share the host's URL namespace,
and exist to
+> compose with the host. Inheritance is the right model for mixins; isolation
is the right model
+> for children."
+
+A quick checklist:
+
+| Use a mixin (`@Rest(mixins=...)`) when… |
Use a child (`@Rest(children=...)`) when… |
+|--------------------------------------------------------------------------|------------------------------------------------------------------------|
+| The addon lives inline at the host's URL namespace (`/healthz` next to
`/items`). | The addon owns its own URL subtree (`/admin/*` under `/`). |
+| The addon should inherit the host's serializer / guard / hook chain. |
The addon should be isolated from the host's resolution chain. |
+| The addon is a lightweight composer (small set of `@RestOp` methods). |
The addon is a heavyweight independent resource (own DB, own lifecycle). |
+| You want per-addon overrides scoped to the addon's endpoints. |
You want per-resource isolation between addon and host. |
+
+Most "I want to add probes / version endpoint / favicon / OpenAPI docs to my
API" use cases are
+mixins. Most "I want to mount an unrelated REST surface under the same
servlet" use cases are
+children. The divergence is not deferred — it's the intentional v1 model.
+
+## Guard inheritance security note
+
+Guards inherit by default. This is a deliberate "fail strict" choice. The
resolved-decisions log
+explains:
+
+> "Surprises around security defaults should err on 'too strict' (mixin
endpoint accidentally
+> protected) not 'too loose' (mixin endpoint accidentally exposed). A
`BearerTokenGuard` on the
+> host automatically protects mixin endpoints unless the mixin explicitly opts
out."
+
+Concretely: if the host declares `@Rest(guards=BearerTokenGuard.class)`, every
mixin endpoint on
+that host is also protected by `BearerTokenGuard` — even if the mixin's class
declares no guards
+itself. The only way to expose a mixin endpoint without the host's guard chain
is to declare
+`@Rest(noInherit={"guards"})` on the mixin's class.
+
+`BasicHealthResource` is the canonical example of a deliberately-unguarded
mixin: probes
+(`/healthz`, `/readyz`, `/livez`) need to be reachable by the load balancer /
orchestrator even
+when the rest of the API requires authentication. `BasicHealthResource`
declares
+`@Rest(noInherit={"guards"})` so its endpoints stay unguarded regardless of
what guard chain the
+host carries.
+
+The asymmetry — host adds a guard, mixin endpoints become protected unless the
mixin opts out —
+is intentional. Accidentally protected is recoverable (the user sees a
401/403, the mixin
+author adds `noInherit`); accidentally exposed is not (the endpoint is
reachable to attackers
+before anyone notices).
+
+## See also
+
+- [REST Server — Mixins and Multi-Mount
Paths](/docs/topics/RestServerCompositionMixinsAndPaths)
+ — the broader `@Rest(mixins=...)` / `@Rest(paths=...)` reference. This topic
page is the
+ sub-context-and-inheritance follow-on.
+- [Child Resources](/docs/topics/ChildResources) — the alternative composition
primitive when
+ isolation (not inheritance) is what you want.
+- [Health / Readiness / Liveness Probes](/docs/topics/HealthProbes) — the
canonical
+ `noInherit={"guards"}` mixin in the codebase.
+- [REST Server — Logging and
Debugging](/docs/topics/RestServerLoggingAndDebugging) —
+ per-mixin `callLogger` / `debugEnablement` override patterns.
+- [REST Server — SVL Variables](/docs/topics/RestServerSvlVariables) — context
for the
+ `$L{key}` variable referenced from the Messages section.