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 0faddc98d3 feat: per-RestContext @Value resolution against 
@Rest(config=...) Configs via BeanStore seam (TODO-95)
0faddc98d3 is described below

commit 0faddc98d39730e5c275ae8cd6fb8b4ff80df25d
Author: James Bognar <[email protected]>
AuthorDate: Wed May 27 09:03:48 2026 -0400

    feat: per-RestContext @Value resolution against @Rest(config=...) Configs 
via BeanStore seam (TODO-95)
---
 .../juneau/commons/inject/ValueResolver.java       |  86 ++++-
 .../apache/juneau/commons/reflect/FieldInfo.java   |   4 +-
 .../juneau/commons/reflect/ParameterInfo.java      |   4 +-
 .../apache/juneau/commons/settings/Settings.java   |  75 +++++
 .../juneau/commons/svl/vars/PropertyVar.java       |  34 ++
 .../java/org/apache/juneau/rest/RestContext.java   | 113 ++++++-
 .../juneau/rest/Settings_NoLeakedSources_Test.java | 137 ++++++++
 .../RestContext_ValueAgainstConfig_Test.java       | 375 +++++++++++++++++++++
 8 files changed, 818 insertions(+), 10 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/ValueResolver.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/ValueResolver.java
index aab1469256..75dea4ad9a 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/ValueResolver.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/ValueResolver.java
@@ -159,10 +159,37 @@ public final class ValueResolver {
         * @return The coerced value.
         */
        public static Object resolve(String expression, Class<?> targetType, 
String siteDescription) {
+               return resolve(expression, targetType, siteDescription, null);
+       }
+
+       /**
+        * Same as {@link #resolve(String, Class, String)} but consults 
caller-scoped
+        * {@link PropertySource} beans resolved from the supplied {@link 
BeanStore} between
+        * the {@link Settings} local/global override stores and the {@link 
Settings} sources chain.
+        *
+        * <p>
+        * The {@code beanStore} parameter is preserved by-reference; if its
+        * {@code PropertySource}-typed contents change between calls, the next 
resolution picks up
+        * the current state. Passing {@code null} (or a {@code BeanStore} with 
no
+        * {@code PropertySource}-typed beans) is byte-for-byte equivalent to 
the zero-argument
+        * overload &mdash; no extra allocation and no behavior change.
+        *
+        * @param expression The {@code @Value} expression.
+        * @param targetType The target Java type. May be a primitive.
+        * @param siteDescription A human-readable description of the site 
(used in error messages).
+        * @param beanStore Caller-scoped {@link BeanStore} consulted for 
{@link PropertySource} beans.
+        *      May be {@code null}.
+        * @return The coerced value.
+        */
+       public static Object resolve(String expression, Class<?> targetType, 
String siteDescription, BeanStore beanStore) {
                if (expression == null)
                        return resolveCoerce(null, targetType, expression, 
siteDescription);
                var template = getCompiledTemplate(expression);
-               var resolved = 
template.resolve(VarResolver.DEFAULT.createSession());
+               var session = VarResolver.DEFAULT.createSession();
+               var sources = scopedSources(beanStore);
+               if (sources != null)
+                       session.bean(PropertySource[].class, sources);
+               var resolved = template.resolve(session);
                return resolveCoerce(resolved, targetType, expression, 
siteDescription);
        }
 
@@ -190,6 +217,28 @@ public final class ValueResolver {
         * @return Either a one-shot coerced value, or a {@code 
Supplier<String>} factory.
         */
        public static Object resolve(String expression, Class<?> targetType, 
Type genericTargetType, String siteDescription) {
+               return resolve(expression, targetType, genericTargetType, 
siteDescription, null);
+       }
+
+       /**
+        * Same as {@link #resolve(String, Class, Type, String)} but consults 
caller-scoped
+        * {@link PropertySource} beans resolved from the supplied {@link 
BeanStore}.
+        *
+        * <p>
+        * The {@code beanStore} reference is captured (not snapshotted) by the 
returned
+        * {@code Supplier<String>} when the declared field/parameter type is 
{@code Supplier<String>},
+        * so re-evaluating reads always see the current state of {@code 
beanStore.getBeansOfType(PropertySource.class)}.
+        *
+        * @param expression The {@code @Value} expression. May be {@code null}.
+        * @param targetType The erased target class.
+        * @param genericTargetType The declared generic type. May be {@code 
null}.
+        * @param siteDescription Human-readable site description for error 
messages.
+        * @param beanStore Caller-scoped {@link BeanStore} consulted for 
{@link PropertySource} beans.
+        *      May be {@code null}.
+        * @return Either a one-shot coerced value, or a {@code 
Supplier<String>} factory.
+        */
+       public static Object resolve(String expression, Class<?> targetType, 
Type genericTargetType, String siteDescription,
+                       BeanStore beanStore) {
                if (isSupplierOfString(targetType, genericTargetType)) {
                        if (expression == null)
                                return (Supplier<String>) () -> null;
@@ -201,9 +250,40 @@ public final class ValueResolver {
                                var literal = 
template.resolve(VarResolver.DEFAULT.createSession());
                                return (Supplier<String>) () -> literal;
                        }
-                       return 
template.asSupplierWithFreshSessions(VarResolver.DEFAULT);
+                       if (beanStore == null)
+                               return 
template.asSupplierWithFreshSessions(VarResolver.DEFAULT);
+                       // Capture the BeanStore by-reference so re-evaluating 
reads see the current state of
+                       // its PropertySource-typed beans (matches the 
"by-reference, not snapshot" contract).
+                       final BeanStore scope = beanStore;
+                       return (Supplier<String>) () -> {
+                               var s = VarResolver.DEFAULT.createSession();
+                               var sources = scopedSources(scope);
+                               if (sources != null)
+                                       s.bean(PropertySource[].class, sources);
+                               return template.resolve(s);
+                       };
                }
-               return resolve(expression, targetType, siteDescription);
+               return resolve(expression, targetType, siteDescription, 
beanStore);
+       }
+
+       /**
+        * Returns the caller-scoped {@link PropertySource} array for the 
supplied {@link BeanStore},
+        * or {@code null} if the store has no {@code PropertySource}-typed 
beans (matching the
+        * "no behavior change when no scoped sources are present" contract).
+        *
+        * <p>
+        * Walks {@link BeanStore#getBeansOfType(Class) 
beanStore.getBeansOfType(PropertySource.class)}.
+        * The returned array preserves the iteration order of {@code 
getBeansOfType} (parent-chain
+        * beans before local beans before overriding-parent beans, then sorted 
by
+        * {@code @Order/@Primary/@Bean.priority()}).
+        */
+       private static PropertySource[] scopedSources(BeanStore beanStore) {
+               if (beanStore == null)
+                       return null;
+               var map = beanStore.getBeansOfType(PropertySource.class);
+               if (map.isEmpty())
+                       return null;
+               return map.values().toArray(new PropertySource[0]);
        }
 
        /** Returns {@code true} if the declared field/parameter type is 
exactly {@code Supplier<String>}. */
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/FieldInfo.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/FieldInfo.java
index 26d416c62d..ee8ecc702b 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/FieldInfo.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/FieldInfo.java
@@ -607,7 +607,9 @@ public class FieldInfo extends AccessibleInfo implements 
Comparable<FieldInfo>,
                        var unwrapped = 
fieldType.unwrap(Optional.class).inner();
                        // Pass the declared (generic) field type so @Value 
Supplier<String> autodetects
                        // Field type IS the opt-in signal for re-evaluating 
reads (Supplier<String> autodetect).
-                       var resolved = ValueResolver.resolve(valueExpr, 
unwrapped, inner.getGenericType(), this.toString());
+                       // BeanStore is forwarded so caller-scoped 
PropertySource beans (e.g. per-RestContext
+                       // @Rest(config=...) Configs) participate in expression 
resolution alongside Settings.
+                       var resolved = ValueResolver.resolve(valueExpr, 
unwrapped, inner.getGenericType(), this.toString(), beanStore);
                        if (fieldType.is(Optional.class)) {
                                // VarResolver substitutes "" for a missing key 
with no default. Collapse both to
                                // Optional.empty() so @Value("${maybe}") 
Optional<T> behaves the same as Spring's.
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ParameterInfo.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ParameterInfo.java
index 3de9320b1c..6105948335 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ParameterInfo.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ParameterInfo.java
@@ -902,7 +902,9 @@ public class ParameterInfo extends ElementInfo implements 
Annotatable {
                        ValueResolver.checkInjectConflict(annos, 
this.toString());
                        // Pass the declared (generic) parameter type so @Value 
Supplier<String> autodetects
                        // Supplier<String> field/parameter type opts in to 
re-evaluating reads.
-                       return ValueResolver.resolve(valueExpr, ptu.inner(), 
inner.getParameterizedType(), this.toString());
+                       // BeanStore is forwarded so caller-scoped 
PropertySource beans (e.g. per-RestContext
+                       // @Rest(config=...) Configs) participate in expression 
resolution alongside Settings.
+                       return ValueResolver.resolve(valueExpr, ptu.inner(), 
inner.getParameterizedType(), this.toString(), beanStore);
                }
 
                if (JsrSupport.isProviderType(ptu.inner())) {
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/Settings.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/Settings.java
index be6fef0cc8..2336c08bbd 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/Settings.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/settings/Settings.java
@@ -377,6 +377,23 @@ public class Settings {
                return this;
        }
 
+       /**
+        * Returns the number of {@link PropertySource}s currently registered 
with this
+        * {@code Settings} instance.
+        *
+        * <p>
+        * Intended primarily for tests that need to verify nothing has leaked 
extra sources into the
+        * process-wide {@link #get() Settings.get()} singleton &mdash; the 
historical failure mode
+        * documented in {@code FINISHED-79-phase6-discovery-report.md} where a 
per-{@code RestContext}
+        * config bridge added a {@code ConfigPropertySource} per cached {@code 
RestContext} via
+        * {@link #addSource(PropertySource)} and never removed it.
+        *
+        * @return The current source count. Always {@code &gt;= 0}.
+        */
+       public int sourceCount() {
+               return sources.size();
+       }
+
        /**
         * Removes a previously-added property source.
         *
@@ -448,6 +465,64 @@ public class Settings {
                });
        }
 
+       /**
+        * Returns the override value for the specified property, consulting 
only the per-thread
+        * (local) and global stores &mdash; <i>not</i> the sources list.
+        *
+        * <p>
+        * Intended for callers (notably the {@code @Value} resolution path) 
that interleave a
+        * caller-scoped property source between the Settings override stores 
and the sources list.
+        * Using this method, the caller can honor the "{@link 
#setLocal(String, String) local}
+        * / {@link #setGlobal(String, String) global} override wins" contract 
before consulting
+        * its own scoped sources, and still fall through to {@link 
#get(String)} (which checks
+        * the sources list) afterwards without the override stores 
accidentally being consulted twice.
+        *
+        * <p>
+        * Lookup order honored by this method:
+        * <ol>
+        *      <li>Per-thread (local) store
+        *      <li>Global store
+        * </ol>
+        *
+        * <p>
+        * If a store is present with an empty value (i.e. an explicit null 
override), the returned
+        * {@link Optional} is {@code Optional.empty()} but {@link 
#isOverridden(String)} would
+        * return {@code true}. Callers needing to distinguish "absent" from 
"explicit null" should
+        * use {@link #isOverridden(String)} alongside this method.
+        *
+        * @param name The property name. Must not be <jk>null</jk>.
+        * @return The override value, or {@link Optional#empty()} if neither 
store carries an override.
+        * @see #isOverridden(String)
+        * @see #get(String)
+        */
+       public Optional<String> getOverride(String name) {
+               assertArgNotNull(ARG_name, name);
+               var v = localStore.get().get(name);
+               if (v.isPresent())
+                       return v.value();
+               v = globalStore.get().get(name);
+               if (v.isPresent())
+                       return v.value();
+               return Optional.empty();
+       }
+
+       /**
+        * Returns <jk>true</jk> if the specified property has an override in 
either the per-thread
+        * (local) or global store.
+        *
+        * <p>
+        * Distinguishes the "explicit null override" case &mdash; where {@link 
#getOverride(String)}
+        * returns {@link Optional#empty()} because the override value itself 
was {@code null} &mdash;
+        * from the "no override" case.
+        *
+        * @param name The property name. Must not be <jk>null</jk>.
+        * @return <jk>true</jk> if either store carries an override (even with 
a {@code null} value).
+        */
+       public boolean isOverridden(String name) {
+               assertArgNotNull(ARG_name, name);
+               return localStore.get().get(name).isPresent() || 
globalStore.get().get(name).isPresent();
+       }
+
        /**
         * Looks up a system property, returning a default value if not found.
         *
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/svl/vars/PropertyVar.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/svl/vars/PropertyVar.java
index 9aeba335b3..8dc9173ae3 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/svl/vars/PropertyVar.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/svl/vars/PropertyVar.java
@@ -24,6 +24,24 @@ import org.apache.juneau.commons.svl.*;
  *
  * <p>
  * The format for this var is <js>"$P{propertyName[,defaultValue]}"</js>.
+ *
+ * <h5 class='section'>Caller-scoped property sources:</h5>
+ * <p>
+ * Resolution consults the singleton {@link Settings} chain by default. 
Callers (notably the
+ * {@code @Value} injection path) that need to inject additional, caller-scoped
+ * {@link PropertySource}s in front of the global chain can attach them to the
+ * {@link VarResolverSession} as a session bean of type {@code 
PropertySource[]}:
+ *
+ * <p class='bjava'>
+ *     <jv>session</jv>.bean(PropertySource[].<jk>class</jk>, <jk>new</jk> 
PropertySource[] { <jv>source1</jv>, <jv>source2</jv> });
+ * </p>
+ *
+ * <p>
+ * When the session carries that bean, this var consults the array in order 
between the
+ * {@link Settings#getOverride(String) local/global override stores} (which 
still win &mdash;
+ * matching the existing test-override contract) and the regular {@link 
Settings#get(String)
+ * Settings sources chain}. Sources earlier in the array take precedence over 
sources later
+ * in the array. The contract is identical to today when no such session bean 
is attached.
  */
 public class PropertyVar extends DefaultingVar {
 
@@ -39,6 +57,22 @@ public class PropertyVar extends DefaultingVar {
 
        @Override /* Overridden from Var */
        public String resolve(VarResolverSession session, String key) {
+               var scoped = session == null ? null : 
session.getBean(PropertySource[].class).orElse(null);
+               if (scoped == null || scoped.length == 0)
+                       return Settings.get().get(key).orElse(null);
+
+               // Caller-scoped sources are present. Honor Settings 
local/global overrides first
+               // so test-override semantics still win over caller-scoped 
sources, then walk the
+               // caller-scoped chain, then fall through to the global sources 
list.
+               if (Settings.get().isOverridden(key))
+                       return Settings.get().getOverride(key).orElse(null);
+               for (var src : scoped) {
+                       if (src == null)
+                               continue;
+                       var r = src.get(key);
+                       if (r.isPresent())
+                               return r.value().orElse(null);
+               }
                return Settings.get().get(key).orElse(null);
        }
 }
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 c5c0ee7dff..14d0d77fb5 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
@@ -63,6 +63,7 @@ import org.apache.juneau.commons.function.Memoizer;
 import org.apache.juneau.commons.lang.*;
 import org.apache.juneau.commons.logging.Logger;
 import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.commons.settings.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.config.*;
 import org.apache.juneau.config.vars.*;
@@ -726,6 +727,107 @@ public class RestContext extends Context {
                // @formatter:on
        }
 
+       /**
+        * Registers a {@link PropertySource} bean named {@code "rest.config"} 
in this resource's
+        * {@code BeanStore} so that {@code @Value("${cfg-key}")} fields on the 
resource bean (and on
+        * request-scoped beans whose {@code BeanStore} parent-walks to this 
one) resolve against the
+        * resource's {@code @Rest(config=...)} {@link Config}(s).
+        *
+        * <p>
+        * Lookup order honored by the registered source:
+        * <ol>
+        *      <li>The child class's resolved {@link Config} &mdash; sourced 
from {@link #rawConfig} so that
+        *              any {@link Bean @Bean Config} factory-method override 
on the resource bean is preserved.
+        *      <li>Any additional, distinct {@code @Rest(config=...)} {@code 
Config}s declared on parent
+        *              classes (child-to-parent order, dedup'd by resolved 
file name). Child keys win on
+        *              collision; parent keys fill the gaps.
+        * </ol>
+        *
+        * <p>
+        * Critically, this registration lives on the per-resource {@code 
BeanStore} &mdash; <i>not</i> on
+        * the process-wide {@code Settings.get()} singleton. The static {@code 
RestContext} cache in
+        * {@code MockRestClient} (and any other long-lived cache) is therefore 
benign: leaked
+        * {@code RestContext}s carry their {@code PropertySource}s in their 
own {@code BeanStore}s.
+        * The global {@code Settings} source list does not grow, and {@code 
Settings.get(name)}'s
+        * reverse-order walk stays O(constant).
+        */
+       private void registerRestConfigPropertySources() {
+               var sources = collectRestConfigPropertySources();
+               if (sources.isEmpty())
+                       return;
+               final List<PropertySource> chain = List.copyOf(sources);
+               PropertySource src;
+               if (chain.size() == 1) {
+                       src = chain.get(0);
+               } else {
+                       src = name -> {
+                               for (var s : chain) {
+                                       var r = s.get(name);
+                                       if (r.isPresent())
+                                               return r;
+                               }
+                               return PropertyLookupResult.missing();
+                       };
+               }
+               beanStore.addBean(PropertySource.class, src, "rest.config");
+       }
+
+       /**
+        * Walks the {@code @Rest(config=...)} annotation chain (child-first) 
and builds a
+        * {@link ConfigPropertySource} for each distinct, non-empty config 
attribute.
+        *
+        * <p>
+        * The first (most-derived) slot reuses {@code rawConfig.get()} so that 
any {@link Bean @Bean}
+        * {@code Config} factory-method override on the resource bean is 
preserved &mdash; the rest of
+        * the framework already sees that exact instance via {@link 
#getConfig()} and {@code @RestInit}
+        * parameter resolution. Parent slots load via {@code 
Config.create().name(...)} since {@code @Bean}
+        * overrides apply to the most-derived class only.
+        *
+        * <p>
+        * Edge case: a resource with no {@code @Rest(config=...)} annotation 
but with a {@link Bean @Bean}
+        * {@code Config} factory method has a non-null {@code rawConfig.get()} 
and no annotation-driven
+        * slot. That single {@code rawConfig} is still registered so 
user-supplied {@code Config}s flow
+        * through to {@code @Value} resolution.
+        */
+       private List<PropertySource> collectRestConfigPropertySources() {
+               var bs = beanStore();
+               var vr = bs.getBean(VarResolver.class, 
PROP_bootstrapVarResolver).orElseGet(this::getBootstrapVarResolver);
+               var result = new ArrayList<PropertySource>();
+               var seen = new LinkedHashSet<String>();
+               // AnnotationProvider returns child-first; iterate in the same 
order so child wins on collision.
+               var anns = AnnotationProvider.INSTANCE.find(Rest.class, 
info(resourceClass));
+               for (var i = 0; i < anns.size(); i++) {
+                       var raw = anns.get(i).inner().config();
+                       if (raw == null || raw.isEmpty())
+                               continue;
+                       var resolvedName = vr.resolve(raw);
+                       if (resolvedName == null || resolvedName.isEmpty())
+                               continue;
+                       if (! seen.add(resolvedName))
+                               continue;
+                       Config cfg;
+                       if (result.isEmpty()) {
+                               // Most-derived child slot. rawConfig.get() 
captures @Bean Config override; reuse it.
+                               cfg = rawConfig.get();
+                       } else if ("SYSTEM_DEFAULT".equals(resolvedName)) {
+                               cfg = Config.getSystemDefault();
+                       } else {
+                               cfg = 
Config.create().varResolver(vr).name(resolvedName).build();
+                       }
+                       if (cfg != null)
+                               result.add(new ConfigPropertySource(cfg));
+               }
+               // Edge case: no @Rest(config=...) annotation produced a slot, 
but rawConfig.get() is non-null
+               // (e.g. a @Bean Config factory method without a matching 
@Rest(config=...) attribute, or the
+               // SYSTEM_DEFAULT branch fired without an annotation). Still 
register rawConfig as the sole source.
+               if (result.isEmpty()) {
+                       var rc = rawConfig.get();
+                       if (rc != null)
+                               result.add(new ConfigPropertySource(rc));
+               }
+               return result;
+       }
+
        private static final class LifecycleInvokerPair {
                final MethodList methods;
                final MethodInvoker[] invokers;
@@ -1846,11 +1948,12 @@ public class RestContext extends Context {
                        // runtime Config) — @RestInit hooks that take Config 
as a parameter will see the fully
                        // resolved instance instead of the raw bootstrap 
Config (9.5 behavior change).
                        rawConfig.get();
-                       // NOTE: per-RestContext bridging of the 
@Rest(config=...) Config into Settings.get()
-                       // was removed in 9.5 (perf regression — leaked 
ConfigPropertySource instances into the
-                       // global Settings list because MockRestClient caches 
RestContext instances statically
-                       // and never invokes destroy()). Per-RestContext 
@Value("${cfg-key}") resolution
-                       // against the resource-scoped Config is tracked as 
TODO-95 (BeanStore-routed lookup).
+                       // Per-RestContext @Value resolution bridge: register 
the @Rest(config=...) Configs as
+                       // PropertySource beans inside THIS resource's 
BeanStore (NOT on the process-wide
+                       // Settings.get() singleton — that path was a 6.7x perf 
regression because
+                       // MockRestClient's static RestContext cache leaks 
instances and Settings.get()'s source
+                       // list grew unbounded). Per-resource isolation comes 
for free from the BeanStore scope.
+                       registerRestConfigPropertySources();
 
                        // Register memoizer-backed defaults for every 
framework-managed type.  These sit at the
                        // bottom of the precedence order and only fire when no 
@Bean method, no programmatic
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/Settings_NoLeakedSources_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/Settings_NoLeakedSources_Test.java
new file mode 100644
index 0000000000..edbc3d0bed
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/Settings_NoLeakedSources_Test.java
@@ -0,0 +1,137 @@
+/*
+ * 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 org.apache.juneau.*;
+import org.apache.juneau.commons.settings.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Regression test guarding the BeanStore-routed {@code @Rest(config=...)} 
bridge against
+ * re-introducing the per-{@code RestContext} {@link Settings} source leak 
that caused the
+ * 6.7&times; full-suite slowdown during an earlier prototype.
+ *
+ * <p>
+ * In FINISHED-79, an earlier draft of @Rest(config=...) bridging registered a
+ * {@code ConfigPropertySource} per resolved RestContext via {@code 
Settings.get().addSource(...)}.
+ * MockRestClient statically caches RestContext instances and never invokes 
{@code destroy()},
+ * so the global {@code Settings} source list grew unbounded across the test 
suite — a 6.7x
+ * wall-clock regression as {@code Settings.get(name)}'s reverse-walk got O(N).
+ *
+ * <p>
+ * This test asserts that:
+ * <ol>
+ *     <li>Building a single @Rest(config=...) MockRestClient adds zero new 
sources to
+ *             {@code Settings.get()}.
+ *     <li>Building hundreds of mixed @Rest / @Rest(config=...) 
MockRestClients leaves the source
+ *             count unchanged.
+ * </ol>
+ *
+ * <p>
+ * If a future change accidentally routes @Rest(config=...) Configs through
+ * {@code Settings.get().addSource(...)} (or any other process-wide register), 
this test fails
+ * immediately, well before the performance regression shows up in wall-clock 
test time.
+ */
+@SuppressWarnings({
+       "serial" // BasicRestServlet is Serializable; not relevant in test 
fixtures.
+})
+class Settings_NoLeakedSources_Test extends TestBase {
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Distinct @Rest resources — mixed config=/no-config — to exercise 
both code paths.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest public static class NoConfig01 extends BasicRestServlet {}
+       @Rest public static class NoConfig02 extends BasicRestServlet {}
+       @Rest public static class NoConfig03 extends BasicRestServlet {}
+       @Rest public static class NoConfig04 extends BasicRestServlet {}
+       @Rest public static class NoConfig05 extends BasicRestServlet {}
+       @Rest public static class NoConfig06 extends BasicRestServlet {}
+       @Rest public static class NoConfig07 extends BasicRestServlet {}
+       @Rest public static class NoConfig08 extends BasicRestServlet {}
+       @Rest public static class NoConfig09 extends BasicRestServlet {}
+       @Rest public static class NoConfig10 extends BasicRestServlet {}
+
+       // We deliberately use a config name that points at a non-existent 
file. The
+       // FileStore.DEFAULT-backed Config still builds (the in-memory 
ConfigMap is empty) and the
+       // bridge still registers a "rest.config" PropertySource in the 
RestContext's BeanStore —
+       // exactly the path we want to exercise without polluting the test cwd 
with fixture files.
+       @Rest(config="settings_noleakedsources_test_a.cfg") public static class 
WithConfig01 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_b.cfg") public static class 
WithConfig02 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_c.cfg") public static class 
WithConfig03 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_d.cfg") public static class 
WithConfig04 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_e.cfg") public static class 
WithConfig05 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_f.cfg") public static class 
WithConfig06 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_g.cfg") public static class 
WithConfig07 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_h.cfg") public static class 
WithConfig08 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_i.cfg") public static class 
WithConfig09 extends BasicRestServlet {}
+       @Rest(config="settings_noleakedsources_test_j.cfg") public static class 
WithConfig10 extends BasicRestServlet {}
+
+       private static final Class<?>[] NO_CONFIG_CLASSES = {
+               NoConfig01.class, NoConfig02.class, NoConfig03.class, 
NoConfig04.class, NoConfig05.class,
+               NoConfig06.class, NoConfig07.class, NoConfig08.class, 
NoConfig09.class, NoConfig10.class,
+       };
+
+       private static final Class<?>[] WITH_CONFIG_CLASSES = {
+               WithConfig01.class, WithConfig02.class, WithConfig03.class, 
WithConfig04.class, WithConfig05.class,
+               WithConfig06.class, WithConfig07.class, WithConfig08.class, 
WithConfig09.class, WithConfig10.class,
+       };
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance — single instantiation adds zero new sources.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void a01_singleInstantiation_zeroNewSources() {
+               var before = Settings.get().sourceCount();
+               MockRestClient.build(WithConfig01.class);
+               var after = Settings.get().sourceCount();
+               assertEquals(before, after,
+                       "@Rest(config=...) must not leak ConfigPropertySource 
instances into Settings.get().");
+       }
+
+       @Test void a02_singleInstantiation_noConfig_zeroNewSources() {
+               var before = Settings.get().sourceCount();
+               MockRestClient.build(NoConfig01.class);
+               var after = Settings.get().sourceCount();
+               assertEquals(before, after,
+                       "@Rest (no config=) must not add any new Settings 
sources.");
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance — 200 instantiations across mixed classes leave the 
source count unchanged.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void a03_twoHundredInstantiations_zeroNewSources() {
+               var before = Settings.get().sourceCount();
+               // 200 builds = 10 base classes × 10 builds × 2 mixes (config + 
no-config) = matches the
+               // FINISHED-79 cache-saturation threshold that produced the 
6.7x regression.
+               for (var i = 0; i < 10; i++) {
+                       for (var c : NO_CONFIG_CLASSES)
+                               MockRestClient.build(c);
+                       for (var c : WITH_CONFIG_CLASSES)
+                               MockRestClient.build(c);
+               }
+               var after = Settings.get().sourceCount();
+               assertEquals(before, after,
+                       "After 200 @Rest builds, Settings.get() source list 
must not grow (FINISHED-79 regression guard).");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/RestContext_ValueAgainstConfig_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/RestContext_ValueAgainstConfig_Test.java
new file mode 100644
index 0000000000..377dbb0e0a
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/RestContext_ValueAgainstConfig_Test.java
@@ -0,0 +1,375 @@
+/*
+ * 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.annotation;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.commons.settings.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Acceptance tests for per-{@code RestContext} {@code @Value} resolution 
against
+ * {@code @Rest(config=...)} {@link org.apache.juneau.config.Config}s.
+ *
+ * <p>
+ * The test writes its config fixtures to the cwd in {@link BeforeAll 
@BeforeAll} (because the
+ * default {@link org.apache.juneau.config.store.FileStore#DEFAULT 
FileStore.DEFAULT} resolves
+ * names against the cwd; with Maven Surefire that is the module directory). 
The cwd-resident
+ * files are removed in {@link AfterAll @AfterAll}. Names use a fixed {@code 
todo95-} prefix so
+ * they are easy to spot and clean up by hand should a test crash partway 
through.
+ *
+ * <p>
+ * Each resource class uses a unique name so {@code MockRestClient}'s static 
{@code RestContext}
+ * cache does not return a stale {@code RestContext} from an unrelated test 
class.
+ */
+@SuppressWarnings({
+       "serial" // Test resources extend BasicRestServlet which is 
serializable; not relevant.
+})
+class RestContext_ValueAgainstConfig_Test extends TestBase {
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Fixture management — write cfg files to cwd before tests, delete 
them after.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       private static final String CFG_A = "todo95-rcvac-a.cfg";
+       private static final String CFG_B = "todo95-rcvac-b.cfg";
+       private static final String CFG_PARENT = "todo95-rcvac-parent.cfg";
+       private static final String CFG_CHILD = "todo95-rcvac-child.cfg";
+       private static final String CFG_OVERRIDE = "todo95-rcvac-override.cfg";
+       private static final String CFG_ASYNC = "todo95-rcvac-async.cfg";
+
+       @BeforeAll
+       static void writeFixtures() throws IOException {
+               write(CFG_A,
+                       "api.key = secret-A",
+                       "api.url = https://a.example.org/";,
+                       "foo = A",
+                       "[section]",
+                       "nested = nested-A");
+               write(CFG_B,
+                       "api.key = secret-B",
+                       "foo = B");
+               write(CFG_PARENT,
+                       "parent.only = parent-value",
+                       "shared = parent-shared");
+               write(CFG_CHILD,
+                       "child.only = child-value",
+                       "shared = child-shared");
+               write(CFG_OVERRIDE,
+                       "override.key = from-config");
+               write(CFG_ASYNC,
+                       "async.greeting = hello-from-config");
+       }
+
+       @AfterAll
+       static void removeFixtures() throws IOException {
+               for (var n : List.of(CFG_A, CFG_B, CFG_PARENT, CFG_CHILD, 
CFG_OVERRIDE, CFG_ASYNC))
+                       Files.deleteIfExists(Path.of(n));
+       }
+
+       private static void write(String name, String... lines) throws 
IOException {
+               Files.writeString(Path.of(name), 
String.join(System.lineSeparator(), lines) + System.lineSeparator(),
+                       StandardCharsets.UTF_8);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance #1 — Happy path. @Rest(config=...) + @Value("${cfg-key}") 
resolves to the Config value.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(config=CFG_A)
+       public static class HappyPathResource extends BasicRestServlet {
+               @Value("${api.key}")
+               String apiKey;
+
+               @Value("${api.url}")
+               String apiUrl;
+
+               @Value("${section/nested}")
+               String nested;
+       }
+
+       @Test void a01_happyPath_configKeyResolves() throws Exception {
+               var rc = build(HappyPathResource.class);
+               var bean = (HappyPathResource) rc.getResource();
+               assertEquals("secret-A", bean.apiKey);
+               assertEquals("https://a.example.org/";, bean.apiUrl);
+               assertEquals("nested-A", bean.nested);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance #2 — Fall-through. Key absent from Config falls back to 
Settings/env.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(config=CFG_A)
+       public static class FallThroughResource extends BasicRestServlet {
+               // "api.key" is in the Config; "todo95.absent.key" is not — 
should hit the default branch.
+               @Value("${todo95.absent.key:default-fallback}")
+               String absentDefault;
+
+               // Use a Settings.setGlobal key during the test to prove 
Settings sources still resolve.
+               @Value("${todo95.absent.from.settings}")
+               String fromSettings;
+       }
+
+       @Test void a02_fallThrough_defaultBranch_andSettings() throws Exception 
{
+               Settings.get().setGlobal("todo95.absent.from.settings", 
"from-settings");
+               try {
+                       var rc = build(FallThroughResource.class);
+                       var bean = (FallThroughResource) rc.getResource();
+                       assertEquals("default-fallback", bean.absentDefault);
+                       assertEquals("from-settings", bean.fromSettings);
+               } finally {
+                       
Settings.get().unsetGlobal("todo95.absent.from.settings");
+               }
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance #3 — Resource isolation. Resource A and Resource B both 
have key "foo" but with
+       // different values; each sees its own.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(config=CFG_A)
+       public static class IsolationResourceA extends BasicRestServlet {
+               @Value("${foo}")
+               String foo;
+       }
+
+       @Rest(config=CFG_B)
+       public static class IsolationResourceB extends BasicRestServlet {
+               @Value("${foo}")
+               String foo;
+       }
+
+       @Test void a03_resourceIsolation_each_sees_own_config() throws 
Exception {
+               var rcA = build(IsolationResourceA.class);
+               var rcB = build(IsolationResourceB.class);
+               assertEquals("A", ((IsolationResourceA) rcA.getResource()).foo);
+               assertEquals("B", ((IsolationResourceB) rcB.getResource()).foo);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance #4 — No-config resource. @Rest with no config attribute 
resolves via Settings only.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest
+       public static class NoConfigResource extends BasicRestServlet {
+               @Value("${todo95.noconfig.key:noconfig-default}")
+               String value;
+
+               @Value("${todo95.noconfig.fromSettings}")
+               String fromSettings;
+       }
+
+       @Test void a04_noConfig_falls_through_to_settings_only() throws 
Exception {
+               Settings.get().setGlobal("todo95.noconfig.fromSettings", 
"settings-value");
+               try {
+                       var rc = build(NoConfigResource.class);
+                       var bean = (NoConfigResource) rc.getResource();
+                       assertEquals("noconfig-default", bean.value);
+                       assertEquals("settings-value", bean.fromSettings);
+               } finally {
+                       
Settings.get().unsetGlobal("todo95.noconfig.fromSettings");
+               }
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance #5 — Inheritance. Parent + child @Rest(config=...) 
annotations: child wins on
+       // collision, parent fills the gaps.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(config=CFG_PARENT)
+       public static class InheritanceParent extends BasicRestServlet {
+               @Value("${shared}")
+               String shared;
+
+               @Value("${parent.only}")
+               String parentOnly;
+
+               @Value("${child.only:absent-on-parent}")
+               String childOnly;
+       }
+
+       @Rest(config=CFG_CHILD)
+       public static class InheritanceChild extends InheritanceParent {}
+
+       @Test void a05_inheritance_child_wins_parent_fills_gaps() throws 
Exception {
+               var rc = build(InheritanceChild.class);
+               var bean = (InheritanceChild) rc.getResource();
+               // Child wins on collision: shared resolves to child's value.
+               assertEquals("child-shared", bean.shared);
+               // Parent fills the gap: parent.only is only in parent.cfg, 
child sees it.
+               assertEquals("parent-value", bean.parentOnly);
+               // Child has its own key:
+               assertEquals("child-value", bean.childOnly);
+       }
+
+       @Test void a05b_parent_alone_resolves_parent_keys() throws Exception {
+               var rc = build(InheritanceParent.class);
+               var bean = (InheritanceParent) rc.getResource();
+               assertEquals("parent-shared", bean.shared);
+               assertEquals("parent-value", bean.parentOnly);
+               // child.only is absent from parent.cfg → falls through to the 
default branch.
+               assertEquals("absent-on-parent", bean.childOnly);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance #6 — Settings.setLocal()/setGlobal() overrides still win 
over resource Config.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       // MockRestClient caches RestContext per class, so a @Value String 
field is captured ONCE at
+       // resource-bean construction time. To exercise the "override wins 
later" path we use a
+       // Supplier<String> @Value — it re-evaluates on each .get() call 
against a fresh
+       // VarResolverSession that carries the resource's BeanStore as a 
session bean.
+       @Rest(config=CFG_OVERRIDE)
+       public static class OverrideResource extends BasicRestServlet {
+               @Value("${override.key}")
+               Supplier<String> value;
+       }
+
+       @Test void a06_setGlobal_overrides_resource_config() throws Exception {
+               var rc = build(OverrideResource.class);
+               var bean = (OverrideResource) rc.getResource();
+               // No override active → resource Config value wins.
+               assertEquals("from-config", bean.value.get());
+
+               Settings.get().setGlobal("override.key", 
"from-settings-global");
+               try {
+                       // Override active → setGlobal must beat resource 
Config (per OQA #3).
+                       assertEquals("from-settings-global", bean.value.get(),
+                               "Settings.setGlobal must win over resource 
@Rest(config) values.");
+               } finally {
+                       Settings.get().unsetGlobal("override.key");
+               }
+               // Override gone → resource Config value visible again 
(Supplier re-evaluates).
+               assertEquals("from-config", bean.value.get());
+       }
+
+       @Test void a06b_setLocal_overrides_resource_config() throws Exception {
+               var rc = build(OverrideResource.class);
+               var bean = (OverrideResource) rc.getResource();
+
+               Settings.get().setLocal("override.key", "from-settings-local");
+               try {
+                       assertEquals("from-settings-local", bean.value.get(),
+                               "Settings.setLocal must win over resource 
@Rest(config) values.");
+               } finally {
+                       Settings.get().unsetLocal("override.key");
+               }
+               assertEquals("from-config", bean.value.get());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance #7 — Async / virtual-thread composition. 
CompletableFuture<String> @RestOp method
+       // body reads a @Value field; assertion holds whether the future 
resolves on the dispatch thread
+       // or a worker thread.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Rest(config=CFG_ASYNC)
+       public static class AsyncResource extends BasicRestServlet {
+               @Value("${async.greeting}")
+               String greeting;
+
+               @Value("${async.greeting}")
+               Supplier<String> greetingSupplier;
+       }
+
+       @Test void a07_async_composition_value_visible() throws Exception {
+               var rc = build(AsyncResource.class);
+               var bean = (AsyncResource) rc.getResource();
+               // The field value is captured at injection time — verify 
directly on the bean.
+               assertEquals("hello-from-config", bean.greeting);
+               // The Supplier<String> re-evaluates on every .get() call.
+               assertEquals("hello-from-config", bean.greetingSupplier.get());
+
+               // Re-evaluation from a worker thread (CompletableFuture) must 
see the same value because
+               // the BeanStore reference captured by the Supplier 
participates regardless of which thread
+               // calls .get() — i.e., the resolution is not pinned to the 
dispatch thread.
+               var worker = 
CompletableFuture.supplyAsync(bean.greetingSupplier::get).get(5, 
TimeUnit.SECONDS);
+               assertEquals("hello-from-config", worker);
+       }
+
+       @Rest(config=CFG_ASYNC, virtualThreads="true")
+       public static class AsyncVirtualThreadsResource extends 
BasicRestServlet {
+               @Value("${async.greeting}")
+               String greeting;
+
+               @Value("${async.greeting}")
+               Supplier<String> greetingSupplier;
+       }
+
+       @Test void a07b_virtualThreads_value_visible() throws Exception {
+               var rc = build(AsyncVirtualThreadsResource.class);
+               var bean = (AsyncVirtualThreadsResource) rc.getResource();
+               // Field injection captured at construction time (the BeanStore 
had the rest.config source
+               // in place by then, so this verifies the construction-time 
path holds when the resource is
+               // configured with virtualThreads="true").
+               assertEquals("hello-from-config", bean.greeting);
+               // Re-evaluation from a worker thread continues to see the 
value (BeanStore-by-reference
+               // capture in ValueResolver's supplier path, not pinned to the 
dispatch thread).
+               var worker = 
CompletableFuture.supplyAsync(bean.greetingSupplier::get).get(5, 
TimeUnit.SECONDS);
+               assertEquals("hello-from-config", worker);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Acceptance #8 — Registration is under name "rest.config" in the 
resource BeanStore.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void a08_propertySource_registered_under_rest_config_name() 
throws Exception {
+               var rc = build(HappyPathResource.class);
+               var src = rc.getBeanStore().getBean(PropertySource.class, 
"rest.config").orElse(null);
+               assertNotNull(src, "Expected a PropertySource bean registered 
as \"rest.config\" in the resource BeanStore.");
+               assertEquals("secret-A", 
src.get("api.key").value().orElse(null));
+       }
+
+       @Test void a08b_no_config_no_registration() throws Exception {
+               // A @Rest resource with no config= attribute and no @Bean 
Config method should leave the
+               // "rest.config" slot empty (rawConfig.get() returns an empty 
Config that the bridge skips).
+               var rc = build(NoConfigResource.class);
+               var src = rc.getBeanStore().getBean(PropertySource.class, 
"rest.config").orElse(null);
+               // An empty Config still has the bridge registered (since 
rawConfig.get() is non-null), but
+               // querying any unknown key yields missing. Either no bean 
registered OR a bean that returns
+               // missing for an arbitrary key is acceptable; assert the 
latter.
+               if (src != null)
+                       assertFalse(src.get("definitely-not-here").isPresent());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Helper — build a RestContext and look it up in the global registry 
so the test can read
+       // back the resource bean and the BeanStore directly.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       private static RestContext build(Class<?> resourceClass) throws 
Exception {
+               // MockRestClient.build() instantiates a RestContext for the 
resource class (and caches it
+               // statically); look the result up in the global registry to 
read the injected fields.
+               MockRestClient.build(resourceClass);
+               var rc = RestContext.getGlobalRegistry().get(resourceClass);
+               assertNotNull(rc, "RestContext for " + 
resourceClass.getSimpleName() + " not in REGISTRY after build.");
+               return rc;
+       }
+}


Reply via email to