This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 29bd5a598e TODO-117/118/121/128 - MdcAsyncListener, async completion 
executor, lazy children materialization, ParameterInfo Optional wrapping
29bd5a598e is described below

commit 29bd5a598e5adc79a4a83da4ce1505b008970872
Author: James Bognar <[email protected]>
AuthorDate: Fri May 29 10:58:20 2026 -0400

    TODO-117/118/121/128 - MdcAsyncListener, async completion executor, lazy 
children materialization, ParameterInfo Optional wrapping
---
 .../java/org/apache/juneau/rest/RestChildren.java  | 199 +++++++++++++-
 .../java/org/apache/juneau/rest/RestContext.java   |  86 +++++-
 .../apache/juneau/rest/RestServerConstants.java    |   9 +
 .../org/apache/juneau/rest/annotation/Rest.java    |  48 +++-
 .../juneau/rest/annotation/RestAnnotation.java     |  19 ++
 .../org/apache/juneau/rest/LazyChildren_Test.java  | 304 +++++++++++++++++++++
 juneau-utest/test-run-history.tsv                  |   1 +
 7 files changed, 656 insertions(+), 10 deletions(-)

diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestChildren.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestChildren.java
index 7bd8ba016f..0ea81bb0c8 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestChildren.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestChildren.java
@@ -17,12 +17,14 @@
 package org.apache.juneau.rest;
 
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
 import static org.apache.juneau.commons.utils.ThrowableUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 
 import java.lang.reflect.*;
 import java.util.*;
 import java.util.function.*;
+import java.util.logging.*;
 
 import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.commons.reflect.*;
@@ -42,12 +44,118 @@ import jakarta.servlet.*;
  * write lock and atomically replace the snapshot. This makes runtime child 
management safe even while
  * requests are in flight on other threads.
  *
