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 ded04f3056 docs: TODO-73 runtime-overridable @Rest(paths=...) — close
out
ded04f3056 is described below
commit ded04f3056a11060aaaea74d2d1e5d6c8540c743
Author: James Bognar <[email protected]>
AuthorDate: Sun May 24 10:23:00 2026 -0400
docs: TODO-73 runtime-overridable @Rest(paths=...) — close out
Release notes + RestServerComposition topic page updates accompanying the
juneau commit closing out TODO-73 (3-rung precedence chain — programmatic >
getter > annotation, per-element SVL + comma-split on @Rest(paths=...)
elements,
getPaths() Object contract, RestContext.Args ctor consolidation,
host-class-only
semantics on @Rest.path() / @Rest.paths() javadoc).
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/9.5.0.md | 55 +++++++++++
pages/topics/10.07a.RestServerComposition.md | 138 +++++++++++++++++++++++++++
2 files changed, 193 insertions(+)
diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 5d51815214..1776792120 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2056,6 +2056,61 @@ 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).
+#### Runtime-Overridable `@Rest(paths=...)` Resolution Chain (TODO-73)
+
+The `paths` array on `@Rest` is now a *default* in a three-rung
**runtime-override resolution
+chain**, walked once during `RestContext` construction and exposed via
`RestContext.getPaths()`.
+Apps can substitute the top-level mount paths at deploy time without
recompiling the resource.
+
+Precedence (highest first):
+
+1. **Programmatic** — `RestContext.Args.paths()` /
`RestContext.Builder.paths(String...)`.
+ `null` means "inherit from lower rungs"; `new String[0]` is the
explicit-clear sentinel.
+2. **`getPaths()` virtual getter** — added to both `RestServlet` and
`RestObject` (default
+ `null`). Returning `null` inherits the lower rung; `new String[0]` is the
explicit-clear
+ sentinel; non-empty wins.
+3. **`@Rest(paths={"…"})` annotation default** — *new behavior:* each element
runs through
+ [SVL](/docs/topics/RestServerSvlVariables) substitution and the resolved
value is then
+ split on `,` (trim each piece, drop empties). A single template element can
therefore
+ expand to zero, one, or many mount paths.
+
+The annotation-default rung now supports the same SVL surface as any other
`@Rest` string member:
+
+```java
+@Rest(paths={"$C{health.paths}"}) // single key,
multi-value
+@Rest(paths={"/api", "$C{extra.paths}"}) // mix literal +
resolved
+@Rest(paths={"$E{HEALTH_PATHS,/healthz,/readyz}"}) // env var w/
comma-separated default
+```
+
+SVL `$C{key}` consults whatever Juneau `Config` is registered on the bean
store (test overlay,
+`@Bean` factory, or the framework's runtime `Config`); `$E{NAME,default}` and
+`$S{prop,default}` use the bootstrap variable catalog without needing any
beans. An SVL failure
+or missing key with no default substitutes empty, which the comma-split then
drops — same
+semantics as every other SVL-bearing `@Rest` member.
+
+The same chain runs whether the app is wired via Juneau auto-discovery
(`JettyServerComponent`)
+or Spring Boot's manual registration. A new public mount-time helper
+`RestContext.resolveTopLevelPaths(Class, Object, BeanStore)` lets Spring Boot
users compute the
+resolved paths inside their `@Bean ServletRegistrationBean<MyServlet>(...)`
factories — the
+programmatic Builder rung is N/A for that flow (no `RestContext` exists at
registration time;
+substitute via the getter or by registering a `Config` bean that the
annotation's `$C{...}`
+elements can resolve through).
+
+> **Spring Boot `application.yaml` integration is planned under a separate
work item.** Once
+> that lands, `$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.
+
+See [REST Server — Mixins and Multi-Mount Paths § Runtime-overridable
paths](/docs/topics/RestServerCompositionMixinsAndPaths#runtime-overridable-paths-since-950)
+for the full reference, including the precedence table, worked examples for
each rung, the
+Spring Boot integration walkthrough, and the `null`-vs-empty-array semantic
table.
+
+This change is purely additive — resources that don't override `getPaths()`,
aren't constructed
+with a programmatic `Args.paths()`, and don't include SVL markers in their
`@Rest(paths=...)`
+literals continue to behave exactly as they did under FINISHED-72 (the
annotation default wins
+on its own, element-for-element). Pre-9.5.0 users who already adopted
`@Rest(paths=...)` see no
+behavior change unless they introduce an SVL marker or an embedded comma in an
element.
+
#### 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:
diff --git a/pages/topics/10.07a.RestServerComposition.md
b/pages/topics/10.07a.RestServerComposition.md
index 35cbaf3fd6..c049be2e72 100644
--- a/pages/topics/10.07a.RestServerComposition.md
+++ b/pages/topics/10.07a.RestServerComposition.md
@@ -367,6 +367,144 @@ probe resource, declare it explicitly
(`paths={"/healthz","/healthz/extra/segmen
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 (since 9.5.0)
+
+The `paths` array on `@Rest` is a *default* — apps frequently need to
substitute it at deploy
+time without recompiling the resource (e.g. probe URLs that vary by Kubernetes
namespace, internal
+endpoints whose paths differ between the AWS and GCP clusters, etc.). Juneau
resolves the
+top-level mount paths through a three-rung **runtime-override resolution
chain**, walked once
+during `RestContext` construction.
+
+### Precedence (highest first)
+
+| Rung | Source | Notes
|
+|------|------------------------------------------------|------------------------------------------------------|
+| 1 | Programmatic — `RestContext.Args.paths()` /
`RestContext.Builder.paths(String...)` | Highest precedence. `null` = inherit;
`new String[0]` = explicit clear. |
+| 2 | `RestServlet.getPaths()` / `RestObject.getPaths()` virtual getter |
`null` = inherit lower rung; non-`null` (incl. empty) wins. |
+| 3 | `@Rest(paths={"…"})` annotation default | Lowest rung. Each
element runs through SVL, then is split on `,` (trim, drop empties). |
+
+The resolved array is exposed via `RestContext.getPaths()` (and consumed at
mount time by
+`JettyServerComponent` and the public mount-time helper
+`RestContext.resolveTopLevelPaths(Class, Object, BeanStore)` for Spring Boot's
+`ServletRegistrationBean` flow).
+
+### Worked examples
+
+**Rung 1 — programmatic override (test fixtures, custom mounts):**
+
+```java
+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:**
+
+```java
+@Rest(paths={"/healthz","/readyz"})
+public class TenantHealthResource extends BasicHealthResource {
+ @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
`BasicHealthResource`.
+
+**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:
+
+```java
+// 1. Single Config key whose value is a comma-separated list.
+@Rest(paths={"$C{health.paths}"})
+public class HealthResource extends BasicRestServlet { ... }
+```
+
+```ini
+# my-app.cfg
+health.paths = /probe/live, /probe/ready
+```
+
+```java
+// 2. Mix literal and resolved elements.
+@Rest(paths={"/api", "$C{extra.paths}"})
+public class ApiResource extends BasicRestServlet { ... }
+```
+
+```java
+// 3. Env var with comma-separated default baked in.
+@Rest(paths={"$E{HEALTH_PATHS,/healthz,/readyz}"})
+public class HealthResource extends BasicRestServlet { ... }
+```
+
+```bash
+# 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:
+
+```java
+@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 under a separate
work item.** Once
+> that lands, `$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
+
+| Value | Programmatic (Rung 1) | Getter (Rung 2) |
+|---------------|-----------------------|-----------------|
+| `null` | No override; lower rungs resolve. | Inherit lower rung. |
+| `new String[0]` | Explicit clear; no top-level mounts. | Explicit clear; no
top-level mounts. |
+
+Picking the right sentinel matters: `null` lets the annotation default still
resolve beneath
+you; `new String[0]` short-circuits and produces a resource with no mounts
(which surfaces a
+clear "no mounts" error from the hosting runtime when nothing is reachable).
+
### Mixin operations don't surface in Swagger as "imported"
The Swagger / OpenAPI emission walks the importing resource's full operation
tree, so mixin