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 6fe7ecce24 docs: TODO-72 mixins/paths topic page + 9.5 release notes +
sidebar (incl. 10.20b conditional-GET + 15.10 health probes)
6fe7ecce24 is described below
commit 6fe7ecce24506f7e79afc1272662c201d601f19f
Author: James Bognar <[email protected]>
AuthorDate: Sat May 23 09:27:04 2026 -0400
docs: TODO-72 mixins/paths topic page + 9.5 release notes + sidebar (incl.
10.20b conditional-GET + 15.10 health probes)
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/9.5.0.md | 98 ++++++
pages/topics/10.07a.RestServerComposition.md | 389 ++++++++++++++++++++++++
pages/topics/10.20b.RestServerConditionalGet.md | 232 ++++++++++++++
pages/topics/15.10.HealthProbes.md | 156 ++++++++++
pages/topics/23.01.V9.5-migration-guide.md | 6 +
sidebars.ts | 15 +
6 files changed, 896 insertions(+)
diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index bf119275b1..36a707736a 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -1986,6 +1986,39 @@ String name
### juneau-rest-server
+#### Health Probe SPI + Resource (TODO-65)
+
+`juneau-rest-server` now includes a built-in probe SPI and aggregation
resource under
+`org.apache.juneau.rest.health`:
+
+- `HealthIndicator` functional SPI (`Health check()`).
+- `Health` value type with static builders: `Health.up(name)`,
`Health.down(name, throwable)`,
+ `Health.unknown(name)`, plus structured detail entries.
+- `HealthProbe` / `HealthStatus` enums for probe categorization and aggregate
state.
+- `BasicHealthResource` exposing `/healthz`, `/readyz`, and `/livez` with
aggregate body format:
+ `{status, components:{name:{status,details}}}`.
+- HTTP status mapping follows Kubernetes conventions: `503` if any component
is `DOWN`, else `200`.
+- `HealthProbeSettings` introduces per-indicator timeout control (default
`1s`); exceptions/timeouts
+ are converted to `DOWN` with `details.error`.
+
+#### `@Rest(mixins=...)` Composition + `@Rest(paths=...)` Multi-Mount
+
+See [REST Server — Mixins and Multi-Mount
Paths](/docs/topics/RestServerCompositionMixinsAndPaths) for the full reference.
+
+The health-probe follow-up adds two new `@Rest` composition/mounting
primitives:
+
+- **`mixins`** (`Class<?>[]`) — graft `@RestOp`-group methods from mixin
classes into a resource's
+ operation tree. Local methods on the importing resource win on path
collisions.
+- **`paths`** (`String[]`) — optional multi-mount path specs for top-level
servlet registration.
+ Jetty mounts a single servlet instance at each declared path.
+
+`BasicHealthResource` now declares
`@Rest(paths={"/healthz","/readyz","/livez"})` instead of
+`@Rest(path="/")`, eliminating startup collisions with apps whose root
resource already owns `/*`.
+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).
+
#### OpenAPI 3.1 Emission + `apiFormat` Knob (TODO-63)
`juneau-rest-server` now ships first-class OpenAPI 3.1 document generation
alongside the existing Swagger v2 path:
@@ -2327,6 +2360,53 @@ public class AccountResource {
See [REST Server — RFC 7807 Problem
Details](/docs/topics/RestServerProblemDetails) for the full topic, including
worked examples for the resource-level opt-in, the per-op fanout, the
`ProblemException` throw path, and `ProblemMapper` registration.
+#### Conditional-GET / ETag helpers
+
+`juneau-rest-server` now ships an RFC 7232 conditional-request layer so
handlers no longer need to hand-roll `ETag` / `Last-Modified` / `If-None-Match`
/ `If-Match` plumbing. The wiring is purely additive — existing handlers
continue to work unchanged.
+
+##### Response-side setters on `RestResponse`
+
+Six fluent setters on <a
href="/site/apidocs/org/apache/juneau/rest/RestResponse.html"
target="_blank">RestResponse</a>:
+
+- `eTag(String)` / `eTag(EntityTag)` — sets the `ETag` response header. The
string form delegates to `EntityTag.of(...)` so quoting (strong: `"v1"`; weak:
`W/"v1"`) is validated up front.
+- `lastModified(Instant)` / `lastModified(ZonedDateTime)` — sets the
`Last-Modified` response header in RFC 7231 IMF-fixdate format. Values are
truncated to second precision and converted to UTC.
+- `cacheControl(String)` / `cacheControl(CacheControlBuilder)` — sets the
`Cache-Control` response header. The builder form gives a typed API for the
standard directives.
+
+##### Typed `Cache-Control` builder
+
+New <a
href="/site/apidocs/org/apache/juneau/http/header/CacheControlBuilder.html"
target="_blank">CacheControlBuilder</a> in `juneau-rest-common` offers a
typo-resistant fluent API over the directive set: `publicCache()` /
`privateCache()`, `noCache()` / `noStore()` / `noTransform()`,
`mustRevalidate()` / `proxyRevalidate()`, `immutable()`, `maxAge(long)` /
`maxAge(Duration)`, `sMaxAge(...)`, `staleWhileRevalidate(...)`,
`staleIfError(...)`, and `extension(String)` for forward-compat di [...]
+
+##### Request-side `checkPreconditions(RestResponse)`
+
+New <a
href="/site/apidocs/org/apache/juneau/rest/RestRequest.html#checkPreconditions-org.apache.juneau.rest.RestResponse-"
target="_blank">RestRequest.checkPreconditions(RestResponse)</a> evaluates the
four conditional request headers against the `ETag` / `Last-Modified` already
set on the response, and returns an `Optional<BasicHttpException>`:
+
+- Empty → no precondition matched and the handler may proceed.
+- Non-empty → the contained exception (a `BasicHttpException` carrying `304
Not Modified` or `412 Precondition Failed`) should be thrown to short-circuit.
+
+The implementation follows RFC 7232 §6 ordering exactly: `If-Match` →
`If-Unmodified-Since` → `If-None-Match` → `If-Modified-Since`. ETag matching
honors RFC 7232 §2.3.2 (strong comparison for `If-Match`; weak comparison for
`If-None-Match`). `If-Modified-Since` is only consulted on `GET` / `HEAD`.
Malformed dates are silently ignored per Postel's law.
+
+##### Typical handler shape
+
+```java
+@RestGet("/orders/{id}")
+public Order get(@Path long id, RestRequest req, RestResponse res) {
+ var order = repo.find(id);
+ res.eTag("\"" + order.version() + "\"")
+ .lastModified(order.updated())
+
.cacheControl(CacheControlBuilder.create().publicCache().maxAge(60).build());
+ req.checkPreconditions(res).ifPresent(e -> { throw e; });
+ return order;
+}
+```
+
+##### Notes
+
+- The response's `ETag` / `Last-Modified` headers **must be set before**
`checkPreconditions(...)` is called — the check reads from the already-set
response headers so it can compare against the values the handler intends to
send.
+- `If-Match` always uses strong comparison: a `W/"v1"` response ETag never
matches an `If-Match`, even if the opaque tags are identical.
+- `If-None-Match` uses weak comparison: `"v1"` and `W/"v1"` both match. The
wildcard `*` matches when any ETag is set on the response.
+
+See [REST Server — Conditional-GET / ETag
Helpers](/docs/topics/RestServerConditionalGet) for the full topic, including
the 16-cell precedence matrix, weak/strong ETag rules, and a worked round-trip
example.
+
### juneau-rest-client
#### REST session option wire helpers
@@ -2452,6 +2532,24 @@ See <a
href="/docs/topics/MicroserviceCoreInject">Inject-Aware Microservice</a>
### juneau-microservice-jetty
+#### `HealthProbeConfiguration` (TODO-65)
+
+`juneau-microservice-jetty` now ships `HealthProbeConfiguration`, an opt-in
`@Configuration` module
+that contributes:
+
+- `@Bean Servlet healthProbeServlet()` returning `BasicHealthResource` for
auto-mount through
+ `JettyServerComponent`.
+- `@Bean HealthProbeSettings` defaulting to a `1s` indicator timeout
(overridable via user bean).
+
+End-state bootstrap:
+
+```java
+Microservice.create()
+ .configurations(JettyConfiguration.class, HealthProbeConfiguration.class,
AppConfig.class)
+ .build()
+ .start();
+```
+
#### `JettyMicroservice` Replaced by `JettyConfiguration` (TODO-36,
**BREAKING**)
The `JettyMicroservice` subclass, `JettyMicroserviceListener` interface, and
`BasicJettyMicroserviceListener`
diff --git a/pages/topics/10.07a.RestServerComposition.md
b/pages/topics/10.07a.RestServerComposition.md
new file mode 100644
index 0000000000..35cbaf3fd6
--- /dev/null
+++ b/pages/topics/10.07a.RestServerComposition.md
@@ -0,0 +1,389 @@
+---
+title: "REST Server — Mixins and Multi-Mount Paths"
+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
+ `@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.
+- **`@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-9.5.0
+behavior unchanged. They are also not mutually exclusive: a single class can
declare both at once
+(`BasicHealthResource` ships with both supported), and downstream consumers
pick whichever
+deployment style fits their app.
+
+## Motivation
+
+### Why composition?
+
+Java single-inheritance forces a binary choice when you want to pull in a
built-in resource such
+as `BasicHealthResource`:
+
+- Subclass it — and lose the ability to subclass your own framework type
(`BasicRestServlet`,
+ `BasicRestObjectGroup`, 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 `BasicHealthResource`
+
+The recommended way to wire Kubernetes probes into an existing root servlet:
+
+```java
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.health.*;
+import org.apache.juneau.rest.servlet.*;
+
+@Rest(
+ path="/",
+ mixins=BasicHealthResource.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:
+
+```java
+@Rest
+public class HealthMixin {
+ @RestGet(path="/same")
+ public String fromMixin() { return "mixin"; }
+}
+
+@Rest(mixins=HealthMixin.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:
+
+```java
+@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`:
+
+```java
+@Rest(paths={"/healthz","/readyz","/livez"})
+public class BasicHealthResource 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 `BasicHealthResource`
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. `BasicHealthResource` ships with
`@Rest(paths={"/healthz","/readyz","/livez"})`
+and is wired in via `HealthProbeConfiguration`:
+
+```java
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.apache.juneau.rest.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`| `BasicHealthResource` | `@Bean Servlet healthProbeServlet()` +
`@Rest(paths=...)` |
+| `/readyz` | `BasicHealthResource` | (same instance, second mapping) |
+| `/livez` | `BasicHealthResource` | (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 `BasicHealthResource`.
+
+### Single `ServletHolder`, multiple mappings
+
+Multi-mount uses **one** `ServletHolder` instance and registers it under each
path-spec:
+
+```java
+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 on the addon class. `BasicHealthResource` does this. |
+
+The two are not mutually exclusive. `BasicHealthResource` declares
+`@Rest(paths={"/healthz","/readyz","/livez"})` so it can be auto-mounted
standalone (Option B in
+the probe docs), and downstream apps can additionally pull it in via
+`@Rest(mixins=BasicHealthResource.class)` on their own root resource (Option
A). 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 is `@RestOp` methods only
+
+The mixin walk grafts `@RestOp`-group **methods** from the mixin class. It
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
+ 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 mixin's full config, the right tool is `@Rest(children=...)`
(or an explicit
+servlet mount via `@Rest(paths=...)`), not `mixins`.
+
+### `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.
+
+### 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.
+
+## See also
+
+- [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.
+- [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
+ overall request-routing pipeline.
diff --git a/pages/topics/10.20b.RestServerConditionalGet.md
b/pages/topics/10.20b.RestServerConditionalGet.md
new file mode 100644
index 0000000000..fc1bb2bdb7
--- /dev/null
+++ b/pages/topics/10.20b.RestServerConditionalGet.md
@@ -0,0 +1,232 @@
+---
+title: "Conditional-GET / ETag Helpers"
+slug: RestServerConditionalGet
+---
+
+Juneau REST servers ship a minimal but complete [RFC 7232 — Conditional
Requests](https://www.rfc-editor.org/rfc/rfc7232) helper layer so handlers no
longer need to hand-roll `ETag` / `Last-Modified` / `If-None-Match` /
`If-Match` plumbing. The feature couples three pieces:
+
+- A handful of fluent setters on <a
href="/site/apidocs/org/apache/juneau/rest/RestResponse.html"
target="_blank">RestResponse</a> — `eTag(...)`, `lastModified(...)`,
`cacheControl(...)`.
+- A typed <a
href="/site/apidocs/org/apache/juneau/http/header/CacheControlBuilder.html"
target="_blank">CacheControlBuilder</a> in `juneau-rest-common` for
typo-resistant `Cache-Control` directive construction.
+- A `checkPreconditions(...)` helper on <a
href="/site/apidocs/org/apache/juneau/rest/RestRequest.html"
target="_blank">RestRequest</a> that evaluates the four conditional request
headers in the exact order RFC 7232 §6 specifies, and returns an
`Optional<BasicHttpException>` carrying the right `304 Not Modified` or `412
Precondition Failed` for the caller to throw.
+
+The wiring is purely additive — existing handlers continue to work unchanged.
+
+## Motivation
+
+A textbook conditional-GET handler looks like this:
+
+```java
+@RestGet("/{id}")
+public Order get(@Path long id, RestRequest req, RestResponse res) {
+ var order = repo.find(id);
+ var tag = "\"" + order.version() + "\"";
+ res.eTag(tag).lastModified(order.updated());
+ req.checkPreconditions(res).ifPresent(e -> { throw e; });
+ return order;
+}
+```
+
+Without the helpers, every handler that wants conditional-GET support has to:
+
+1. Format `ETag` / `Last-Modified` headers correctly (quoting, IMF-fixdate,
UTC).
+2. Read four request headers (`If-Match`, `If-Unmodified-Since`,
`If-None-Match`, `If-Modified-Since`).
+3. Implement RFC 7232 §6 ordering (which header wins when more than one is
present).
+4. Implement RFC 7232 §2.3.2 ETag matching rules (strong vs weak comparison).
+5. Pick the right status code (`304` vs `412`) and short-circuit cleanly.
+
+That's a lot of fiddly RFC bookkeeping. The helpers compress all of it into a
one-line call.
+
+## Response-side setters on `RestResponse`
+
+Six fluent setters, all chainable:
+
+| Method | Sets | Notes |
+|-------------------------------------|---------------------|-------|
+| `eTag(String)` | `ETag` | Delegates to
`EntityTag.of(...)` so quoting (strong: `"v1"`; weak: `W/"v1"`) is validated up
front. |
+| `eTag(EntityTag)` | `ETag` | When the caller
already has a typed `EntityTag`. |
+| `lastModified(Instant)` | `Last-Modified` | Truncated to
second precision; formatted as RFC 7231 IMF-fixdate in UTC. |
+| `lastModified(ZonedDateTime)` | `Last-Modified` | Converted to UTC
before formatting. |
+| `cacheControl(String)` | `Cache-Control` | Raw header value
(e.g. `"public, max-age=3600"`). |
+| `cacheControl(CacheControlBuilder)` | `Cache-Control` | Typed builder
form — see below. |
+
+```java
+res.eTag("\"v42\"")
+ .lastModified(Instant.parse("2026-05-22T00:00:00Z"))
+ .cacheControl("public, max-age=60");
+```
+
+Produces:
+
+```http
+ETag: "v42"
+Last-Modified: Fri, 22 May 2026 00:00:00 GMT
+Cache-Control: public, max-age=60
+```
+
+## `CacheControlBuilder` — typed directive construction
+
+<a href="/site/apidocs/org/apache/juneau/http/header/CacheControlBuilder.html"
target="_blank">CacheControlBuilder</a> (in `juneau-rest-common`) is a
typo-resistant fluent API over the standard directive set:
+
+| Method | Directive |
+|-------------------------------------|---------------------------------|
+| `publicCache()` / `privateCache()` | `public` / `private` |
+| `noCache()` | `no-cache` |
+| `noStore()` | `no-store` |
+| `noTransform()` | `no-transform` |
+| `mustRevalidate()` | `must-revalidate` |
+| `proxyRevalidate()` | `proxy-revalidate` |
+| `immutable()` | `immutable` |
+| `maxAge(long)` / `maxAge(Duration)` | `max-age=N` |
+| `sMaxAge(long)` / `sMaxAge(Duration)` | `s-maxage=N` |
+| `staleWhileRevalidate(long)` | `stale-while-revalidate=N` |
+| `staleIfError(long)` | `stale-if-error=N` |
+| `extension(String)` | Arbitrary extension directive |
+
+Directives are emitted in a stable order — cacheability (`public` / `private`)
first, then boolean directives in source order (`no-cache` / `no-store` /
`no-transform` / `must-revalidate` / `proxy-revalidate` / `immutable`), then
numeric directives (`max-age` / `s-maxage` / `stale-while-revalidate` /
`stale-if-error`), then extensions. This makes snapshot-based testing
deterministic.
+
+```java
+res.cacheControl(CacheControlBuilder.create()
+ .publicCache()
+ .maxAge(Duration.ofMinutes(5))
+ .mustRevalidate()
+ .build());
+// → Cache-Control: public, must-revalidate, max-age=300
+```
+
+## Request-side `checkPreconditions(RestResponse)`
+
+<a
href="/site/apidocs/org/apache/juneau/rest/RestRequest.html#checkPreconditions-org.apache.juneau.rest.RestResponse-"
target="_blank">RestRequest.checkPreconditions(RestResponse)</a> evaluates the
four conditional request headers against the `ETag` / `Last-Modified` already
set on the response and returns an `Optional<BasicHttpException>`:
+
+| Return | Meaning
|
+|---------------------------------------|--------------------------------------------------------------------------|
+| `Optional.empty()` | No precondition matched — handler
may proceed. |
+| `Optional<BasicHttpException>` (304) | A `BasicHttpException` carrying `304
Not Modified` — throw to short-circuit. |
+| `Optional<BasicHttpException>` (412) | A `BasicHttpException` carrying `412
Precondition Failed` — throw to short-circuit. |
+
+The handler then chooses how to react:
+
+```java
+// Optional.ifPresent — short-circuit by throwing the carried exception:
+req.checkPreconditions(res).ifPresent(e -> { throw e; });
+
+// Or unwrap and re-shape the response yourself:
+var opt = req.checkPreconditions(res);
+if (opt.isPresent()) {
+ var ex = opt.get();
+ res.setStatus(ex.getStatusCode());
+ return null;
+}
+```
+
+### Ordering rule
+
+`checkPreconditions(...)` reads from the response's **already-set** `ETag` and
`Last-Modified` headers. Set those before calling the check, so the helper
compares against the values the handler intends to send:
+
+```java
+res.eTag("\"v42\"").lastModified(order.updated()); // 1. tag the response
first
+req.checkPreconditions(res).ifPresent(e -> { throw e; }); // 2. then check
+return order; // 3. handler
body
+```
+
+Setting the headers after the check produces stale comparisons.
+
+### RFC 7232 §6 precedence
+
+The helper applies the four conditional headers in the order RFC 7232 mandates:
+
+1. **`If-Match`** — strong comparison. On mismatch → `412 Precondition
Failed`. If present, `If-Unmodified-Since` is ignored.
+2. **`If-Unmodified-Since`** — only consulted when `If-Match` is absent. If
the resource's `Last-Modified` is strictly later than the supplied date → `412
Precondition Failed`.
+3. **`If-None-Match`** — weak comparison. On match → `304 Not Modified` (for
safe methods) or `412 Precondition Failed` (for unsafe methods). If present,
`If-Modified-Since` is ignored.
+4. **`If-Modified-Since`** — only consulted when `If-None-Match` is absent
**and** the method is `GET` or `HEAD`. If the resource's `Last-Modified` is not
strictly later than the supplied date → `304 Not Modified`.
+
+### ETag matching rules
+
+| Comparison | Used by | Rule
|
+|-------------------|---------------------|---------------------------------------------------------------------|
+| **Strong** | `If-Match` | Both ETags must be strong (no `W/`
prefix) and have identical opaque tags. A weak response ETag never matches an
`If-Match`. |
+| **Weak** | `If-None-Match` | Tags match if their opaque parts
are identical, regardless of weak/strong markers. |
+| **Wildcard `*`** | both | `If-Match: *` matches when the
response has any ETag (else `412`). `If-None-Match: *` matches when any ETag is
set on the response (else passes through). |
+
+### Malformed dates
+
+`If-Modified-Since` / `If-Unmodified-Since` values that cannot be parsed as
HTTP-date are silently ignored — they are treated as if the header were absent.
This follows Postel's law and matches what mainstream servers (nginx, Apache
httpd, Tomcat) do in practice.
+
+## Worked example — round trip
+
+A trivial repository-backed resource that demonstrates the full lifecycle:
+
+```java
+@Rest(path="/orders")
+public class OrderResource {
+
+ @RestGet("/{id}")
+ public Order get(@Path long id, RestRequest req, RestResponse res) {
+ var order = repo.find(id);
+ res.eTag("\"" + order.version() + "\"")
+ .lastModified(order.updated())
+
.cacheControl(CacheControlBuilder.create().publicCache().maxAge(60).build());
+ req.checkPreconditions(res).ifPresent(e -> { throw e; });
+ return order;
+ }
+
+ @RestPut("/{id}")
+ public Order put(@Path long id, @Content Order in, RestRequest req,
RestResponse res) {
+ var current = repo.find(id);
+ res.eTag("\"" + current.version() +
"\"").lastModified(current.updated());
+ req.checkPreconditions(res).ifPresent(e -> { throw e; }); // 412 if
If-Match is stale
+ var next = repo.save(in.withVersion(current.version() + 1));
+ res.eTag("\"" + next.version() + "\"").lastModified(next.updated());
+ return next;
+ }
+}
+```
+
+Wire-level interaction:
+
+```http
+# 1. First GET — full response with ETag + Last-Modified
+GET /orders/1
+→ 200 OK
+ ETag: "1"
+ Last-Modified: Fri, 22 May 2026 00:00:00 GMT
+ Cache-Control: public, max-age=60
+
+# 2. Re-fetch using If-None-Match — short-circuits to 304
+GET /orders/1
+If-None-Match: "1"
+→ 304 Not Modified
+
+# 3. PUT with stale If-Match — rejected
+PUT /orders/1
+If-Match: "0"
+{ ... }
+→ 412 Precondition Failed
+
+# 4. PUT with correct If-Match — succeeds and bumps ETag
+PUT /orders/1
+If-Match: "1"
+{ ... }
+→ 200 OK
+ ETag: "2"
+ Last-Modified: Sat, 23 May 2026 00:00:00 GMT
+```
+
+## Scope notes
+
+The helpers cover the wire-level conditional-GET layer. Out of scope for this
release:
+
+- **Server-side response caching.** The helpers turn an `If-None-Match` match
into a `304`, but the handler still computes the resource. A separate
response-cache layer (compute-once / re-serve) is a future work item.
+- **Weak vs strong ETag policy.** <a
href="/site/apidocs/org/apache/juneau/http/header/EntityTag.html"
target="_blank">EntityTag</a> already models both via `isWeak()`; the helpers
honor whatever the caller produced.
+- **`Vary` header automation.** Callers still set `Vary` explicitly when
appropriate.
+- **`If-Range` / `206 Partial Content`.** Range-request support is a future
work item.
+
+## See also
+
+- [REST Server — HTTP Status Codes](/docs/topics/HttpStatusCodes) — the
framework defaults for non-OK statuses, including `304` and `412`.
+- [Java Method Throwable Types](/docs/topics/JavaMethodThrowableTypes) — what
exceptions Juneau handlers can throw and how they map to HTTP statuses.
+
+## Resources
+
+- [RFC 7232 — Conditional Requests](https://www.rfc-editor.org/rfc/rfc7232)
+- [RFC 7234 — Caching](https://www.rfc-editor.org/rfc/rfc7234) (obsoleted by
RFC 9111)
+- [RFC 9111 — HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)
diff --git a/pages/topics/15.10.HealthProbes.md
b/pages/topics/15.10.HealthProbes.md
new file mode 100644
index 0000000000..79d576ca55
--- /dev/null
+++ b/pages/topics/15.10.HealthProbes.md
@@ -0,0 +1,156 @@
+---
+title: "Health / Readiness / Liveness Probes"
+slug: HealthProbes
+---
+
+> **See also:** [REST Server — Mixins and Multi-Mount
Paths](/docs/topics/RestServerCompositionMixinsAndPaths) — the underlying
`@Rest(mixins=...)` and `@Rest(paths=...)` composition primitives this page is
built on.
+
+Starting with **9.5.0**, Juneau provides an opt-in probe surface for Jetty
microservices:
+
+- `GET /healthz`
+- `GET /readyz`
+- `GET /livez`
+
+Probe routing now has two supported integration styles:
+
+- **Preferred:** mix probe operations into your existing root resource with
+ `@Rest(mixins=BasicHealthResource.class)`.
+- **Fallback:** standalone probe servlet auto-mounted at explicit
`@Rest(paths={...})` path specs
+ (`/healthz`, `/readyz`, `/livez`) via `HealthProbeConfiguration`.
+
+This avoids root-path collisions when your app already owns `@Rest(path="/")`.
+
+The endpoints aggregate every `HealthIndicator` bean from the microservice
bean store and return an
+Actuator-style payload:
+
+```json
+{
+ "status": "UP",
+ "components": {
+ "dbHealth": {
+ "status": "UP",
+ "details": {
+ "validationQueryMs": 12
+ }
+ }
+ }
+}
+```
+
+HTTP status is:
+
+- **`200 OK`** when no component is `DOWN`.
+- **`503 Service Unavailable`** when any component is `DOWN`.
+
+## Quick start
+
+### Option A (recommended): mix into existing root resource
+
+```java
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.health.*;
+import org.apache.juneau.rest.servlet.*;
+
+@Rest(
+ path="/",
+ mixins=BasicHealthResource.class
+)
+public class RootResources extends BasicRestServlet {
+ @Bean(name="dbHealth")
+ 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)
+ .build()
+ .start()
+ .join();
+ }
+}
+```
+
+### Option B: standalone auto-mount (escape hatch)
+
+```java
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.apache.juneau.rest.health.*;
+
+import jakarta.servlet.*;
+
+@Configuration
+public class AppConfig {
+
+ @Bean
+ Servlet root() {
+ return new RootResources();
+ }
+
+ @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();
+ }
+}
+```
+
+`HealthProbeConfiguration` contributes a `Servlet` bean
(`BasicHealthResource`) and Jetty mounts it
+at the three explicit path specs declared on the resource via
`@Rest(paths={...})`.
+
+## `HealthIndicator` SPI
+
+`HealthIndicator` is a functional interface:
+
+```java
+@FunctionalInterface
+public interface HealthIndicator {
+ Health check();
+}
+```
+
+Return values are created with `Health.up(name)`, `Health.down(name,
throwable)`, and
+`Health.unknown(name)`, with optional `.detail(key, value)` entries.
+
+Indicators can scope themselves to probe types by overriding `probes()` and
returning a set of
+`HealthProbe` values:
+
+- `HealthProbe.LIVE`
+- `HealthProbe.READY`
+- `HealthProbe.STARTUP`
+
+By default, indicators participate in both liveness and readiness probes.
+
+## Timeout behavior
+
+Each indicator runs with a timeout from `HealthProbeSettings` (default: 1
second). If a check times
+out or throws, the component is marked `DOWN` and the error is surfaced in
`details.error`.
+
+Override defaults by contributing your own settings bean:
+
+```java
+@Bean
+HealthProbeSettings probeSettings() {
+ return HealthProbeSettings.create()
+ .timeout(Duration.ofSeconds(2))
+ .build();
+}
+```
diff --git a/pages/topics/23.01.V9.5-migration-guide.md
b/pages/topics/23.01.V9.5-migration-guide.md
index 1eae1d6893..19d965a620 100644
--- a/pages/topics/23.01.V9.5-migration-guide.md
+++ b/pages/topics/23.01.V9.5-migration-guide.md
@@ -30,6 +30,12 @@ teams jumping from 9.1 (or earlier) directly to 9.5 have a
single reference.
| Custom annotation appliers — user code that subclassed the internal
`AnnotationApplier<Rest, RestContext.Builder>` (or `AnnotationApplier<RestOp,
RestOpContext.Builder>`) to extend the annotation-processing pass (the
`apply(AnnotationInfo<A>, B builder)` hook invoked once per annotation during
context construction). | **Removed.** The builder-based apply-pass is gone;
`RestAnnotation.Apply` (`RestContextApply`) is now a package-private nested
class inside `RestContext` and is not exten [...]
| Custom `RestAnnotation.create(...)` / `RestOpAnnotation.create(...)`
builder-of-builders patterns — programmatic construction of `@Rest` / `@RestOp`
annotation proxies used to feed synthetic annotations into the builder
apply-pass (common in test fixtures and extension libraries). | The annotation
proxy builders still exist for test use (`RestAnnotation.create()` /
`RestOpAnnotation.create()` are still available via annotation-test helpers),
but they no longer feed into a builder apply [...]
+## Health Probe Routing (`mixins` + `paths`)
+
+| Old | New | Notes |
+|-----|-----|-------|
+| `BasicHealthResource` standalone auto-mount used `@Rest(path="/")`, which
collided with root resources mounted at `/*` in Jetty microservices. |
`BasicHealthResource` now uses `@Rest(paths={"/healthz","/readyz","/livez"})`
and can also be composed into an existing root resource via
`@Rest(mixins=BasicHealthResource.class)`. | Prefer `mixins` when you already
have a root `@Rest(path="/")` resource. Keep standalone
`HealthProbeConfiguration` only when you explicitly want a separate servl [...]
+
## SVL and Runtime Input Types Moved to `juneau-commons` (TODO-14)
| Old | New | Notes |
diff --git a/sidebars.ts b/sidebars.ts
index 8c1be7b83b..9c976e4a01 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1327,6 +1327,11 @@ const sidebars: SidebarsConfig = {
id:
'topics/10.07.HandlingFormPosts',
label: '10.7. Handling
Form Posts',
},
+ {
+ type: 'doc',
+ id:
'topics/10.07a.RestServerComposition',
+ label: '10.7a. Mixins
and Multi-Mount Paths',
+ },
{
type: 'doc',
id:
'topics/10.08.RestServerSse',
@@ -1481,6 +1486,11 @@ const sidebars: SidebarsConfig = {
id:
'topics/10.20a.RestServerProblemDetails',
label: '10.20a. RFC
7807 / 9457 Problem Details',
},
+ {
+ type: 'doc',
+ id:
'topics/10.20b.RestServerConditionalGet',
+ label: '10.20b.
Conditional-GET / ETag Helpers',
+ },
{
type: 'doc',
id:
'topics/10.21.BuiltInParameters',
@@ -1847,6 +1857,11 @@ const sidebars: SidebarsConfig = {
id:
'topics/15.09.Extending',
label: '15.9.
Customizing via @Bean',
},
+ {
+ type: 'doc',
+ id:
'topics/15.10.HealthProbes',
+ label: '15.10. Health /
Readiness / Liveness Probes',
+ },
],
},
{