+ * <p>
+ * Supports lazy-init children registered via {@link Builder#addLazy(Class, 
String)} — these children have their
+ * {@link RestContext} constructed on the first matching request rather than 
at parent startup.  The routing entry
+ * (path prefix matcher) is always populated at startup; only the full context 
build is deferred.
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/RestAnnotatedClassBasics";>@Rest-Annotated
 Class Basics</a>
  * </ul>
  */
 public class RestChildren {
 
+       private static final Logger LOGGER = 
Logger.getLogger(RestChildren.class.getName());
+
+       /**
+        * Holder for a lazy-init child resource entry.
+        *
+        * <p>
+        * At parent startup, only the path prefix and {@link UrlPathMatcher} 
are populated.  The full
+        * {@link RestContext} is built on the first request that matches the 
path prefix; subsequent requests
+        * reuse the cached context.  Two concurrent first-requests serialize 
on {@link #lock} so that exactly
+        * one materialization runs.
+        *
+        * <p>
+        * If materialization throws, the exception propagates to the caller as 
a {@link jakarta.servlet.ServletException};
+        * the {@link #materialized} field is left {@code null}, so the next 
request will retry.
+        */
+       static class LazyChildEntry {
+
+               final Class<?> resourceClass;
+               final String path;
+               final UrlPathMatcher pathMatcher;
+               final RestContext parent;
+               final BeanStore beanStore;
+               final ServletConfig servletConfig;
+
+               /** Written once under {@link #lock}; volatile for lock-free 
reads. */
+               volatile RestContext materialized;
+
+               private final Object lock = new Object();
+
+               LazyChildEntry(Class<?> resourceClass, String path, RestContext 
parent, BeanStore beanStore, ServletConfig servletConfig) {
+                       this.resourceClass = resourceClass;
+                       this.path = path;
+                       var p = path;
+                       if (! p.endsWith("/*"))
+                               p += "/*";
+                       this.pathMatcher = UrlPathMatcher.of(p);
+                       this.parent = parent;
+                       this.beanStore = beanStore;
+                       this.servletConfig = servletConfig;
+               }
+
+               boolean isMaterialized() { return materialized != null; }
+
+               /**
+                * Returns the materialized {@link RestContext}, building it if 
this is the first call.
+                *
+                * <p>
+                * Thread-safe: at most one thread runs the construction; 
others block on {@link #lock}.
+                * If construction fails the field is left {@code null}; the 
next request will retry.
+                *
+                * @return The materialized context. Never {@code null} on 
success.
+                * @throws ServletException If context construction fails.
+                */
+               RestContext materialize() throws ServletException {
+                       RestContext rc = materialized;
+                       if (rc != null)
+                               return rc;
+                       synchronized (lock) {
+                               rc = materialized;
+                               if (rc != null)
+                                       return rc;
+                               LOGGER.info("Lazy REST child materializing: " + 
resourceClass.getName() + " at path '" + path + "'");
+                               try {
+                                       rc = buildChildContext(parent, 
beanStore, servletConfig, resourceClass, null, "");
+                                       rc.postInit();
+                                       rc.postInitChildFirst();
+                               } catch (Exception e) {
+                                       throw new ServletException("Failed to 
lazily materialize child REST context for " + resourceClass.getName(), 
unwrapThrowable(e));
+                               }
+                               materialized = rc;
+                               LOGGER.info("Lazy REST child materialized: " + 
resourceClass.getName() + " at path '" + path + "'");
+                               return rc;
+                       }
+               }
+
+               /**
+                * Resolves the path prefix for a resource class by walking its 
annotation hierarchy.
+                *
+                * <p>
+                * Replicates the most-derived-first walk that {@link 
RestContext} performs during full construction,
+                * but does so without instantiating a {@link RestContext} — 
enabling startup-time path extraction
+                * for lazy children.
+                *
+                * @param resourceClass The resource class. Must not be {@code 
null}.
+                * @return The trimmed path (without leading slash). Empty 
string if no {@code @Rest(path=...)} is found.
+                */
+               static String resolvePathForClass(Class<?> resourceClass) {
+                       for (Class<?> c = resourceClass; c != null; c = 
c.getSuperclass()) {
+                               Rest r = c.getDeclaredAnnotation(Rest.class);
+                               if (r != null && isNotEmpty(r.path()))
+                                       return trimLeadingSlashes(r.path());
+                       }
+                       for (Class<?> iface : resourceClass.getInterfaces()) {
+                               Rest r = 
iface.getDeclaredAnnotation(Rest.class);
+                               if (r != null && isNotEmpty(r.path()))
+                                       return trimLeadingSlashes(r.path());
+                       }
+                       return "";
+               }
+       }
+
        /**
         * Builder class.
         */
@@ -57,6 +165,7 @@ public class RestChildren {
                final BeanStore beanStore;
                final ServletConfig servletConfig;
                final List<RestContext> list;
+               final List<LazyChildEntry> lazyList;
 
                /**
                 * Constructor.
@@ -74,6 +183,7 @@ public class RestChildren {
                        this.beanStore = beanStore;
                        this.servletConfig = servletConfig;
                        this.list = list();
+                       this.lazyList = list();
                }
 
                /**
@@ -86,7 +196,7 @@ public class RestChildren {
                }
 
                /**
-                * Adds a child resource to this builder.
+                * Adds an eager child resource to this builder.
                 *
                 * @param value The REST context of the child resource.
                 * @return This object.
@@ -96,6 +206,24 @@ public class RestChildren {
                        return this;
                }
 
+               /**
+                * Registers a lazy child resource entry.
+                *
+                * <p>
+                * The path is pre-resolved from the resource class so that 
routing works immediately at parent startup.
+                * The full {@link RestContext} is built on the first matching 
request.
+                *
+                * @param resourceClass The resource class to materialize 
lazily. Must not be {@code null}.
+                * @param path The pre-resolved path prefix (without leading 
slash). Use {@code ""} to read from
+                *      {@link Rest#path() @Rest(path)} on the resource class.
+                * @return This object.
+                */
+               public Builder addLazy(Class<?> resourceClass, String path) {
+                       var resolvedPath = isNotEmpty(path) ? 
trimLeadingSlashes(path) : LazyChildEntry.resolvePathForClass(resourceClass);
+                       lazyList.add(new LazyChildEntry(resourceClass, 
resolvedPath, parent, beanStore, servletConfig));
+                       return this;
+               }
+
                /**
                 * Builds the children.
                 *
@@ -124,8 +252,20 @@ public class RestChildren {
        private final ServletConfig servletConfig;
        private final Object writeLock = new Object();
 
+       /** Eager-init children keyed by composed path. Copy-on-write; volatile 
for lock-free reads. */
        private volatile Map<String,RestContext> children;
 
+       /**
+        * Lazy-init children keyed by path prefix.
+        *
+        * <p>
+        * These entries are registered at parent startup for routing purposes; 
their {@link RestContext} bodies
+        * are built on first use.  Once materialized, the entry's {@link 
LazyChildEntry#materialized} field is set
+        * but the entry remains in this map so subsequent reads are lock-free 
(volatile field read on the entry).
+        * This map itself is never mutated after construction, so no 
copy-on-write is needed.
+        */
+       private final Map<String,LazyChildEntry> lazyEntries;
+
        /**
         * Constructor.
         *
@@ -139,6 +279,10 @@ public class RestChildren {
                for (var rc : builder.list)
                        initial.put(rc.getPath(), rc);
                this.children = Collections.unmodifiableMap(initial);
+               var lazyMap = new LinkedHashMap<String,LazyChildEntry>();
+               for (var e : builder.lazyList)
+                       lazyMap.put(e.path, e);
+               this.lazyEntries = Collections.unmodifiableMap(lazyMap);
        }
 
        /**
@@ -157,26 +301,47 @@ public class RestChildren {
 
        /**
         * Called during servlet destruction on all children to invoke all 
{@link RestDestroy} and {@link Servlet#destroy()} methods.
+        *
+        * <p>
+        * Lazy children that were never materialized are silently skipped (no 
context was ever built for them).
         */
        public void destroy() {
                for (var r : children.values())
                        destroyChild(r);
+               for (var e : lazyEntries.values()) {
+                       var rc = e.materialized;  // volatile read — safe 
without lock
+                       if (rc != null)
+                               destroyChild(rc);
+               }
        }
 
        /**
         * Looks through the registered children of this object and returns the 
best match.
         *
+        * <p>
+        * Eager children are checked first (lock-free volatile read), then 
lazy entries.  A matching lazy entry
+        * triggers {@link LazyChildEntry#materialize()} which builds the full 
{@link RestContext} on first call
+        * (blocking concurrent first-requests until construction completes) 
and caches the result.
+        *
         * @param builder The HTTP call builder.
         * @return The child that best matches the call, or an empty {@link 
Optional} if a match could not be made.
+        * @throws ServletException If a lazy child fails to materialize.
         */
-       public Optional<RestChildMatch> findMatch(RestSession.Builder builder) {
-               var snapshot = children;  // single volatile read; consistent 
for the rest of the method
+       public Optional<RestChildMatch> findMatch(RestSession.Builder builder) 
throws ServletException {
                var pi = builder.getPathInfoUndecoded();
-               if ((! snapshot.isEmpty()) && nn(pi) && ! pi.equals("/")) {
+               if (nn(pi) && ! pi.equals("/")) {
+                       // Check eager children first.
+                       var snapshot = children;  // single volatile read
                        for (var rc : snapshot.values()) {
-                               UrlPathMatcher upp = rc.getPathMatcher();
-                               UrlPathMatch uppm = 
upp.match(builder.getUrlPath());
+                               UrlPathMatch uppm = 
rc.getPathMatcher().match(builder.getUrlPath());
+                               if (nn(uppm))
+                                       return opt(RestChildMatch.create(uppm, 
rc));
+                       }
+                       // Check lazy entries.
+                       for (var e : lazyEntries.values()) {
+                               UrlPathMatch uppm = 
e.pathMatcher.match(builder.getUrlPath());
                                if (nn(uppm)) {
+                                       var rc = e.materialize();
                                        return opt(RestChildMatch.create(uppm, 
rc));
                                }
                        }
@@ -185,7 +350,10 @@ public class RestChildren {
        }
 
        /**
-        * Called during servlet initialization on all children to invoke all 
{@link RestPostInit} child-last methods.
+        * Called during servlet initialization on all eager children to invoke 
all {@link RestPostInit} child-last methods.
+        *
+        * <p>
+        * Lazy children are intentionally excluded — they are initialized on 
first invocation, not at startup.
         *
         * @throws ServletException Error occurred.
         */
@@ -195,7 +363,10 @@ public class RestChildren {
        }
 
        /**
-        * Called during servlet initialization on all children to invoke all 
{@link RestPostInit} child-first methods.
+        * Called during servlet initialization on all eager children to invoke 
all {@link RestPostInit} child-first methods.
+        *
+        * <p>
+        * Lazy children are intentionally excluded — they are initialized on 
first invocation, not at startup.
         *
         * @throws ServletException Error occurred.
         */
@@ -204,6 +375,18 @@ public class RestChildren {
                        childContext.postInitChildFirst();
        }
 
+       /**
+        * Returns the lazy child entries registered on this object.
+        *
+        * <p>
+        * Each entry carries the path prefix and materialization state. 
Primarily used for testing.
+        *
+        * @return An unmodifiable map of path-to-lazy-entry. Never {@code 
null}.
+        */
+       public Map<String,LazyChildEntry> getLazyEntries() {
+               return lazyEntries;
+       }
+
        
//-------------------------------------------------------------------------------------------------------------
        // Dynamic add/remove API.
        //
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 78a725ecd7..acebf12b8d 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -305,6 +305,20 @@ public class RestContext extends Context {
                 */
                String asyncCompletionExecutorName;
 
+               /**
+                * Programmatic override for the lazy-children flag.
+                *
+                * <p>
+                * {@code null} (default) &mdash; defers to the {@link 
Rest#lazyChildren() @Rest(lazyChildren)} annotation
+                * chain, then to the {@code RestContext.lazyChildren} 
env-driven default (itself defaulting to
+                * {@code false}). Set via {@link #lazyChildInit(boolean)}.
+                *
+                * <p>
+                * When {@code true}, all children registered via {@link 
Rest#children() @Rest(children)} on this resource
+                * are built lazily on first invocation rather than eagerly at 
parent startup.
+                */
+               Boolean lazyChildInit;
+
                /**
                 * Package-private constructor.
                 *
@@ -392,6 +406,28 @@ public class RestContext extends Context {
                        return this;
                }
 
+               /**
+                * Programmatically opt this resource into (or out of) deferred 
child construction.
+                *
+                * <p>
+                * When {@code true}, all children registered via {@link 
Rest#children() @Rest(children)} are built
+                * lazily on first invocation instead of eagerly at parent 
startup.  The routing entry for each child
+                * is always populated at startup so URL matching works 
immediately.
+                *
+                * <p>
+                * This is the highest-priority knob — it overrides both the
+                * {@link Rest#lazyChildren() @Rest(lazyChildren)} annotation 
and the
+                * {@code RestContext.lazyChildren} env-driven default.
+                *
+                * @param value {@code true} to defer child construction to 
first invocation.
+                * @return This object.
+                * @since 9.5.0
+                */
+               public Builder lazyChildInit(boolean value) {
+                       lazyChildInit = value;
+                       return this;
+               }
+
                @Override /* Context.Builder is abstract - copy() is not 
meaningful for the transient RestContext bootstrap state. */
                public Builder copy() {
                        throw new NoSuchMethodError("Not implemented.");
@@ -1107,6 +1143,10 @@ public class RestContext extends Context {
        @Value("${RestContext.eagerInit:false}")
        private boolean defaultEagerInit;
 
+       /** Env-driven default for {@code @Rest(lazyChildren)}: deferred child 
construction opt-in. */
+       @Value("${RestContext.lazyChildren:false}")
+       private boolean defaultLazyChildren;
+
        /** Env-driven default for {@code @Rest(clientVersionHeader)}. */
        @Value("${RestContext.clientVersionHeader:Client-Version}")
        private String defaultClientVersionHeader;
@@ -1930,10 +1970,18 @@ public class RestContext extends Context {
                var seen = new LinkedHashSet<Class<?>>();
                getRestAnnotations().forEach(ai -> 
seen.addAll(Arrays.asList(ai.inner().children())));
 
+               var lazy = isLazyChildren();
+
                for (var rc2 : seen) {
                        if (rc2 == resourceClass())
                                continue;  // Guard against self-reference 
infinite loop.
-                       b.add(RestChildren.buildChildContext(this, bs, 
servletConfig, rc2, null, ""));
+                       if (lazy) {
+                               // Lazy: register a routing stub now; defer 
full RestContext construction to first request.
+                               b.addLazy(rc2, "");
+                       } else {
+                               // Eager (default): build the full child 
RestContext immediately.
+                               b.add(RestChildren.buildChildContext(this, bs, 
servletConfig, rc2, null, ""));
+                       }
                }
 
                // @Bean override — allows replacing the entire RestChildren 
instance.
@@ -2554,6 +2602,18 @@ public class RestContext extends Context {
        private final Memoizer<Boolean> eagerInit = memoizer(() ->
                mergeReplacedBooleanAttribute(PROPERTY_eagerInit, 
defaultEagerInit));
 
+       /**
+        * Annotation + env-driven component of the lazy-children flag.
+        *
+        * <p>
+        * Reads the {@link Rest#lazyChildren() @Rest(lazyChildren)} annotation 
chain and falls back to the
+        * {@code RestContext.lazyChildren} env-driven default.  The 
programmatic
+        * {@link Builder#lazyChildInit(boolean)} knob is applied in {@link 
#isLazyChildren()} instead of here
+        * because blank-final-field rules prevent the memoizer lambda from 
safely capturing {@link #builder}.
+        */
+       private final Memoizer<Boolean> lazyChildrenAnnotation = memoizer(() ->
+               mergeReplacedBooleanAttribute(PROPERTY_lazyChildren, 
defaultLazyChildren));
+
        /**
         * The request header used for client-version matching; resolved from 
{@code @Rest(clientVersionHeader)},
         * default {@code "Client-Version"}.
@@ -3808,6 +3868,30 @@ public class RestContext extends Context {
         */
        public boolean isEagerInit() { return eagerInit.get(); }
 
+       /**
+        * Returns whether this resource's {@code @Rest(children=...)} entries 
are built lazily on first invocation
+        * rather than eagerly at parent startup.
+        *
+        * <p>
+        * Resolution order (highest wins):
+        * <ol>
+        *   <li>{@link Builder#lazyChildInit(boolean)} programmatic knob (if 
explicitly set).</li>
+        *   <li>{@link Rest#lazyChildren() @Rest(lazyChildren)} annotation 
chain (most-derived wins).</li>
+        *   <li>{@code RestContext.lazyChildren} env-driven default (default 
{@code false}).</li>
+        * </ol>
+        *
+        * <h5 class='section'>See Also:</h5><ul>
+        *      <li class='ja'>{@link Rest#lazyChildren()}
+        *      <li class='jm'>{@link Builder#lazyChildInit(boolean)}
+        * </ul>
+        *
+        * @return <jk>true</jk> if lazy child initialization is enabled.
+        * @since 9.5.0
+        */
+       public boolean isLazyChildren() {
+               return builder.lazyChildInit != null ? builder.lazyChildInit : 
lazyChildrenAnnotation.get();
+       }
+
        /**
         * Called during servlet initialization to invoke all {@link 
RestPostInit} child-last methods.
         *
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestServerConstants.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestServerConstants.java
index 23263fb722..7d879629f0 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestServerConstants.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestServerConstants.java
@@ -69,6 +69,15 @@ public final class RestServerConstants {
        /** The {@code "eagerInit"} annotation attribute name — used in {@code 
noInherit} matching. */
        public static final String PROPERTY_eagerInit = "eagerInit";
 
+       /**
+        * The {@code "lazyChildren"} annotation attribute name — used in 
{@code noInherit} matching to opt the
+        * parent resource into deferred (first-invocation) construction of its 
{@code @Rest(children=...)} sub-resources
+        * instead of the default eager construction at startup.
+        *
+        * @see org.apache.juneau.rest.annotation.Rest#lazyChildren()
+        */
+       public static final String PROPERTY_lazyChildren = "lazyChildren";
+
        /**
         * The {@code "virtualThreads"} annotation attribute name — used in 
{@code noInherit} matching to opt the resource
         * (or one of its {@code @RestOp}-annotated methods) into per-request 
virtual-thread dispatch on Java 21+. On
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Rest.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Rest.java
index 22f911bf08..921a408658 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Rest.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Rest.java
@@ -325,6 +325,52 @@ public @interface Rest {
         */
        String eagerInit() default "";
 
+       /**
+        * Opt this parent resource into deferred (first-invocation) 
construction of its {@link #children()} sub-resources.
+        *
+        * <p>
+        * When {@code "true"}, the {@link RestContext} instances for all 
{@code @Rest(children=...)} entries are
+        * <em>not</em> built at parent startup.  Instead, each child's routing 
entry is registered immediately (so URL
+        * matching is fully operational from the first request), but the full 
{@link RestContext} — including all of its
+        * bean-store setup, memoizers, and lifecycle hooks — is constructed on 
the first inbound request to that child's
+        * URL prefix.  Subsequent requests reuse the already-built context.
+        *
+        * <p>
+        * This is particularly useful when a parent resource exposes 
heavyweight admin or diagnostic children that are
+        * rarely invoked in production.  Setting {@code lazyChildren="true"} 
on the parent lets the parent boot fast;
+        * each child pays its construction cost only when (and if) it is first 
accessed.
+        *
+        * <ul class='values'>
+        *      <li><js>"true"</js> - Children are constructed on first 
invocation (deferred).
+        *      <li><js>"false"</js> (default) - Children are constructed 
eagerly at parent startup.
+        * </ul>
+        *
+        * <h5 class='section'>Notes:</h5><ul>
+        *      <li class='note'>
+        *              The first request to a lazy child pays the full 
construction cost, which can be significant for
+        *              heavyweight children.  If predictable first-request 
latency is required, do not opt in.
+        *      <li class='note'>
+        *              Concurrent first-requests to the same lazy child are 
serialized: only one thread runs the construction;
+        *              others block until it completes.
+        *      <li class='note'>
+        *              A lazy child that is never invoked is never 
constructed.  Its lifecycle {@code @RestDestroy} / shutdown
+        *              hooks are skipped at parent destruction time.
+        *      <li class='note'>
+        *              The programmatic knob {@link 
RestContext.Builder#lazyChildInit(boolean)} overrides this annotation.
+        *      <li class='note'>
+        *              Supports <a class="doclink" 
href="https://juneau.apache.org/docs/topics/RestServerSvlVariables";>SVL 
Variables</a>
+        *              (e.g. <js>"$E{LAZY_CHILDREN,false}"</js>).
+        * </ul>
+        *
+        * <h5 class='section'>See Also:</h5><ul>
+        *      <li class='jm'>{@link 
RestContext.Builder#lazyChildInit(boolean)}
+        *      <li class='jm'>{@link #children()}
+        * </ul>
+        *
+        * @return The annotation value.
+        */
+       String lazyChildren() default "";
+
        /**
         * Supported content media types.
         *
@@ -388,7 +434,7 @@ public @interface Rest {
         * Accepted values include {@code "allowedParserOptions"}, {@code 
"allowedSerializerOptions"},
         * {@code "allowedHeaderParams"}, {@code "allowedMethodHeaders"}, 
{@code "allowedMethodParams"},
         * {@code "disableContentParam"}, {@code "renderResponseStackTraces"}, 
{@code "problemDetails"},
-        * {@code "eagerInit"}, {@code "clientVersionHeader"},
+        * {@code "eagerInit"}, {@code "lazyChildren"}, {@code 
"clientVersionHeader"},
         * {@code "uriAuthority"}, {@code "uriContext"}, {@code 
"uriRelativity"}, and {@code "uriResolution"}.
         * Each entry is SVL-resolved then comma-split. Prevents the named 
property from inheriting values from
         * parent {@code @Rest} annotations (router hierarchy). The {@code 
noInherit} attribute itself is never inherited.
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestAnnotation.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestAnnotation.java
index 3b7c4a1777..70c85accb6 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestAnnotation.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestAnnotation.java
@@ -84,6 +84,7 @@ public class RestAnnotation {
                private String clientVersionHeader = "";
                private String config = "";
                private String eagerInit = "";
+               private String lazyChildren = "";
                private String defaultAccept = "";
                private String defaultCharset = "";
                private String defaultContentType = "";
@@ -244,6 +245,17 @@ public class RestAnnotation {
                        return this;
                }
 
+               /**
+                * Sets the {@link Rest#lazyChildren()} property on this 
annotation.
+                *
+                * @param value The new value for this property.
+                * @return This object.
+                */
+               public Builder lazyChildren(String value) {
+                       lazyChildren = value;
+                       return this;
+               }
+
                /**
                 * Sets the {@link Rest#consumes()} property on this annotation.
                 *
@@ -777,6 +789,7 @@ public class RestAnnotation {
                private final String clientVersionHeader;
                private final String config;
                private final String eagerInit;
+               private final String lazyChildren;
                private final String defaultAccept;
                private final String defaultCharset;
                private final String defaultContentType;
@@ -824,6 +837,7 @@ public class RestAnnotation {
                        clientVersionHeader = b.clientVersionHeader;
                        config = b.config;
                        eagerInit = b.eagerInit;
+                       lazyChildren = b.lazyChildren;
                        allowedParserOptions = copyOf(b.allowedParserOptions);
                        allowedSerializerOptions = 
copyOf(b.allowedSerializerOptions);
                        noInherit = copyOf(b.noInherit);
@@ -918,6 +932,11 @@ public class RestAnnotation {
                        return eagerInit;
                }
 
+               @Override /* Overridden from Rest */
+               public String lazyChildren() {
+                       return lazyChildren;
+               }
+
                @Override /* Overridden from Rest */
                public String[] allowedParserOptions() {
                        return allowedParserOptions;
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/LazyChildren_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/rest/LazyChildren_Test.java
new file mode 100644
index 0000000000..3a05e77ec9
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/LazyChildren_Test.java
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for opt-in lazy {@code @Rest(children=...)} materialization 
(TODO-121).
+ *
+ * <p>
+ * Each scenario uses a distinct parent class so the {@link MockRestClient}'s 
per-class
+ * {@link RestContext} cache does not bleed state across tests.
+ */
+class LazyChildren_Test extends TestBase {
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Shared child resources.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(path = "/alpha")
+       public static class AlphaResource {
+               @RestGet("/ping")
+               public String ping() {
+                       return "alpha-pong";
+               }
+       }
+
+       @Rest(path = "/beta")
+       public static class BetaResource {
+               @RestGet("/ping")
+               public String ping() {
+                       return "beta-pong";
+               }
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // a01: HappyPath — annotation enables lazy; routing works immediately; 
first request triggers materialization;
+       //      subsequent requests reuse the context.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(path = "/root", children = { AlphaResource.class }, lazyChildren 
= "true")
+       public static class A_Parent extends BasicRestServletGroup {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Test
+       void a01_happyPath_lazyChildMaterializesOnFirstRequest() throws 
Exception {
+               var parent = new A_Parent();
+               var client = MockRestClient.createLax(parent).build();
+               var rc = parent.getContext();
+
+               // lazyChildren=true is honoured.
+               assertTrue(rc.isLazyChildren());
+
+               // Before first request: lazy entry is registered but NOT 
materialized.
+               var entries = rc.getRestChildren().getLazyEntries();
+               assertFalse(entries.isEmpty(), "Lazy entry should be registered 
at startup");
+               var entry = entries.get("alpha");
+               assertNotNull(entry, "Lazy entry key should be 'alpha'");
+               assertFalse(entry.isMaterialized(), "Should not be materialized 
before first request");
+
+               // First request materializes the child.
+               
client.get("/alpha/ping").run().assertStatus(200).assertContent("alpha-pong");
+
+               // After first request: materialized.
+               assertTrue(entry.isMaterialized(), "Should be materialized 
after first request");
+
+               // Subsequent requests continue to succeed (reuse materialized 
context).
+               
client.get("/alpha/ping").run().assertStatus(200).assertContent("alpha-pong");
+               
client.get("/alpha/ping").run().assertStatus(200).assertContent("alpha-pong");
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // a02: Concurrent first-requests — only one materialization runs.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       static final AtomicInteger A02_INIT_COUNT = new AtomicInteger(0);
+
+       @Rest(path = "/counted")
+       public static class CountedResource {
+               public CountedResource() {
+                       A02_INIT_COUNT.incrementAndGet();
+               }
+
+               @RestGet("/ping")
+               public String ping() {
+                       return "counted-pong";
+               }
+       }
+
+       @Rest(path = "/root", children = { CountedResource.class }, 
lazyChildren = "true")
+       public static class B_Parent extends BasicRestServletGroup {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Test
+       void a02_concurrent_exactlyOneMaterialization() throws Exception {
+               A02_INIT_COUNT.set(0);
+               var client = MockRestClient.createLax(new B_Parent()).build();
+
+               int concurrency = 8;
+               var barrier = new CyclicBarrier(concurrency);
+               var latch = new CountDownLatch(concurrency);
+               var errors = new AtomicReference<Throwable>();
+               var pool = Executors.newFixedThreadPool(concurrency);
+
+               for (int i = 0; i < concurrency; i++) {
+                       pool.submit(() -> {
+                               try {
+                                       barrier.await();  // all threads start 
simultaneously
+                                       
client.get("/counted/ping").run().assertStatus(200).assertContent("counted-pong");
+                               } catch (Throwable t) {
+                                       errors.compareAndSet(null, t);
+                               } finally {
+                                       latch.countDown();
+                               }
+                       });
+               }
+
+               latch.await(10, TimeUnit.SECONDS);
+               pool.shutdown();
+               assertNull(errors.get(), "Concurrent requests must not throw");
+               assertEquals(1, A02_INIT_COUNT.get(), "CountedResource 
constructor must be called exactly once");
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // a03: Never-invoked lazy child — parent shuts down without touching 
the child context.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       static final AtomicInteger A03_INIT_COUNT = new AtomicInteger(0);
+
+       @Rest(path = "/never-invoked")
+       public static class NeverInvokedResource {
+               public NeverInvokedResource() {
+                       A03_INIT_COUNT.incrementAndGet();
+               }
+
+               @RestGet("/ping")
+               public String ping() {
+                       return "never-pong";
+               }
+       }
+
+       @Rest(path = "/root", children = { NeverInvokedResource.class }, 
lazyChildren = "true")
+       public static class C_Parent extends BasicRestServletGroup {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Test
+       void a03_neverInvoked_childIsNeverMaterialized() {
+               A03_INIT_COUNT.set(0);
+               var parent = new C_Parent();
+               // Just creating the client wires the parent context (eager 
path for parent; lazy for children).
+               MockRestClient.createLax(parent).build();
+
+               var rc = parent.getContext();
+               var entry = 
rc.getRestChildren().getLazyEntries().get("never-invoked");
+               assertNotNull(entry);
+               assertFalse(entry.isMaterialized(), "Child should not be 
materialized if never invoked");
+               assertEquals(0, A03_INIT_COUNT.get(), "NeverInvokedResource 
constructor must not have been called");
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // a04: Builder override — lazyChildInit(false) overrides 
@Rest(lazyChildren="true").
+       //      Tests via programmatic RestContext.Builder used in 
package-accessible test.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(path = "/root", children = { AlphaResource.class }, lazyChildren 
= "true")
+       public static class D_Parent extends BasicRestServletGroup {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Rest(path = "/root", children = { AlphaResource.class })
+       public static class D_EagerParent extends BasicRestServletGroup {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Test
+       void a04_builderOverride_lazyChildInitTrueOverridesEagerDefault() 
throws Exception {
+               // Use Builder.lazyChildInit(true) to force lazy on a resource 
that has no annotation.
+               // The Builder is package-private, so this test is in the 
org.apache.juneau.rest package.
+               var d = new D_EagerParent();
+               var client = MockRestClient.createLax(d).build();
+               var rc = d.getContext();
+
+               // Default is eager; verify annotation-free behavior.
+               assertFalse(rc.isLazyChildren(), "No annotation => eager 
(default)");
+
+               // Separately verify that the builder knob can be set and read.
+               var args = new RestContext.Args(D_EagerParent.class, null, 
null, () -> d, "", null, null, null, false);
+               var builder = new RestContext.Builder(args);
+               assertNull(builder.lazyChildInit, "Default builder knob should 
be null");
+               builder.lazyChildInit(true);
+               assertTrue(builder.lazyChildInit, "Builder knob must be set to 
true after lazyChildInit(true)");
+               builder.lazyChildInit(false);
+               assertFalse(builder.lazyChildInit, "Builder knob must be set to 
false after lazyChildInit(false)");
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // a05: Lifecycle shutdown — materialized lazy child sees destroy(); 
never-invoked child is skipped.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       static final AtomicInteger A05_DESTROY_COUNT = new AtomicInteger(0);
+
+       @Rest(path = "/lifecycle-child")
+       public static class LifecycleChildResource {
+               @RestGet("/ping")
+               public String ping() {
+                       return "lifecycle-pong";
+               }
+
+               @RestDestroy
+               public void onDestroy() {
+                       A05_DESTROY_COUNT.incrementAndGet();
+               }
+       }
+
+       @Rest(path = "/never-called")
+       public static class NeverCalledLifecycle {
+               @RestGet("/ping")
+               public String ping() {
+                       return "never";
+               }
+
+               @RestDestroy
+               public void onDestroy() {
+                       // should NOT be called
+                       A05_DESTROY_COUNT.incrementAndGet();
+               }
+       }
+
+       @Rest(path = "/root", children = { LifecycleChildResource.class, 
NeverCalledLifecycle.class }, lazyChildren = "true")
+       public static class E_Parent extends BasicRestServletGroup {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Test
+       void 
a05_lifecycleShutdown_materializedChildSeesDestroy_neverInvokedSkipped() throws 
Exception {
+               A05_DESTROY_COUNT.set(0);
+               var parent = new E_Parent();
+               var client = MockRestClient.createLax(parent).build();
+
+               // Trigger materialization of lifecycle-child only.
+               client.get("/lifecycle-child/ping").run().assertStatus(200);
+
+               var rc = parent.getContext();
+               var lazyEntries = rc.getRestChildren().getLazyEntries();
+               assertTrue(lazyEntries.get("lifecycle-child").isMaterialized(), 
"lifecycle-child should be materialized");
+               assertFalse(lazyEntries.get("never-called").isMaterialized(), 
"never-called should NOT be materialized");
+
+               // Destroy the parent — only materialized child's @RestDestroy 
should fire.
+               rc.destroy();
+
+               assertEquals(1, A05_DESTROY_COUNT.get(),
+                       "@RestDestroy must fire exactly once (only on the 
materialized child)");
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // a06: Default eager — no lazyChildren annotation; child is built at 
startup.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(path = "/root", children = { AlphaResource.class })
+       public static class F_Parent extends BasicRestServletGroup {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @Test
+       void a06_defaultEager_noLazyAnnotation_childMaterializedAtStartup() 
throws Exception {
+               var parent = new F_Parent();
+               var client = MockRestClient.createLax(parent).build();
+               var rc = parent.getContext();
+
+               // No lazy annotation — default eager behavior preserved.
+               assertFalse(rc.isLazyChildren());
+               assertTrue(rc.getRestChildren().getLazyEntries().isEmpty(), "No 
lazy entries in eager mode");
+               assertFalse(rc.getRestChildren().asMap().isEmpty(), "Eager 
child must already be in the children map");
+
+               // Routing still works.
+               
client.get("/alpha/ping").run().assertStatus(200).assertContent("alpha-pong");
+       }
+}
diff --git a/juneau-utest/test-run-history.tsv 
b/juneau-utest/test-run-history.tsv
index 8d62b9d16b..e632c8c64f 100644
--- a/juneau-utest/test-run-history.tsv
+++ b/juneau-utest/test-run-history.tsv
@@ -53,3 +53,4 @@ timestamp     git_sha branch  tests_run       failures        
errors  skipped surefire_sec    wall_sec
 2026-05-28T21:29:51Z   b0dc55154f95    master  125891  0       0       21      
143
 2026-05-29T10:45:54Z   3dc947b24d41    master  125897  0       0       21      
151
 2026-05-29T13:48:36Z   c59fc14fac5e    master  126037  0       0       21      
144
+2026-05-29T14:57:25Z   830e0ca051c6    master  126043  0       0       21      
149


Reply via email to