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 485ae56081 TODO-142 + Tomcat microservice module - standalone servlet
companions/self-registration and embedded Tomcat parity
485ae56081 is described below
commit 485ae56081ff57a6704030c1f341411449156752
Author: James Bognar <[email protected]>
AuthorDate: Sun May 31 12:34:45 2026 -0400
TODO-142 + Tomcat microservice module - standalone servlet
companions/self-registration and embedded Tomcat parity
---
pages/release-notes/9.5.0.md | 75 +++++++++++++++
pages/topics/10.07c.RestServerStandaloneVsMixin.md | 100 +++++++++++++++++++
pages/topics/10.07d.RestServerSelfRegistration.md | 106 +++++++++++++++++++++
sidebars.ts | 10 ++
4 files changed, 291 insertions(+)
diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 84d55f1acc..2794385059 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -3720,6 +3720,60 @@ public Order get(@Path long id, RestRequest req,
RestResponse res) {
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.
+#### Standalone servlet companions for dual-use resources
+
+The bundled `Basic*Resource` mixins are designed for composition into a host
via `@Rest(mixins=...)`,
+where each pins its operations at an op-level sub-path (e.g.
`@RestGet(path="/jsp/*")`). That shape
+is awkward when a resource needs to be deployed *on its own* as a top-level
servlet: mounting a
+mixin-shaped resource at its own path doubles the prefix (`/jsp/jsp/...`).
+
+This release adds thin **standalone `Basic*Servlet` companions** for the
dual-use resources. Each
+companion declares its mount at the class level via
`@Rest(paths="/<token>/*")` (sibling top-level
+registration) and pins its op at `/*`, so it serves at a clean sibling path
(e.g. JSPs at `/jsp/*`)
+without prefix-doubling. The mixin `Basic*Resource` forms are unchanged and
keep their op-pinned
+sub-path behavior.
+
+| Standalone servlet | Mounts at | Delegates to (mixin) |
+| --- | --- | --- |
+| `BasicJspServlet` | `/jsp/*` | `BasicJspResource` |
+| `BasicFreemarkerServlet` | `/freemarker/*` | `BasicFreemarkerResource` |
+| `BasicMustacheServlet` | `/mustache/*` | `BasicMustacheResource` |
+| `BasicThymeleafServlet` | `/thymeleaf/*` | `BasicThymeleafResource` |
+| `BasicStaticFilesServlet` | `/static/*` | `BasicStaticFilesResource` |
+| `BasicVersionServlet` | `/version/*` | `BasicVersionResource` |
+| `BasicAdminServlet` | `/admin/*` | `BasicAdminResource` |
+
+- **Shared logic, no drift.** The four view servlets share an abstract base,
`BasicViewServlet`
+ (in `org.apache.juneau.rest.view`), whose single `@RestGet(path="/*")` op
delegates to a new
+ engine-agnostic `RawTemplateDispatcher` interface. The four `Basic*Resource`
view mixins implement
+ `RawTemplateDispatcher`, so the standalone and mixin forms route raw `.jsp`
/ `.ftlh` / `.mustache`
+ / `.html` dispatch through the *same* code and cannot diverge. The non-view
companions
+ (`BasicStaticFilesServlet`, `BasicVersionServlet`, `BasicAdminServlet`) hold
their matching
+ `Basic*Resource` as a delegate and forward each op to it.
+- **`BasicAdminServlet`** carries the same `@Rest(guards=DenyAllGuard.class)`
default-deny posture as
+ the mixin — every admin path returns `403` until an auth `RestGuardList` is
registered.
+- **`BasicHealthResource`** is intentionally *not* given a companion: it
already extends
+ `BasicRestServlet` (it is itself a standalone-capable servlet) and mounts at
fixed absolute paths
+ (`/healthz`, `/readyz`, `/livez`) that do not suffer the prefix-doubling
problem.
+
+See [REST Server — Standalone vs Mixin
Resources](/docs/topics/RestServerStandaloneVsMixin) for the
+full convention.
+
+#### Servlet self-registration (top-level paths)
+
+Three mechanisms register a servlet at the top-level paths it self-declares
(`@Rest(paths)` /
+`getPaths()`), all reading the same resolution chain via
+`RestContext.resolveTopLevelPaths(Class, Object, BeanStore)` — the same
resolver the Jetty
+microservice auto-discovery loop already uses, so mount paths are identical
across runtimes:
+
+- **Non-Spring (`web.xml` / external WAR):** a
`jakarta.servlet.ServletContainerInitializer`,
+ `JuneauRestServletContainerInitializer` (in `juneau-rest-server`),
auto-mounts concrete
+ `@Rest`-annotated `RestServlet` subclasses that self-declare top-level
paths. It is opt-in via the
+ `juneau.rest.auto-register=true` context init parameter, so it never
collides with servlets
+ declared manually in `web.xml`.
+
+See [REST Server — Self-Registration](/docs/topics/RestServerSelfRegistration)
for the full topic.
+
### juneau-rest-client
#### REST session option wire helpers
@@ -3875,6 +3929,27 @@ public View hello(@Path String name) {
### juneau-rest-server-springboot
+#### Servlet self-registration (Spring Boot)
+
+Two Spring Boot mechanisms register a Juneau servlet at the top-level paths it
self-declares
+(`@Rest(paths)` / `getPaths()`), both deriving the mapping from
+`RestContext.resolveTopLevelPaths(Class, Object, BeanStore)` — no hard-coded
path strings:
+
+- **`JuneauRestAutoConfiguration`** — an opt-in `@AutoConfiguration`
(registered via
+
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`)
gated behind
+ the property `juneau.rest.auto-register=true` (`matchIfMissing=false`). When
active, it contributes
+ a single `ServletContextInitializer` that walks every `RestServlet` bean and
mounts each at its
+ self-declared path, **skipping** any servlet instance already wired into a
manual
+ `ServletRegistrationBean` (explicit registrations win) and any servlet that
resolves to no
+ top-level paths.
+- **`JuneauServletRegistrations.forServlet(servlet, beanStore)`** — an
always-available factory
+ helper that returns a `ServletRegistrationBean` whose URL mappings are
derived from the servlet's
+ own `@Rest(paths)` (or `getPaths()`). Use it from an explicit `@Bean` method
when you want the
+ mount path to come from the servlet rather than a hard-coded string. A
servlet that resolves to no
+ top-level paths is rejected with an `IllegalArgumentException`.
+
+See [REST Server — Self-Registration](/docs/topics/RestServerSelfRegistration)
for the full topic.
+
#### Spring `Environment` bridge — `@Value` reads `application.yaml` (TODO-79)
- Added `org.apache.juneau.rest.springboot.SpringEnvironmentPropertySource` —
a `PropertySource`
diff --git a/pages/topics/10.07c.RestServerStandaloneVsMixin.md
b/pages/topics/10.07c.RestServerStandaloneVsMixin.md
new file mode 100644
index 0000000000..8ad9dc6dcd
--- /dev/null
+++ b/pages/topics/10.07c.RestServerStandaloneVsMixin.md
@@ -0,0 +1,100 @@
+---
+title: "REST Server — Standalone vs Mixin Resources"
+slug: RestServerStandaloneVsMixin
+---
+
+Several of Juneau's bundled `Basic*Resource` classes are **dual-use**: they're
useful both as a
+*mixin* composed into a host resource via
[`@Rest(mixins=...)`](/docs/topics/RestServerCompositionMixinsAndPaths)
+*and* as a *standalone* top-level servlet deployed on their own. Starting in
9.5.0 each dual-use
+resource ships a thin standalone **`Basic*Servlet`** companion so both
deployment styles are
+first-class and behave identically.
+
+## The two shapes
+
+### Mixin form (`Basic*Resource`)
+
+A mixin pins its operations at an **op-level sub-path** so it can be grafted
into a host mounted at
+`/`:
+
+```java
+@Rest(mixins=BasicJspResource.class)
+public class AppResource extends RestServlet { /* ... */ }
+// JSPs now served at /jsp/* alongside the host's own endpoints.
+```
+
+Internally the mixin declares something like `@RestGet(path="/jsp/*")` on its
handler. This is the
+right shape for composition, but it doesn't work if you try to deploy the
resource *by itself* at a
+top-level path: mounting a `/jsp/*`-pinned resource at `/jsp/*` would double
the prefix to
+`/jsp/jsp/...`.
+
+### Standalone form (`Basic*Servlet`)
+
+A standalone companion moves the mount to the **class level** via
`@Rest(paths="/<token>/*")` and
+pins its op at `/*`, so it serves at a clean sibling top-level path with no
prefix-doubling:
+
+```java
+// Microservice: auto-mounted at /jsp/* by the Jetty auto-discovery loop.
+@Bean Servlet jsp() { return new BasicJspServlet(); }
+```
+
+The standalone servlet registers as a **sibling** of the application's other
servlets
+(`/rest/*`, `/jsp/*`, `/static/*`, ...), not as a child composed under a host
root.
+
+## Dual-use catalog
+
+| Standalone servlet | Mounts at | Delegates to (mixin) | Module |
+| --- | --- | --- | --- |
+| `BasicJspServlet` | `/jsp/*` | `BasicJspResource` |
`juneau-rest-server-view-jsp` |
+| `BasicFreemarkerServlet` | `/freemarker/*` | `BasicFreemarkerResource` |
`juneau-rest-server-view-freemarker` |
+| `BasicMustacheServlet` | `/mustache/*` | `BasicMustacheResource` |
`juneau-rest-server-view-mustache` |
+| `BasicThymeleafServlet` | `/thymeleaf/*` | `BasicThymeleafResource` |
`juneau-rest-server-view-thymeleaf` |
+| `BasicStaticFilesServlet` | `/static/*` | `BasicStaticFilesResource` |
`juneau-rest-server` |
+| `BasicVersionServlet` | `/version/*` | `BasicVersionResource` |
`juneau-rest-server` |
+| `BasicAdminServlet` | `/admin/*` | `BasicAdminResource` |
`juneau-rest-server` |
+
+True mixins (the API-docs pack — Swagger / SwaggerUi / OpenApi / Redoc — plus
`BasicRouteIndexResource`
+and `BasicEchoResource`) and the root-convention helpers
(`BasicFaviconResource`,
+`BasicSeoResource`, `BasicWellKnownResource`) remain **mixin-only**: they're
only meaningful composed
+into a host, so they get no standalone companion.
+
+## Shared logic — the two forms can't drift
+
+The companions are deliberately thin. They never re-implement the resource's
behavior; they delegate
+to the matching mixin instance so there is a single source of truth.
+
+- **View servlets** share an abstract base, `BasicViewServlet` (in
`org.apache.juneau.rest.view`),
+ whose single `@RestGet(path="/*")` op delegates to a
`RawTemplateDispatcher`. The four view mixins
+ (`BasicJspResource`, ...) implement `RawTemplateDispatcher`, so raw `.jsp` /
`.ftlh` / `.mustache` /
+ `.html` dispatch runs through the *same* code in both forms.
+
+ ```java
+ @Rest(paths="/jsp/*", responseProcessors=JspViewRenderer.class)
+ public class BasicJspServlet extends BasicViewServlet {
+ private final BasicJspResource delegate =
BasicJspResource.create().build();
+ @Override protected RawTemplateDispatcher dispatcher() { return
delegate; }
+ }
+ ```
+
+- **Non-view servlets** (`BasicStaticFilesServlet`, `BasicVersionServlet`,
`BasicAdminServlet`) hold
+ their matching `Basic*Resource` as a delegate and forward each op to it
verbatim.
+
+> **Note — `RawTemplateDispatcher` vs `ViewRenderer`.** These are different
contracts.
+>
[`ViewRenderer`](/site/apidocs/org/apache/juneau/rest/view/ViewRenderer.html)
is the
+> response-processor marker that renders `View`-typed `@RestOp` *return
values*.
+> `RawTemplateDispatcher` covers the *raw* file-serving path (serving a
template directly by its
+> trailing request path). A view mixin implements both.
+
+## Why `BasicHealthResource` has no companion
+
+`BasicHealthResource` is intentionally left without a `Basic*Servlet`. It
already extends
+`BasicRestServlet` — it *is* a standalone-capable servlet — and it mounts at
fixed absolute paths
+(`/healthz`, `/readyz`, `/livez`) that don't suffer the prefix-doubling
problem the `/<token>/*`
+mixins do. Adding a companion would be redundant.
+
+## See also
+
+- [REST Server — Mixins and Multi-Mount
Paths](/docs/topics/RestServerCompositionMixinsAndPaths)
+- [REST Server — Self-Registration](/docs/topics/RestServerSelfRegistration)
+- [Static-Files Mixin](/docs/topics/StaticFilesMixin)
+- [Convention-Endpoints Mixin Pack](/docs/topics/ConventionEndpointsMixins)
+- [Ops / Introspection Mixin Pack](/docs/topics/OpsIntrospectionMixins)
diff --git a/pages/topics/10.07d.RestServerSelfRegistration.md
b/pages/topics/10.07d.RestServerSelfRegistration.md
new file mode 100644
index 0000000000..06a12578dd
--- /dev/null
+++ b/pages/topics/10.07d.RestServerSelfRegistration.md
@@ -0,0 +1,106 @@
+---
+title: "REST Server — Self-Registration"
+slug: RestServerSelfRegistration
+---
+
+A Juneau servlet can declare *where it wants to be mounted* via the top-level
path chain —
+[`@Rest(paths=...)`](/docs/topics/RestServerCompositionMixinsAndPaths) or a
`getPaths()` override —
+rather than having the deployer hard-code a URL pattern at registration time.
Three mechanisms read
+that self-declaration and register the servlet accordingly, one per runtime.
+
+All three resolve the mount path through the **same** resolver:
+
+```java
+String[] paths = RestContext.resolveTopLevelPaths(servletClass,
servletInstance, beanStore);
+```
+
+`resolveTopLevelPaths(...)` walks the precedence chain *programmatic override
→ `getPaths()` getter →
+`@Rest(paths)` annotation default* (with SVL substitution on annotation
elements, so `$C{...}` /
+`${env}` templates resolve against the supplied bean store). It reads the
**top-level** chain
+(`paths()` / `getPaths()`), **never** `path()` (which is for
child-composition). Because every
+mechanism funnels through this one method, mount paths are identical across
runtimes.
+
+## Microservice (Jetty)
+
+No new API — the existing `JettyServerComponent` auto-discovery loop already
calls
+`resolveTopLevelPaths(...)` for each `@Bean Servlet` it finds and mounts it at
the resolved paths.
+A standalone companion (or any `@Rest(paths=...)` servlet) is mounted simply
by exposing it as a
+bean:
+
+```java
+@Bean Servlet version() { return new BasicVersionServlet(); } //
auto-mounted at /version/*
+```
+
+## Spring Boot
+
+### Opt-in auto-configuration
+
+`JuneauRestAutoConfiguration` (in `juneau-rest-server-springboot`, registered
via
+`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`)
walks every
+`RestServlet` bean in the application context and mounts each at its
self-declared path.
+
+It is **gated behind a property** (no default — `matchIfMissing=false`):
+
+```properties
+juneau.rest.auto-register=true
+```
+
+When unset (or any value other than `true`) the auto-configuration contributes
nothing and servlet
+registration stays fully manual. When active it:
+
+1. Skips any servlet instance already wired into a manual
`ServletRegistrationBean` (explicit
+ registrations always win — no double-mounts).
+2. Resolves each servlet's mount paths via `resolveTopLevelPaths(...)` using
the application's
+ registered `BeanStore`.
+3. Registers the servlet at each resolved path. Servlets that resolve to no
top-level paths (pure
+ mixins / child-composed resources) are skipped.
+
+### Explicit factory helper
+
+When you'd rather register servlets yourself but still want the mount path to
come from the servlet
+(not a hard-coded string), use the always-available factory helper:
+
+```java
+@Bean
+ServletRegistrationBean<?> jspServlet(BeanStore beanStore) {
+ return JuneauServletRegistrations.forServlet(new BasicJspServlet(),
beanStore);
+}
+```
+
+`JuneauServletRegistrations.forServlet(...)` derives the registration's URL
mappings from the
+servlet's `@Rest(paths)` / `getPaths()`. A servlet that resolves to no
top-level paths is rejected
+with an `IllegalArgumentException`.
+
+## Non-Spring (`web.xml` / external WAR)
+
+For plain Servlet-spec deployments, `juneau-rest-server` ships a
+`jakarta.servlet.ServletContainerInitializer`,
`JuneauRestServletContainerInitializer` (discovered
+via `META-INF/services/jakarta.servlet.ServletContainerInitializer`). Through
+`@HandlesTypes(RestServlet.class)` the container hands it every `RestServlet`
on the webapp
+classpath; it auto-mounts each concrete, `@Rest`-annotated,
no-arg-instantiable servlet that
+self-declares top-level paths.
+
+It is **opt-in** via a context init parameter (parity with the Spring Boot
property), so it never
+collides with servlets the deployer declared manually in `web.xml`:
+
+```xml
+<context-param>
+ <param-name>juneau.rest.auto-register</param-name>
+ <param-value>true</param-value>
+</context-param>
+```
+
+## Cross-runtime parity
+
+| Runtime | Mechanism | Activation | Reads |
+| --- | --- | --- | --- |
+| Jetty microservice | `JettyServerComponent` auto-discovery | always (for
`@Bean Servlet`) | `resolveTopLevelPaths(...)` |
+| Spring Boot | `JuneauRestAutoConfiguration` |
`juneau.rest.auto-register=true` | `resolveTopLevelPaths(...)` |
+| Spring Boot | `JuneauServletRegistrations.forServlet(...)` | explicit
`@Bean` | `resolveTopLevelPaths(...)` |
+| `web.xml` / WAR | `JuneauRestServletContainerInitializer` |
`juneau.rest.auto-register` context-param | `resolveTopLevelPaths(...)` |
+
+## See also
+
+- [REST Server — Standalone vs Mixin
Resources](/docs/topics/RestServerStandaloneVsMixin)
+- [REST Server — Mixins and Multi-Mount
Paths](/docs/topics/RestServerCompositionMixinsAndPaths)
+- [juneau-rest-server-springboot
Basics](/docs/topics/JuneauRestServerSpringbootBasics)
diff --git a/sidebars.ts b/sidebars.ts
index 5a4c61afbd..9a1232bb3c 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1342,6 +1342,16 @@ const sidebars: SidebarsConfig = {
id:
'topics/10.07a.RestServerComposition',
label: '10.7a. Mixins
and Multi-Mount Paths',
},
+ {
+ type: 'doc',
+ id:
'topics/10.07c.RestServerStandaloneVsMixin',
+ label: '10.7c.
Standalone vs Mixin Resources',
+ },
+ {
+ type: 'doc',
+ id:
'topics/10.07d.RestServerSelfRegistration',
+ label: '10.7d. Servlet
Self-Registration',
+ },
{
type: 'doc',
id:
'topics/10.08.RestServerSse',