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 d303a7f57c refactor: align bean-store precedence and make RestContext
eager init optional
d303a7f57c is described below
commit d303a7f57c4648b1a8784150ad2a86240474fad3
Author: James Bognar <[email protected]>
AuthorDate: Thu May 7 13:40:02 2026 -0400
refactor: align bean-store precedence and make RestContext eager init
optional
---
.../juneau/commons/inject/BasicBeanStore2.java | 137 ++++++++-
.../juneau/commons/inject/WritableBeanStore.java | 31 ++
.../java/org/apache/juneau/cp/BasicBeanStore.java | 159 +++++++++-
.../java/org/apache/juneau/rest/RestContext.java | 328 ++++++++++++---------
.../apache/juneau/rest/RestServerConstants.java | 3 +
.../org/apache/juneau/rest/annotation/Rest.java | 27 +-
.../juneau/rest/annotation/RestAnnotation.java | 19 ++
.../apache/juneau/rest/annotation/RestInject.java | 27 ++
.../juneau/commons/inject/BasicBeanStore_Test.java | 191 ++++++++++++
.../java/org/apache/juneau/cp/BeanStore_Test.java | 89 ++++++
.../juneau/rest/RestContext_Precedence_Test.java | 128 ++++++++
11 files changed, 976 insertions(+), 163 deletions(-)
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BasicBeanStore2.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BasicBeanStore2.java
index f365319be4..1c5fed3531 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BasicBeanStore2.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BasicBeanStore2.java
@@ -40,6 +40,16 @@ import org.apache.juneau.commons.collections.*;
* <p>
* This implementation is thread-safe and supports parent bean stores for
hierarchical bean resolution.
*
+ * <h5 class='section'>Resolution order:</h5>
+ * <p>
+ * Bean lookups consult the following sources in this order, returning the
first match:
+ * <ol>
+ * <li>{@linkplain #BasicBeanStore2(BeanStore,BeanStore) Overriding
parent} (e.g. a Spring application context bridge), if non-{@code null}.
+ * <li>Local entries added via {@link #addBean(Class,Object) addBean} /
{@link #addSupplier(Class,Supplier) addSupplier}.
+ * <li>Regular {@linkplain #BasicBeanStore2(BeanStore) parent}, if
non-{@code null}.
+ * <li>Local default suppliers added via {@link
#addDefaultSupplier(Class,Supplier) addDefaultSupplier} (memoizer-backed
framework defaults).
+ * </ol>
+ *
* <h5 class='section'>See Also:</h5><ul>
* <li class='jc'>{@link BeanStore} - Read-only bean lookup interface
* <li class='jc'>{@link WritableBeanStore} - Writable bean store interface
@@ -52,23 +62,54 @@ public class BasicBeanStore2 implements WritableBeanStore {
// Property name constants
private static final String PROP_bean = "bean";
+ private static final String PROP_defaults = "defaults";
private static final String PROP_entries = "entries";
private static final String PROP_identity = "identity";
private static final String PROP_name = "name";
+ private static final String PROP_overridingParent = "overridingParent";
private static final String PROP_parent = "parent";
private static final String PROP_type = "type";
private final ConcurrentHashMap<Class<?>, ConcurrentHashMap<String,
Supplier<?>>> entries;
+ private final ConcurrentHashMap<Class<?>, ConcurrentHashMap<String,
Supplier<?>>> defaults;
private final BeanStore parent;
+ private final BeanStore overridingParent;
/**
* Constructor.
*
- * @param parent The parent bean store. Can be <jk>null</jk>. Bean
searches are performed recursively up this parent chain.
+ * @param parent The parent bean store. Can be <jk>null</jk>. Bean
searches are performed recursively up this parent chain
+ * <i>after</i> local entries are checked.
*/
public BasicBeanStore2(BeanStore parent) {
+ this(parent, null);
+ }
+
+ /**
+ * Constructor that accepts an overriding parent bean store.
+ *
+ * <p>
+ * The {@code overridingParent} is consulted <i>before</i> any local
entries during bean lookup, allowing an outer
+ * scope (typically a Spring {@code ApplicationContext} bridge) to take
precedence over local registrations. The
+ * regular {@code parent} continues to be consulted as a fallback after
local entries.
+ *
+ * <p>
+ * Final resolution order:
+ * <ol>
+ * <li>{@code overridingParent} (if non-{@code null})
+ * <li>local entries (added via {@link #addBean(Class,Object)
addBean} / {@link #addSupplier(Class,Supplier) addSupplier})
+ * <li>regular {@code parent} (if non-{@code null})
+ * <li>local default suppliers (added via {@link
#addDefaultSupplier(Class,Supplier) addDefaultSupplier})
+ * </ol>
+ *
+ * @param parent The parent bean store, used as a fallback after local
entries. Can be <jk>null</jk>.
+ * @param overridingParent The overriding parent bean store, consulted
before local entries. Can be <jk>null</jk>.
+ */
+ public BasicBeanStore2(BeanStore parent, BeanStore overridingParent) {
this.parent = parent;
+ this.overridingParent = overridingParent;
entries = new ConcurrentHashMap<>();
+ defaults = new ConcurrentHashMap<>();
addSupplier(BasicBeanStore2.class, ()->this, null);
}
@@ -162,6 +203,45 @@ public class BasicBeanStore2 implements WritableBeanStore {
return this;
}
+ /**
+ * Adds a fallback supplier for an unnamed bean of the specified type
to this store.
+ *
+ * <p>
+ * Default suppliers are consulted only after local {@linkplain
#addBean(Class,Object) entries} and the regular
+ * {@linkplain #BasicBeanStore2(BeanStore) parent} have been searched.
They are intended for memoizer-backed
+ * framework defaults that should not shadow explicit user
registrations or beans inherited from an
+ * {@linkplain #BasicBeanStore2(BeanStore,BeanStore) overriding parent}
(e.g. Spring).
+ *
+ * @param <T> The bean type.
+ * @param beanType The bean type.
+ * @param supplier The bean supplier.
+ * @return This object.
+ */
+ @Override
+ public <T> BasicBeanStore2 addDefaultSupplier(Class<T> beanType,
Supplier<T> supplier) {
+ return addDefaultSupplier(beanType, supplier, null);
+ }
+
+ /**
+ * Adds a fallback supplier for a named bean of the specified type to
this store.
+ *
+ * <p>
+ * See {@link #addDefaultSupplier(Class,Supplier)} for ordering
semantics.
+ *
+ * @param <T> The bean type.
+ * @param beanType The bean type.
+ * @param supplier The bean supplier.
+ * @param name The bean name. Can be <jk>null</jk>.
+ * @return This object.
+ */
+ @Override
+ public <T> BasicBeanStore2 addDefaultSupplier(Class<T> beanType,
Supplier<T> supplier, String name) {
+ var typeMap = defaults.computeIfAbsent(beanType, k -> new
ConcurrentHashMap<>());
+ var key = emptyIfNull(name);
+ typeMap.put(key, supplier);
+ return this;
+ }
+
/**
* Removes all beans from this store.
*
@@ -173,6 +253,7 @@ public class BasicBeanStore2 implements WritableBeanStore {
@Override
public BasicBeanStore2 clear() {
entries.clear();
+ defaults.clear();
return this;
}
@@ -224,15 +305,20 @@ public class BasicBeanStore2 implements WritableBeanStore
{
"unchecked" // Type erasure requires cast to Map<String,T>
})
public <T> Map<String,T> getBeansOfType(Class<T> beanType) {
+ // Build the result respecting the priority order used by
getBean / resolve:
+ // defaults (lowest) < parent < entries (local) <
overridingParent (highest)
+ // Higher-priority maps overwrite lower-priority ones with the
same name.
Map<String,T> result = map();
- if (nn(parent)) {
- var parentBeans = parent.getBeansOfType(beanType);
- parentBeans.forEach(result::put);
- }
+ var defaultMap = defaults.get(beanType);
+ if (nn(defaultMap))
+ defaultMap.forEach((name, supplier) -> result.put(name,
(T)supplier.get()));
+ if (nn(parent))
+ parent.getBeansOfType(beanType).forEach(result::put);
var typeMap = entries.get(beanType);
- if (nn(typeMap)) {
+ if (nn(typeMap))
typeMap.forEach((name, supplier) -> result.put(name,
(T)supplier.get()));
- }
+ if (nn(overridingParent))
+
overridingParent.getBeansOfType(beanType).forEach(result::put);
return result;
}
@@ -295,6 +381,13 @@ public class BasicBeanStore2 implements WritableBeanStore {
"unchecked" // Type erasure requires cast for supplier
resolution
})
protected <T> Optional<Supplier<T>> resolve(Class<T> beanType, String
name) {
+ // (1) Overriding parent (e.g. Spring) — wins over local
entries.
+ if (nn(overridingParent)) {
+ var fromOverriding =
overridingParent.getBeanSupplier(beanType, name);
+ if (fromOverriding.isPresent())
+ return fromOverriding;
+ }
+ // (2) Local regular entries.
var typeMap = entries.get(beanType);
if (nn(typeMap)) {
var key = emptyIfNull(name);
@@ -302,8 +395,20 @@ public class BasicBeanStore2 implements WritableBeanStore {
if (nn(supplier))
return opt((Supplier<T>)supplier);
}
- if (nn(parent))
- return parent.getBeanSupplier(beanType, name);
+ // (3) Regular parent fallback.
+ if (nn(parent)) {
+ var fromParent = parent.getBeanSupplier(beanType, name);
+ if (fromParent.isPresent())
+ return fromParent;
+ }
+ // (4) Local default suppliers (memoizer-backed framework
defaults).
+ var defaultMap = defaults.get(beanType);
+ if (nn(defaultMap)) {
+ var key = emptyIfNull(name);
+ var supplier = defaultMap.get(key);
+ if (nn(supplier))
+ return opt((Supplier<T>)supplier);
+ }
return opte();
}
@@ -335,8 +440,22 @@ public class BasicBeanStore2 implements WritableBeanStore {
.a(PROP_type, cns(type))
.a(PROP_bean, id(supplier.get()))
.a(PROP_name, name))));
+ var defaultList = list();
+ defaults.forEach((type, typeMap) -> typeMap.forEach((name,
supplier) -> defaultList.add(filteredBeanPropertyMap()
+ .a(PROP_type, cns(type))
+ .a(PROP_bean, id(supplier.get()))
+ .a(PROP_name, name))));
+ Object overridingParentValue = null;
+ if (nn(overridingParent)) {
+ if (overridingParent instanceof BasicBeanStore2 op2)
+ overridingParentValue = op2.properties();
+ else
+ overridingParentValue = s(overridingParent);
+ }
return filteredBeanPropertyMap()
.a(PROP_entries, entryList)
+ .a(PROP_defaults, defaultList.isEmpty() ? null :
defaultList)
+ .a(PROP_overridingParent, overridingParentValue)
.a(PROP_identity, id(this))
.a(PROP_parent, parent instanceof BasicBeanStore2
parent2 ? parent2.properties() : s(parent));
// @formatter:on
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/WritableBeanStore.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/WritableBeanStore.java
index a548948330..b85ecea82e 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/WritableBeanStore.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/WritableBeanStore.java
@@ -99,6 +99,37 @@ public interface WritableBeanStore extends BeanStore {
*/
<T> WritableBeanStore addSupplier(Class<T> beanType, Supplier<T>
supplier, String name);
+ /**
+ * Adds a fallback supplier for an unnamed bean of the specified type
to this store.
+ *
+ * <p>
+ * Default suppliers are consulted only after regular {@linkplain
#addBean(Class,Object) entries} and the regular {@linkplain
BasicBeanStore2#BasicBeanStore2(BeanStore) parent}
+ * chain have been searched. They serve as a "use this if nothing else
has provided one" hook,
+ * useful for memoizer-backed framework defaults that should not shadow
explicit user
+ * registrations or beans inherited from an overriding parent (e.g. a
Spring application context).
+ *
+ * @param <T> The bean type.
+ * @param beanType The bean type.
+ * @param supplier The bean supplier. Must not be <jk>null</jk>.
+ * @return This object for method chaining.
+ */
+ <T> WritableBeanStore addDefaultSupplier(Class<T> beanType, Supplier<T>
supplier);
+
+ /**
+ * Adds a fallback supplier for a named bean of the specified type to
this store.
+ *
+ * <p>
+ * Default suppliers are consulted only after regular {@linkplain
#addBean(Class,Object,String) entries} and the regular
+ * parent chain have been searched. See {@link
#addDefaultSupplier(Class,Supplier)}.
+ *
+ * @param <T> The bean type.
+ * @param beanType The bean type.
+ * @param supplier The bean supplier. Must not be <jk>null</jk>.
+ * @param name The bean name. Can be <jk>null</jk> for unnamed beans.
+ * @return This object for method chaining.
+ */
+ <T> WritableBeanStore addDefaultSupplier(Class<T> beanType, Supplier<T>
supplier, String name);
+
/**
* Removes all beans from this store.
*
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/BasicBeanStore.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/BasicBeanStore.java
index 01b3c58d39..7d0829921d 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/BasicBeanStore.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/BasicBeanStore.java
@@ -53,8 +53,20 @@ import org.apache.juneau.commons.reflect.*;
* <li class='jm'>{@link #addBean(Class,Object,String)
addBean(Class,Object,String)}
* <li class='jm'>{@link #addSupplier(Class,Supplier)
addSupplier(Class,Supplier)}
* <li class='jm'>{@link #addSupplier(Class,Supplier,String)
addSupplier(Class,Supplier,String)}
+ * <li class='jm'>{@link #addDefaultSupplier(Class,Supplier)
addDefaultSupplier(Class,Supplier)} (last-resort fallback)
+ * <li class='jm'>{@link #addDefaultSupplier(Class,Supplier,String)
addDefaultSupplier(Class,Supplier,String)}
* </ul>
*
+ * <h5 class='section'>Resolution order:</h5>
+ * <p>
+ * Bean lookups consult the following sources in this order, returning the
first match:
+ * <ol>
+ * <li>{@linkplain Builder#overridingParent(BasicBeanStore) Overriding
parent} (e.g. a Spring application context bridge), if non-{@code null}.
+ * <li>Local entries added via {@link #addBean(Class,Object) addBean} /
{@link #addSupplier(Class,Supplier) addSupplier}.
+ * <li>Regular {@linkplain Builder#parent(BasicBeanStore) parent}, if
non-{@code null}.
+ * <li>Local default suppliers added via {@link
#addDefaultSupplier(Class,Supplier) addDefaultSupplier} (memoizer-backed
framework defaults).
+ * </ol>
+ *
* <p>
* Beans are retrieved through the following methods:
* <ul class='javatreec'>
@@ -106,6 +118,7 @@ public class BasicBeanStore {
public static class Builder {
BasicBeanStore parent;
+ BasicBeanStore overridingParent;
boolean readOnly;
boolean threadSafe;
Class<? extends BasicBeanStore> type;
@@ -177,6 +190,22 @@ public class BasicBeanStore {
return this;
}
+ /**
+ * Specifies an overriding-parent bean store.
+ *
+ * <p>
+ * The overriding parent is consulted <i>before</i> any local
entries during bean lookup, allowing an outer
+ * scope (typically a Spring {@code ApplicationContext} bridge)
to take precedence over local registrations.
+ * The regular {@link #parent(BasicBeanStore)} continues to be
consulted as a fallback after local entries.
+ *
+ * @param value The overriding-parent bean store. Can be
<jk>null</jk>.
+ * @return This object.
+ */
+ public Builder overridingParent(BasicBeanStore value) {
+ overridingParent = value;
+ return this;
+ }
+
/**
* Specifies that the bean store is read-only.
*
@@ -251,9 +280,12 @@ public class BasicBeanStore {
private final Deque<Entry<?>> entries;
private final Map<Class<?>,Entry<?>> unnamedEntries;
+ private final Deque<Entry<?>> defaults;
+ private final Map<Class<?>,Entry<?>> unnamedDefaults;
private final Map<Class<?>,Class<?>> beanTypes;
final Optional<BasicBeanStore> parent;
+ final Optional<BasicBeanStore> overridingParent;
final boolean readOnly;
final boolean threadSafe;
final SimpleReadWriteLock lock;
@@ -265,11 +297,14 @@ public class BasicBeanStore {
*/
protected BasicBeanStore(Builder builder) {
parent = opt(builder.parent);
+ overridingParent = opt(builder.overridingParent);
readOnly = builder.readOnly;
threadSafe = builder.threadSafe;
lock = threadSafe ? new SimpleReadWriteLock() :
SimpleReadWriteLock.NO_OP;
entries = threadSafe ? new ConcurrentLinkedDeque<>() : new
LinkedList<>();
unnamedEntries = threadSafe ? new ConcurrentHashMap<>() : map();
+ defaults = threadSafe ? new ConcurrentLinkedDeque<>() : new
LinkedList<>();
+ unnamedDefaults = threadSafe ? new ConcurrentHashMap<>() :
map();
beanTypes = threadSafe ? new ConcurrentHashMap<>() : map();
var e = createEntry(BasicBeanStore.class, ()->this, null);
entries.addFirst(e);
@@ -364,6 +399,47 @@ public class BasicBeanStore {
return this;
}
+ /**
+ * Adds a fallback supplier for an unnamed bean of the specified type
to this factory.
+ *
+ * <p>
+ * Default suppliers are consulted only after local {@linkplain
#addBean(Class,Object) entries} and the regular
+ * {@linkplain Builder#parent(BasicBeanStore) parent} have been
searched. They are intended for memoizer-backed
+ * framework defaults that should not shadow explicit user
registrations or beans inherited from an
+ * {@linkplain Builder#overridingParent(BasicBeanStore) overriding
parent} (e.g. Spring).
+ *
+ * @param <T> The class to associate this bean with.
+ * @param beanType The class to associate this bean with.
+ * @param bean The bean supplier.
+ * @return This object.
+ */
+ public <T> BasicBeanStore addDefaultSupplier(Class<T> beanType,
Supplier<T> bean) {
+ return addDefaultSupplier(beanType, bean, null);
+ }
+
+ /**
+ * Adds a fallback supplier for a named bean of the specified type to
this factory.
+ *
+ * <p>
+ * See {@link #addDefaultSupplier(Class,Supplier)} for ordering
semantics.
+ *
+ * @param <T> The class to associate this bean with.
+ * @param beanType The class to associate this bean with.
+ * @param bean The bean supplier.
+ * @param name The bean name if this is a named bean. Can be
<jk>null</jk>.
+ * @return This object.
+ */
+ public <T> BasicBeanStore addDefaultSupplier(Class<T> beanType,
Supplier<T> bean, String name) {
+ assertCanWrite();
+ var e = createEntry(beanType, bean, name);
+ try (var x = lock.write()) {
+ defaults.addFirst(e);
+ if (e(name))
+ unnamedDefaults.put(beanType, e);
+ }
+ return this;
+ }
+
/**
* Clears out all bean in this bean store.
*
@@ -377,6 +453,8 @@ public class BasicBeanStore {
try (var x = lock.write()) {
unnamedEntries.clear();
entries.clear();
+ unnamedDefaults.clear();
+ defaults.clear();
beanTypes.clear();
}
return this;
@@ -451,11 +529,26 @@ public class BasicBeanStore {
})
public <T> Optional<T> getBean(Class<T> beanType) {
try (var x = lock.read()) {
+ // (1) Overriding parent (e.g. Spring) — wins over
local entries.
+ if (overridingParent.isPresent()) {
+ var fromOverriding =
overridingParent.get().getBean(beanType);
+ if (fromOverriding.isPresent())
+ return fromOverriding;
+ }
+ // (2) Local entries.
var e = (Entry<T>)unnamedEntries.get(beanType);
if (nn(e))
return opt(e.get());
- if (parent.isPresent())
- return parent.get().getBean(beanType);
+ // (3) Regular parent fallback.
+ if (parent.isPresent()) {
+ var fromParent = parent.get().getBean(beanType);
+ if (fromParent.isPresent())
+ return fromParent;
+ }
+ // (4) Local default suppliers.
+ var d = (Entry<T>)unnamedDefaults.get(beanType);
+ if (nn(d))
+ return opt(d.get());
return opte();
}
}
@@ -473,11 +566,26 @@ public class BasicBeanStore {
})
public <T> Optional<T> getBean(Class<T> beanType, String name) {
try (var x = lock.read()) {
+ // (1) Overriding parent.
+ if (overridingParent.isPresent()) {
+ var fromOverriding =
overridingParent.get().getBean(beanType, name);
+ if (fromOverriding.isPresent())
+ return fromOverriding;
+ }
+ // (2) Local entries.
var e = (Entry<T>)entries.stream().filter(x2 ->
x2.matches(beanType, name)).findFirst().orElse(null);
if (nn(e))
return opt(e.get());
- if (parent.isPresent())
- return parent.get().getBean(beanType, name);
+ // (3) Regular parent fallback.
+ if (parent.isPresent()) {
+ var fromParent = parent.get().getBean(beanType,
name);
+ if (fromParent.isPresent())
+ return fromParent;
+ }
+ // (4) Local default suppliers.
+ var d = (Entry<T>)defaults.stream().filter(x2 ->
x2.matches(beanType, name)).findFirst().orElse(null);
+ if (nn(d))
+ return opt(d.get());
return opte();
}
}
@@ -561,7 +669,10 @@ public class BasicBeanStore {
* @return <jk>true</jk> if this store contains the specified unnamed
bean type.
*/
public boolean hasBean(Class<?> beanType) {
- return unnamedEntries.containsKey(beanType) || parent.map(x ->
x.hasBean(beanType)).orElse(false);
+ return overridingParent.map(x ->
x.hasBean(beanType)).orElse(false)
+ || unnamedEntries.containsKey(beanType)
+ || parent.map(x -> x.hasBean(beanType)).orElse(false)
+ || unnamedDefaults.containsKey(beanType);
}
/**
@@ -572,7 +683,43 @@ public class BasicBeanStore {
* @return <jk>true</jk> if this store contains the specified named
bean type.
*/
public boolean hasBean(Class<?> beanType, String name) {
- return entries.stream().anyMatch(x -> x.matches(beanType,
name)) || parent.map(x -> x.hasBean(beanType, name)).orElse(false);
+ return overridingParent.map(x -> x.hasBean(beanType,
name)).orElse(false)
+ || entries.stream().anyMatch(x -> x.matches(beanType,
name))
+ || parent.map(x -> x.hasBean(beanType,
name)).orElse(false)
+ || defaults.stream().anyMatch(x -> x.matches(beanType,
name));
+ }
+
+ /**
+ * Returns <jk>true</jk> if this store has a {@linkplain
#addDefaultSupplier(Class,Supplier) default supplier}
+ * registered locally for the specified unnamed bean type.
+ *
+ * <p>
+ * This is intended for callers that need to distinguish "framework
default present" from "any binding
+ * exists" — for example, the {@code RestContext} {@code @RestInject}
eager walk uses this signal to
+ * skip types that are managed by an internal memoizer (replacing the
legacy {@code DELAYED_INJECTION}
+ * skip-list). Parent and overriding-parent stores are <i>not</i>
consulted.
+ *
+ * @param beanType The bean type to check.
+ * @return <jk>true</jk> if a default supplier for the unnamed bean
type is registered on this store.
+ */
+ public boolean hasDefaultSupplier(Class<?> beanType) {
+ return unnamedDefaults.containsKey(beanType);
+ }
+
+ /**
+ * Returns <jk>true</jk> if this store has a {@linkplain
#addDefaultSupplier(Class,Supplier,String) default supplier}
+ * registered locally for the specified bean type and name.
+ *
+ * <p>
+ * See {@link #hasDefaultSupplier(Class)} for the rationale. Parent
and overriding-parent stores are
+ * <i>not</i> consulted.
+ *
+ * @param beanType The bean type to check.
+ * @param name The bean name. Can be <jk>null</jk> for unnamed beans.
+ * @return <jk>true</jk> if a default supplier for the bean type and
name is registered on this store.
+ */
+ public boolean hasDefaultSupplier(Class<?> beanType, String name) {
+ return defaults.stream().anyMatch(x -> x.matches(beanType,
name));
}
protected FluentMap<String,Object> properties() {
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 400599d0a0..b542c44c4b 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
@@ -221,61 +221,6 @@ public class RestContext extends Context {
*/
static class Builder extends Context.Builder implements ServletConfig {
- // @formatter:off
- private static final Set<Class<?>> DELAYED_INJECTION = set(
- BasicBeanStore.Builder.class,
- BasicBeanStore.class,
- CallLogger.class,
- Config.class,
- DebugEnablement.class,
- EncoderSet.Builder.class,
- EncoderSet.class,
- FileFinder.Builder.class,
- FileFinder.class,
- HttpPartParser.class,
- HttpPartSerializer.class,
- JsonSchemaGenerator.class,
- Logger.class,
- Messages.class,
- MethodExecStore.class,
- ParserSet.Builder.class,
- ParserSet.class,
- ResponseProcessorList.Builder.class,
- ResponseProcessorList.class,
- RestChildren.Builder.class,
- RestChildren.class,
- RestOpArgList.Builder.class,
- RestOpArgList.class,
- RestOperations.Builder.class,
- RestOperations.class,
- SerializerSet.Builder.class,
- SerializerSet.class,
- StaticFiles.class,
- SwaggerProvider.class,
- ThrownStore.class,
- VarList.class,
- VarResolver.class
- );
-
- private static final Set<String> DELAYED_INJECTION_NAMES = set(
- PROP_bootstrapVarResolver,
- "destroyMethods",
- "endCallMethods",
- "postCallMethods",
- "postInitChildFirstMethods",
- "postInitMethods",
- "preCallMethods",
- "startCallMethods"
- );
- // @formatter:on
-
-
//-----------------------------------------------------------------------------------------------------------------
- // The following fields are meant to be modifiable.
- // They should not be declared final.
- // Read-only snapshots of these will be made in
RestServletContext.
-
//-----------------------------------------------------------------------------------------------------------------
-
-
private final Class<?> resourceClass;
private final RestContext parentContext;
private final ServletConfig inner;
@@ -393,7 +338,11 @@ public class RestContext extends Context {
* @return A new bean store builder.
*/
private BasicBeanStore.Builder createBeanStore(BasicBeanStore parentBs,
Supplier<?> resource) {
- var v = Value.of(BasicBeanStore.create().parent(parentBs));
+ // The 9.5 precedence model places the parent (Spring or
parent-resource bootstrap) as the
+ // overriding parent so it wins over local entries.
Memoizer-backed framework defaults are
+ // registered later in the constructor via addDefaultSupplier,
putting them at the bottom of
+ // the resolution order.
+ var v =
Value.of(BasicBeanStore.create().overridingParent(parentBs));
// Apply @Rest(beanStore).
rstream(AnnotationProvider.INSTANCE.find(Rest.class,
info(resourceClass))).map(x ->
x.inner().beanStore()).filter(ClassUtils::isNotVoid).forEach(x ->
v.get().type(x));
@@ -407,6 +356,59 @@ public class RestContext extends Context {
private RestContext parentContext() { return parentContext; }
private RestOperations restOperations() { return restOperations.get(); }
+ /**
+ * Registers memoizer-backed default suppliers for every
framework-managed bean type on the supplied
+ * bean store.
+ *
+ * <p>
+ * Default suppliers sit at the bottom of the bean-store resolution
order: they fire only when no
+ * {@link RestInject} method, no programmatic {@code addBean(...)}
call, and no Spring/overriding-parent
+ * binding has been registered for the type. This is the mechanism
that replaces the old
+ * {@code DELAYED_INJECTION} list — by registering the framework's own
factories as defaults
+ * <i>before</i> the {@link RestInject} method walk runs, any {@link
RestInject} method whose parameters
+ * include framework types can now resolve those parameters lazily
through the bean store without
+ * requiring a hand-maintained skip list.
+ */
+ private void registerFrameworkDefaults(BasicBeanStore bs) {
+ // @formatter:off
+ // Unnamed framework types backed by per-resource memoizers.
+ bs.addDefaultSupplier(BeanContext.class, beanContext::get);
+ bs.addDefaultSupplier(EncoderSet.class, encoders::get);
+ bs.addDefaultSupplier(SerializerSet.class, serializers::get);
+ bs.addDefaultSupplier(ParserSet.class, parsers::get);
+ bs.addDefaultSupplier(Logger.class, logger::get);
+ bs.addDefaultSupplier(java.util.logging.Logger.class,
logger::get);
+ bs.addDefaultSupplier(ThrownStore.class, thrownStore::get);
+ bs.addDefaultSupplier(MethodExecStore.class,
methodExecStore::get);
+ bs.addDefaultSupplier(Messages.class, messages::get);
+ bs.addDefaultSupplier(VarResolver.class, varResolver::get);
+ bs.addDefaultSupplier(Config.class, config::get);
+ bs.addDefaultSupplier(ResponseProcessor[].class,
responseProcessors::get);
+ bs.addDefaultSupplier(CallLogger.class, callLogger::get);
+ bs.addDefaultSupplier(HttpPartSerializer.class,
partSerializer::get);
+ bs.addDefaultSupplier(HttpPartParser.class, partParser::get);
+ bs.addDefaultSupplier(JsonSchemaGenerator.class,
jsonSchemaGenerator::get);
+ bs.addDefaultSupplier(StaticFiles.class, staticFiles::get);
+ bs.addDefaultSupplier(FileFinder.class, staticFiles::get);
+ bs.addDefaultSupplier(DebugEnablement.class,
debugEnablement::get);
+ bs.addDefaultSupplier(SwaggerProvider.class,
swaggerProvider::get);
+ bs.addDefaultSupplier(RestOperations.class,
restOperations::get);
+ bs.addDefaultSupplier(RestChildren.class, restChildren::get);
+ // Named framework beans (replaces DELAYED_INJECTION_NAMES).
+ bs.addDefaultSupplier(VarResolver.class,
this::getBootstrapVarResolver, PROP_bootstrapVarResolver);
+ bs.addDefaultSupplier(HeaderList.class,
defaultRequestHeaders::get, PROP_defaultRequestHeaders);
+ bs.addDefaultSupplier(HeaderList.class,
defaultResponseHeaders::get, PROP_defaultResponseHeaders);
+ bs.addDefaultSupplier(NamedAttributeMap.class,
defaultRequestAttributes::get, PROP_defaultRequestAttributes);
+ bs.addDefaultSupplier(MethodList.class, () ->
destroyInvokerPair.get().methods, "destroyMethods");
+ bs.addDefaultSupplier(MethodList.class, () ->
endCallInvokerPair.get().methods, "endCallMethods");
+ bs.addDefaultSupplier(MethodList.class, postCallMethods::get,
"postCallMethods");
+ bs.addDefaultSupplier(MethodList.class, () ->
postInitChildFirstInvokerPair.get().methods, "postInitChildFirstMethods");
+ bs.addDefaultSupplier(MethodList.class, () ->
postInitInvokerPair.get().methods, "postInitMethods");
+ bs.addDefaultSupplier(MethodList.class, preCallMethods::get,
"preCallMethods");
+ bs.addDefaultSupplier(MethodList.class, () ->
startCallInvokerPair.get().methods, "startCallMethods");
+ // @formatter:on
+ }
+
private static final class LifecycleInvokerPair {
final MethodList methods;
final MethodInvoker[] invokers;
@@ -426,7 +428,6 @@ public class RestContext extends Context {
*/
private final Memoizer<BeanContext.Builder> beanContextBuilder =
memoizer(() -> {
var v = Value.of(BeanContext.create());
- beanStore().getBean(BeanContext.class).ifPresent(x ->
v.get().impl(x));
v.get().apply(annotationWork);
return v.get();
});
@@ -447,7 +448,9 @@ public class RestContext extends Context {
private final Memoizer<Config> rawConfig = memoizer(() -> {
var bs = beanStore();
var v = Value.<Config>empty();
- var vr = bs.getBean(VarResolver.class).orElseThrow(() -> new
IllegalArgumentException("VarResolver not found."));
+ // Bootstrap VarResolver is registered under
PROP_bootstrapVarResolver during construction; the
+ // unnamed VarResolver slot is reserved for the full runtime
resolver default supplier.
+ var vr = bs.getBean(VarResolver.class,
PROP_bootstrapVarResolver).orElseGet(this::getBootstrapVarResolver);
var cfv = Value.<String>empty();
rstream(AnnotationProvider.INSTANCE.find(Rest.class,
info(resourceClass()))).map(x ->
x.inner().config()).filter(Utils::ne).forEach(x -> cfv.set(vr.resolve(x)));
var cf = cfv.orElse("");
@@ -459,7 +462,6 @@ public class RestContext extends Context {
cb.name(cf);
v.set(cb.build());
}
- bs.getBean(Config.class).ifPresent(v::set);
new BeanCreateMethodFinder<>(Config.class, resource().get(),
bs).addBean(Config.class,
v.get()).find(RestContext::isRestInjectMethod).run(v::set);
return v.get();
});
@@ -489,7 +491,6 @@ public class RestContext extends Context {
.bean(FileFinder.class,
FileFinder.create(bs).cp(resourceClass(), null, true).build())
.build()
);
- bs.getBean(VarResolver.class,
PROP_bootstrapVarResolver).ifPresent(v::set);
new BeanCreateMethodFinder<>(VarResolver.class,
resource().get(), bs).find(x -> isRestInjectMethod(x,
PROP_bootstrapVarResolver)).run(v::set);
return v.get();
});
@@ -515,7 +516,6 @@ public class RestContext extends Context {
.filter(c -> c != CallLogger.Void.class)
.reduce((first, second) -> second)
.ifPresent(creator::type);
- bs.getBean(CallLogger.class).ifPresent(creator::impl);
new BeanCreateMethodFinder<>(CallLogger.class,
resource().get(), bs).find(RestContext::isRestInjectMethod).run(creator::impl);
return creator.orElse(null);
});
@@ -584,7 +584,6 @@ public class RestContext extends Context {
.filter(c -> c != DebugEnablement.Void.class)
.reduce((first, second) -> second)
.ifPresent(creator::type);
- bs.getBean(DebugEnablement.class).ifPresent(creator::impl);
new BeanCreateMethodFinder<>(DebugEnablement.class,
resource().get(), bs).find(RestContext::isRestInjectMethod).run(creator::impl);
return creator.orElse(null);
});
@@ -605,7 +604,6 @@ public class RestContext extends Context {
.filter(StringUtils::isNotBlank)
.map(BasicNamedAttribute::ofPair)
.forEach(v.get()::add));
- beanStore().getBean(NamedAttributeMap.class,
PROP_defaultRequestAttributes).ifPresent(v::set);
new BeanCreateMethodFinder<>(NamedAttributeMap.class,
resource().get(), beanStore()).addBean(NamedAttributeMap.class, v.get()).find(x
-> isRestInjectMethod(x, PROP_defaultRequestAttributes)).run(v::set);
return v.get();
});
@@ -630,7 +628,6 @@ public class RestContext extends Context {
if (isNotBlank(defaultContentType))
v.get().setDefault(contentType(defaultContentType));
});
- beanStore().getBean(HeaderList.class,
PROP_defaultRequestHeaders).ifPresent(v::set);
new BeanCreateMethodFinder<>(HeaderList.class,
resource().get(), beanStore()).addBean(HeaderList.class, v.get()).find(x ->
isRestInjectMethod(x, PROP_defaultRequestHeaders)).run(v::set);
return v.get();
});
@@ -645,7 +642,6 @@ public class RestContext extends Context {
private final Memoizer<HeaderList> defaultResponseHeaders = memoizer(()
-> {
var v = Value.of(HeaderList.create());
getRestAnnotationsTopDown().forEach(ai ->
Arrays.stream(ai.inner().defaultResponseHeaders()).filter(StringUtils::isNotBlank).map(this::resolve).filter(StringUtils::isNotBlank).map(s
-> stringHeader(s)).forEach(v.get()::setDefault));
- beanStore().getBean(HeaderList.class,
PROP_defaultResponseHeaders).ifPresent(v::set);
new BeanCreateMethodFinder<>(HeaderList.class,
resource().get(), beanStore()).addBean(HeaderList.class, v.get()).find(x ->
isRestInjectMethod(x, PROP_defaultResponseHeaders)).run(v::set);
return v.get();
});
@@ -674,7 +670,6 @@ public class RestContext extends Context {
var bs = beanStore();
var v = Value.of(EncoderSet.create(bs));
bs.getBeanType(EncoderSet.class).ifPresent(x ->
v.get().type(x));
- bs.getBean(EncoderSet.class).ifPresent(x -> v.get().impl(x));
getRestAnnotationsForProperty(PROPERTY_encoders).forEach(ai ->
v.get().add(ai.inner().encoders()));
new BeanCreateMethodFinder<>(EncoderSet.class,
resource().get(), bs).addBean(EncoderSet.Builder.class,
v.get()).find(RestContext::isRestInjectMethod).run(x -> v.get().impl(x));
return v.get();
@@ -701,7 +696,6 @@ public class RestContext extends Context {
private final Memoizer<JsonSchemaGenerator.Builder>
jsonSchemaGeneratorBuilder = memoizer(() -> {
var bs = beanStore();
var v = Value.of(JsonSchemaGenerator.create());
- bs.getBean(JsonSchemaGenerator.class).ifPresent(x ->
v.get().impl(x));
new BeanCreateMethodFinder<>(JsonSchemaGenerator.class,
resource().get(), bs).find(RestContext::isRestInjectMethod).run(x ->
v.get().impl(x));
v.get().apply(annotationWork);
return v.get();
@@ -721,7 +715,6 @@ public class RestContext extends Context {
*/
private final Memoizer<Logger> logger = memoizer(() -> {
var v = Value.of(Logger.getLogger(cn(resourceClass())));
- beanStore().getBean(Logger.class).ifPresent(v::set);
new BeanCreateMethodFinder<>(Logger.class, resource().get(),
beanStore()).addBean(Logger.class,
v.get()).find(RestContext::isRestInjectMethod).run(v::set);
return v.get();
});
@@ -741,7 +734,6 @@ public class RestContext extends Context {
// (it depends on getMessages()).
var vrs = getBootstrapVarResolver().createSession();
getRestAnnotationsTopDown().forEach(ai ->
ai.getString(PROPERTY_messages).filter(StringUtils::isNotBlank).ifPresent(s ->
b.location(vrs.resolve(s))));
- beanStore().getBean(Messages.class).ifPresent(b::impl);
new BeanCreateMethodFinder<>(Messages.class, resource().get(),
beanStore()).addBean(Messages.Builder.class,
b).find(RestContext::isRestInjectMethod).run(b::impl);
return b.build();
});
@@ -756,7 +748,6 @@ public class RestContext extends Context {
var bs = beanStore();
var b =
MethodExecStore.create(bs).thrownStoreOnce(getThrownStore());
bs.getBeanType(MethodExecStore.class).ifPresent(b::type);
- bs.getBean(MethodExecStore.class).ifPresent(b::impl);
new BeanCreateMethodFinder<>(MethodExecStore.class,
resource().get(), bs).addBean(MethodExecStore.Builder.class,
b).find(RestContext::isRestInjectMethod).run(b::impl);
return b.build();
});
@@ -773,7 +764,6 @@ public class RestContext extends Context {
var bs = beanStore();
var v = Value.of(ParserSet.create(bs));
bs.getBeanType(ParserSet.class).ifPresent(x -> v.get().type(x));
- bs.getBean(ParserSet.class).ifPresent(x -> v.get().impl(x));
getRestAnnotationsForProperty(PROPERTY_parsers).forEach(ai ->
v.get().add(ai.inner().parsers()));
new BeanCreateMethodFinder<>(ParserSet.class, resource().get(),
bs).addBean(ParserSet.Builder.class,
v.get()).find(RestContext::isRestInjectMethod).run(x -> v.get().impl(x));
return v.get();
@@ -790,7 +780,6 @@ public class RestContext extends Context {
private final Memoizer<HttpPartParser.Creator> partParserCreator =
memoizer(() -> {
var bs = beanStore();
Value<HttpPartParser.Creator> v =
Value.of(HttpPartParser.creator().type(OpenApiParser.class));
- bs.getBean(HttpPartParser.class).ifPresent(x ->
v.get().impl(x));
opt(resource().get() instanceof HttpPartParser x ? x :
null).ifPresent(x -> v.get().impl(x));
bs.getBeanType(HttpPartParser.class).ifPresent(x ->
v.get().type(x));
new BeanCreateMethodFinder<>(HttpPartParser.class,
resource().get(), bs).find(RestContext::isRestInjectMethod).run(x ->
v.get().impl(x));
@@ -821,7 +810,6 @@ public class RestContext extends Context {
private final Memoizer<HttpPartSerializer.Creator>
partSerializerCreator = memoizer(() -> {
var bs = beanStore();
Value<HttpPartSerializer.Creator> v =
Value.of(HttpPartSerializer.creator().type(OpenApiSerializer.class));
- bs.getBean(HttpPartSerializer.class).ifPresent(x ->
v.get().impl(x));
opt(resource().get() instanceof HttpPartSerializer x ? x :
null).ifPresent(x -> v.get().impl(x));
bs.getBeanType(HttpPartSerializer.class).ifPresent(x ->
v.get().type(x));
new BeanCreateMethodFinder<>(HttpPartSerializer.class,
resource().get(), bs).find(RestContext::isRestInjectMethod).run(x ->
v.get().impl(x));
@@ -927,7 +915,6 @@ public class RestContext extends Context {
getRestAnnotationsForProperty(PROPERTY_responseProcessors)
.forEach(ai ->
v.get().add(ai.inner().responseProcessors()));
// Bean-store override REPLACES the entire annotation-derived
list.
- bs.getBean(ResponseProcessorList.class).ifPresent(x ->
v.get().impl(x));
// @RestInject method override REPLACES the entire
annotation-derived list.
new BeanCreateMethodFinder<>(ResponseProcessorList.class,
resource().get(), bs)
.addBean(ResponseProcessorList.Builder.class, v.get())
@@ -953,7 +940,6 @@ public class RestContext extends Context {
getRestAnnotationsForProperty(PROPERTY_restOpArgs)
.forEach(ai -> v.get().add(ai.inner().restOpArgs()));
// Bean-store override REPLACES the entire annotation-derived
list.
- bs.getBean(RestOpArgList.class).ifPresent(x -> v.get().impl(x));
// @RestInject method override REPLACES the entire
annotation-derived list.
new BeanCreateMethodFinder<>(RestOpArgList.class,
resource().get(), bs)
.addBean(RestOpArgList.Builder.class, v.get())
@@ -974,7 +960,6 @@ public class RestContext extends Context {
var bs = beanStore();
var v = Value.of(SerializerSet.create(bs));
bs.getBeanType(SerializerSet.class).ifPresent(x ->
v.get().type(x));
- bs.getBean(SerializerSet.class).ifPresent(x -> v.get().impl(x));
getRestAnnotationsForProperty(PROPERTY_serializers).forEach(ai
-> v.get().add(ai.inner().serializers()));
new BeanCreateMethodFinder<>(SerializerSet.class,
resource().get(), bs).addBean(SerializerSet.Builder.class,
v.get()).find(RestContext::isRestInjectMethod).run(x -> v.get().impl(x));
return v.get();
@@ -1013,7 +998,6 @@ public class RestContext extends Context {
.filter(c -> c != StaticFiles.Void.class)
.reduce((first, second) -> second)
.ifPresent(creator::type);
- bs.getBean(StaticFiles.class).ifPresent(creator::impl);
new BeanCreateMethodFinder<>(StaticFiles.class,
resource().get(), bs).find(RestContext::isRestInjectMethod).run(creator::impl);
return creator.orElse(null);
});
@@ -1036,7 +1020,6 @@ public class RestContext extends Context {
.filter(c -> c != SwaggerProvider.Void.class)
.reduce((first, second) -> second)
.ifPresent(creator::type);
- bs.getBean(SwaggerProvider.class).ifPresent(creator::impl);
new BeanCreateMethodFinder<>(SwaggerProvider.class,
resource().get(), bs).find(RestContext::isRestInjectMethod).run(creator::impl);
return creator.orElse(null);
});
@@ -1052,7 +1035,6 @@ public class RestContext extends Context {
var bs = beanStore();
var b = ThrownStore.create(bs).impl(parentContext() == null ?
null : parentContext().getThrownStore());
bs.getBeanType(ThrownStore.class).ifPresent(b::type);
- bs.getBean(ThrownStore.class).ifPresent(b::impl);
new BeanCreateMethodFinder<>(ThrownStore.class,
resource().get(), bs).addBean(ThrownStore.Builder.class,
b).find(RestContext::isRestInjectMethod).run(b::impl);
return b.build();
});
@@ -1070,7 +1052,6 @@ public class RestContext extends Context {
var b = getBootstrapVarResolver().copy()
.bean(Messages.class, getMessages())
.bean(Config.class, rawConfig.get());
- bs.getBean(VarResolver.class).ifPresent(b::impl);
new BeanCreateMethodFinder<>(VarResolver.class,
resource().get(), bs).addBean(VarResolver.Builder.class,
b).find(RestContext::isRestInjectMethod).run(b::impl);
return b.build();
});
@@ -1086,6 +1067,7 @@ public class RestContext extends Context {
*/
@SuppressWarnings({"java:S3776", "java:S1141"})
private final Memoizer<RestOperations> restOperations = memoizer(() ->
safe(() -> {
+ initializeFrameworkBeansForRestOps();
var bs = beanStore();
var v = Value.of(RestOperations.create(bs));
var ap = getBeanContext().getAnnotationProvider();
@@ -1206,12 +1188,14 @@ public class RestContext extends Context {
var rs = new ResourceSupplier(resourceClass,
assertArgNotNull("resource", builder.args.resource()));
resource = rs;
- // --- beanStore setup (moved from Builder constructor,
May 2026 refactor) ---
+ // --- beanStore setup (May 2026 refactor;
precedence-flipped 9.5) ---
// Determine the parent (bootstrap) store: inherited
from parent resource if present.
BasicBeanStore parentBs = parentContext != null ?
parentContext.bootstrapBeanStore : null;
// Build the initial beanStore; apply @Rest(beanStore)
+ optional @RestInject override.
+ // In the new 9.5 precedence model, the parent (Spring
or parent-resource bootstrap) is
+ // installed as the overriding parent so it wins over
local entries.
// @formatter:off
BasicBeanStore bs = createBeanStore(parentBs, rs)
.build()
@@ -1220,18 +1204,34 @@ public class RestContext extends Context {
.addBean(ServletContext.class,
(nn(builder.inner) ? builder.inner : builder).getServletContext());
// @formatter:on
- // If no parent store, promote bs to bootstrap and
layer a fresh child store on top.
+ // If no parent store, promote bs to bootstrap and
layer a fresh per-resource store on top.
if (parentBs == null) {
bootstrapBeanStore = bs;
- bs = BasicBeanStore.of(bootstrapBeanStore);
+ bs =
BasicBeanStore.create().overridingParent(bootstrapBeanStore).build();
} else {
bootstrapBeanStore = parentBs;
}
beanStore = bs;
beanStore.add(BasicBeanStore.class, beanStore);
- beanStore.add(VarResolver.class,
getBootstrapVarResolver());
- beanStore.add(Config.class, rawConfig.get());
+ // Register the bootstrap VarResolver as a NAMED entry
only. The unnamed VarResolver
+ // slot is intentionally left to the
registerFrameworkDefaults() default supplier (which
+ // resolves to the full runtime VarResolver) so that
internal accessors such as
+ // getVarResolver() return the right thing when routed
through beanStore.getBean(...).
+ beanStore.add(VarResolver.class,
getBootstrapVarResolver(), PROP_bootstrapVarResolver);
+ // Force-build raw Config now (fail fast if
@Rest(config) is misconfigured). The unnamed
+ // Config slot in the bean store is intentionally left
to the default supplier (the full
+ // 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();
+
+ // Register memoizer-backed defaults for every
framework-managed type. These sit at the
+ // bottom of the precedence order and only fire when no
@RestInject method, no programmatic
+ // add, and no Spring/overriding-parent bean has been
registered for the type. This is
+ // what removes the need for the old DELAYED_INJECTION
gate-keeping list — the @RestInject
+ // walk below can now invoke any framework type's
factory and still resolve framework
+ // dependencies through the bean store.
+ registerFrameworkDefaults(beanStore);
var rci2 = ClassInfo.of(resourceClass);
@@ -1248,17 +1248,22 @@ public class RestContext extends Context {
));
// @formatter:on
- // Run non-delayed @RestInject methods.
+ // Run @RestInject methods for non-framework types.
Framework types (those with a default
+ // supplier registered above) handle their @RestInject
scan inside the corresponding memoizer
+ // body so that user methods can declare the
framework's Builder type as a parameter; running
+ // them again here would either re-invoke side effects
or skip them entirely (for Pattern-2
+ // methods whose Builder parameter isn't injected
here). This auto-derives the legacy
+ // DELAYED_INJECTION list from the default-supplier
registrations.
rci2.getAllMethods().stream().filter(x ->
x.hasAnnotation(RestInject.class)).forEach(x -> {
var rt = x.getReturnType().<Object>inner();
var name =
RestInjectAnnotation.name(x.getAnnotations(RestInject.class).findFirst().map(AnnotationInfo::inner).orElse(null));
- if (! (Builder.DELAYED_INJECTION.contains(rt)
|| Builder.DELAYED_INJECTION_NAMES.contains(name))) {
- // @formatter:off
- new BeanCreateMethodFinder<>(rt,
resource.get(), beanStore)
-
.find(RestContext::isRestInjectMethod)
- .run(y -> beanStore.add(rt, y,
name));
- // @formatter:on
- }
+ if (beanStore.hasDefaultSupplier(rt, name))
+ return;
+ // @formatter:off
+ new BeanCreateMethodFinder<>(rt,
resource.get(), beanStore)
+ .find(RestContext::isRestInjectMethod)
+ .run(y -> beanStore.add(rt, y, name));
+ // @formatter:on
});
// Run @RestInit-annotated methods on the resource
object (deduplicated by signature, top-down order).
@@ -1328,37 +1333,20 @@ public class RestContext extends Context {
.addBean(AnnotationWorkList.class,
annotationWork);
// @formatter:on
- bs.addBean(BeanContext.class, getBeanContext());
- bs.add(EncoderSet.class, getEncoders());
- bs.add(SerializerSet.class, getSerializers());
- bs.add(ParserSet.class, getParsers());
- var lg = getLogger();
- bs.addBean(Logger.class, lg);
- bs.addBean(java.util.logging.Logger.class, lg); // Also
register under java.util.logging.Logger for CallLogger compatibility
- bs.addBean(ThrownStore.class, getThrownStore());
- bs.addBean(MethodExecStore.class, getMethodExecStore());
- var msgs = getMessages();
- bs.addBean(Messages.class, msgs);
- bs.add(VarResolver.class, getVarResolver());
- bs.add(Config.class, getConfig());
- bs.add(ResponseProcessor[].class,
getResponseProcessors());
- bs.addBean(CallLogger.class, getCallLogger());
- bs.add(HttpPartSerializer.class, getPartSerializer());
- bs.add(HttpPartParser.class, getPartParser());
- bs.add(JsonSchemaGenerator.class,
getJsonSchemaGenerator());
- var sf = getStaticFiles();
- bs.addBean(StaticFiles.class, sf);
- bs.addBean(FileFinder.class, sf);
- bs.add(HeaderList.class, getDefaultRequestHeaders(),
PROP_defaultRequestHeaders);
- bs.add(HeaderList.class, getDefaultResponseHeaders(),
PROP_defaultResponseHeaders);
- bs.add(NamedAttributeMap.class,
getDefaultRequestAttributes(), PROP_defaultRequestAttributes);
- bs.addBean(DebugEnablement.class, getDebugEnablement());
- // Force-initialize restOperations and restChildren now
so that any construction failures
- // (e.g. bad @RestOp method or invalid child class)
surface here inside the try-catch and
- // propagate as a proper init exception rather than
lazily on first use.
- getRestOperations();
- getRestChildren();
- bs.addBean(SwaggerProvider.class, getSwaggerProvider());
+ if (isEagerInit()) {
+ // Force-fire the framework-bean memoizers in
dependency-friendly order so their @Rest()
+ // annotation walks (e.g.
`@Rest(partParser=…)`, `@Rest(partSerializer=…)`, `@Rest(encoders=…)`,
+ // etc.) execute eagerly inside the try-catch.
These walks MUTATE the cached creators that
+ // downstream RestOpContext memoizers later
copy from, so they must run before
+ // getRestOperations() builds the per-op
contexts. We do NOT re-add results to the bean
+ // store — the default suppliers registered
earlier already cover lookups.
+ initializeFrameworkBeansForRestOps();
+
+ // Force-initialize restOperations and
restChildren now so that any construction failures
+ // (e.g. bad @RestOp method or invalid child
class) surface here inside the try-catch.
+ getRestOperations();
+ getRestChildren();
+ }
// produces/consumes are resolved lazily via the
produces/consumes memoizers below
// (April 2026 refactor, 2026-04-19) — they walk the
@Rest(produces=...) / @Rest(consumes=...)
@@ -1552,6 +1540,13 @@ public class RestContext extends Context {
private final Memoizer<Boolean> renderResponseStackTraces = memoizer(()
->
mergeReplacedBooleanAttribute(PROPERTY_renderResponseStackTraces,
env("RestContext.renderResponseStackTraces", false)));
+ /**
+ * Whether framework memoizers and operation/child contexts should be
force-initialized during constructor execution;
+ * resolved from {@code @Rest(eagerInit)}.
+ */
+ private final Memoizer<Boolean> eagerInit = memoizer(() ->
+ mergeReplacedBooleanAttribute(PROPERTY_eagerInit,
env("RestContext.eagerInit", false)));
+
/**
* The request header used for client-version matching; resolved from
{@code @Rest(clientVersionHeader)},
* default {@code "Client-Version"}.
@@ -1919,7 +1914,7 @@ public class RestContext extends Context {
*
* @return The bean store associated with this context.
*/
- public BeanContext getBeanContext() { return beanContext.get(); }
+ public BeanContext getBeanContext() { return
beanStore.getBean(BeanContext.class).orElse(null); }
BeanContext.Builder getBeanContextBuilder() { return
beanContextBuilder.get(); }
EncoderSet.Builder getEncodersBuilder() { return
encodersBuilder.get(); }
@@ -1966,7 +1961,7 @@ public class RestContext extends Context {
* The call logger to use for this resource.
* <br>Never <jk>null</jk>.
*/
- public CallLogger getCallLogger() { return callLogger.get(); }
+ public CallLogger getCallLogger() { return
beanStore.getBean(CallLogger.class).orElse(null); }
/**
* Returns the name of the client version header name used by this
resource.
@@ -1991,7 +1986,7 @@ public class RestContext extends Context {
* The resolving config file associated with this servlet.
* <br>Never <jk>null</jk>.
*/
- public Config getConfig() { return config.get(); }
+ public Config getConfig() { return
beanStore.getBean(Config.class).orElse(null); }
/**
* Returns the explicit list of supported content types for this
resource.
@@ -2013,7 +2008,7 @@ public class RestContext extends Context {
*
* @return The debug enablement bean for this context.
*/
- public DebugEnablement getDebugEnablement() { return
debugEnablement.get(); }
+ public DebugEnablement getDebugEnablement() { return
beanStore.getBean(DebugEnablement.class).orElse(null); }
/**
* Returns the default request attributes for this resource.
@@ -2026,7 +2021,7 @@ public class RestContext extends Context {
* The default request headers for this resource in an
unmodifiable list.
* <br>Never <jk>null</jk>.
*/
- public NamedAttributeMap getDefaultRequestAttributes() { return
defaultRequestAttributes.get(); }
+ public NamedAttributeMap getDefaultRequestAttributes() { return
beanStore.getBean(NamedAttributeMap.class,
PROP_defaultRequestAttributes).orElse(null); }
/**
* Returns the default request headers for this resource.
@@ -2039,7 +2034,7 @@ public class RestContext extends Context {
* The default request headers for this resource in an
unmodifiable list.
* <br>Never <jk>null</jk>.
*/
- public HeaderList getDefaultRequestHeaders() { return
defaultRequestHeaders.get(); }
+ public HeaderList getDefaultRequestHeaders() { return
beanStore.getBean(HeaderList.class, PROP_defaultRequestHeaders).orElse(null); }
/**
* Returns the default response headers for this resource.
@@ -2052,14 +2047,14 @@ public class RestContext extends Context {
* The default response headers for this resource in an
unmodifiable list.
* <br>Never <jk>null</jk>.
*/
- public HeaderList getDefaultResponseHeaders() { return
defaultResponseHeaders.get(); }
+ public HeaderList getDefaultResponseHeaders() { return
beanStore.getBean(HeaderList.class, PROP_defaultResponseHeaders).orElse(null); }
/**
* Returns the encoders associated with this context.
*
* @return The encoders associated with this context.
*/
- public EncoderSet getEncoders() { return encoders.get(); }
+ public EncoderSet getEncoders() { return
beanStore.getBean(EncoderSet.class).orElse(null); }
/**
* Returns the path for this resource as defined by the {@link
Rest#path() @Rest(path)} annotation or
@@ -2083,7 +2078,7 @@ public class RestContext extends Context {
* The HTTP-part serializer associated with this resource.
* <br>Never <jk>null</jk>.
*/
- public JsonSchemaGenerator getJsonSchemaGenerator() { return
jsonSchemaGenerator.get(); }
+ public JsonSchemaGenerator getJsonSchemaGenerator() { return
beanStore.getBean(JsonSchemaGenerator.class).orElse(null); }
/**
* Returns the HTTP call for the current request.
@@ -2105,7 +2100,7 @@ public class RestContext extends Context {
* The logger for this resource.
* <br>Never <jk>null</jk>.
*/
- public Logger getLogger() { return logger.get(); }
+ public Logger getLogger() { return
beanStore.getBean(Logger.class).orElse(null); }
/**
* Returns the resource bundle used by this resource.
@@ -2114,21 +2109,21 @@ public class RestContext extends Context {
* The resource bundle for this resource.
* <br>Never <jk>null</jk>.
*/
- public Messages getMessages() { return messages.get(); }
+ public Messages getMessages() { return
beanStore.getBean(Messages.class).orElse(null); }
/**
* Returns the timing statistics on all method executions on this class.
*
* @return The timing statistics on all method executions on this class.
*/
- public MethodExecStore getMethodExecStore() { return
methodExecStore.get(); }
+ public MethodExecStore getMethodExecStore() { return
beanStore.getBean(MethodExecStore.class).orElse(null); }
/**
* Returns the parsers associated with this context.
*
* @return The parsers associated with this context.
*/
- public ParserSet getParsers() { return parsers.get(); }
+ public ParserSet getParsers() { return
beanStore.getBean(ParserSet.class).orElse(null); }
/**
* Returns the HTTP-part parser associated with this resource.
@@ -2137,7 +2132,7 @@ public class RestContext extends Context {
* The HTTP-part parser associated with this resource.
* <br>Never <jk>null</jk>.
*/
- public HttpPartParser getPartParser() { return partParser.get(); }
+ public HttpPartParser getPartParser() { return
beanStore.getBean(HttpPartParser.class).orElse(null); }
/**
* Returns the HTTP-part serializer associated with this resource.
@@ -2146,7 +2141,7 @@ public class RestContext extends Context {
* The HTTP-part serializer associated with this resource.
* <br>Never <jk>null</jk>.
*/
- public HttpPartSerializer getPartSerializer() { return
partSerializer.get(); }
+ public HttpPartSerializer getPartSerializer() { return
beanStore.getBean(HttpPartSerializer.class).orElse(null); }
/**
* Returns the path for this resource as defined by the {@link
Rest#path() @Rest(path)} annotation or
@@ -2221,7 +2216,7 @@ public class RestContext extends Context {
* The response processors for this resource.
* <br>Never <jk>null</jk>.
*/
- public ResponseProcessor[] getResponseProcessors() { return
responseProcessors.get(); }
+ public ResponseProcessor[] getResponseProcessors() { return
beanStore.getBean(ResponseProcessor[].class).orElse(null); }
/**
* Returns the {@link RestOpArg} classes registered on this resource.
@@ -2248,7 +2243,7 @@ public class RestContext extends Context {
* An unmodifiable map of child resources.
* Keys are the {@link Rest#path() @Rest(path)} annotation defined
on the child resource.
*/
- public RestChildren getRestChildren() { return restChildren.get(); }
+ public RestChildren getRestChildren() { return
beanStore.getBean(RestChildren.class).orElse(null); }
/**
* Returns the REST Java methods defined in this resource.
@@ -2259,7 +2254,7 @@ public class RestContext extends Context {
* @return
* An unmodifiable map of Java method names to call method objects.
*/
- public RestOperations getRestOperations() { return
restOperations.get(); }
+ public RestOperations getRestOperations() { return
beanStore.getBean(RestOperations.class).orElse(null); }
/**
* Returns the bootstrap bean store for this context.
@@ -2277,7 +2272,7 @@ public class RestContext extends Context {
*
* @return The serializers associated with this context.
*/
- public SerializerSet getSerializers() { return serializers.get(); }
+ public SerializerSet getSerializers() { return
beanStore.getBean(SerializerSet.class).orElse(null); }
/**
* Returns the servlet init parameter returned by {@link
ServletConfig#getInitParameter(String)}.
@@ -2296,7 +2291,7 @@ public class RestContext extends Context {
* The static files for this resource.
* <br>Never <jk>null</jk>.
*/
- public StaticFiles getStaticFiles() { return staticFiles.get(); }
+ public StaticFiles getStaticFiles() { return
beanStore.getBean(StaticFiles.class).orElse(null); }
/**
* Gives access to the internal statistics on this context.
@@ -2336,7 +2331,7 @@ public class RestContext extends Context {
* The information provider for this resource.
* <br>Never <jk>null</jk>.
*/
- public SwaggerProvider getSwaggerProvider() { return
swaggerProvider.get(); }
+ public SwaggerProvider getSwaggerProvider() { return
beanStore.getBean(SwaggerProvider.class).orElse(null); }
/**
* Returns the stack trace database associated with this context.
@@ -2345,7 +2340,7 @@ public class RestContext extends Context {
* The stack trace database for this resource.
* <br>Never <jk>null</jk>.
*/
- public ThrownStore getThrownStore() { return thrownStore.get(); }
+ public ThrownStore getThrownStore() { return
beanStore.getBean(ThrownStore.class).orElse(null); }
/**
* Returns the authority path of the resource.
@@ -2454,7 +2449,7 @@ public class RestContext extends Context {
*
* @return The var resolver in use by this resource.
*/
- public VarResolver getVarResolver() { return varResolver.get(); }
+ public VarResolver getVarResolver() { return
beanStore.getBean(VarResolver.class).orElse(null); }
/**
* Returns the bootstrap (pre-runtime) variable resolver used during
context construction.
@@ -2487,6 +2482,17 @@ public class RestContext extends Context {
*/
public boolean isRenderResponseStackTraces() { return
renderResponseStackTraces.get(); }
+ /**
+ * Returns whether framework beans and operation/child contexts are
eagerly initialized at construction time.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='ja'>{@link Rest#eagerInit()}
+ * </ul>
+ *
+ * @return <jk>true</jk> if eager initialization is enabled.
+ */
+ public boolean isEagerInit() { return eagerInit.get(); }
+
/**
* Called during servlet initialization to invoke all {@link
RestPostInit} child-last methods.
*
@@ -2541,6 +2547,34 @@ public class RestContext extends Context {
return getDebugEnablement().isDebug(this, call.getRequest());
}
+ /**
+ * Ensures framework bean memoizers that can mutate creator state from
{@code @Rest(...)} annotation walks
+ * have run before {@link RestOpContext} instances are built.
+ */
+ private void initializeFrameworkBeansForRestOps() {
+ getBeanContext();
+ getEncoders();
+ getSerializers();
+ getParsers();
+ getLogger();
+ getThrownStore();
+ getMethodExecStore();
+ getMessages();
+ getVarResolver();
+ getConfig();
+ getResponseProcessors();
+ getCallLogger();
+ getPartSerializer();
+ getPartParser();
+ getJsonSchemaGenerator();
+ getStaticFiles();
+ getDefaultRequestHeaders();
+ getDefaultResponseHeaders();
+ getDefaultRequestAttributes();
+ getDebugEnablement();
+ getSwaggerProvider();
+ }
+
private static Set<String> newCaseInsensitiveSet(String value) {
var s = new TreeSet<>(String.CASE_INSENSITIVE_ORDER) {
private static final long serialVersionUID = 1L;
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 7d82be18c4..adb7fa4875 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
@@ -63,6 +63,9 @@ public final class RestServerConstants {
/** The {@code "renderResponseStackTraces"} annotation attribute name —
used in {@code noInherit} matching. */
public static final String PROPERTY_renderResponseStackTraces =
"renderResponseStackTraces";
+ /** The {@code "eagerInit"} annotation attribute name — used in {@code
noInherit} matching. */
+ public static final String PROPERTY_eagerInit = "eagerInit";
+
/** The {@code "clientVersionHeader"} annotation attribute name — used
in {@code noInherit} matching. */
public static final String PROPERTY_clientVersionHeader =
"clientVersionHeader";
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 51ecdbfe5d..000482d33a 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
@@ -242,6 +242,31 @@ public @interface Rest {
*/
String config() default "";
+ /**
+ * Eagerly initializes framework-managed memoizers during {@link
RestContext} construction.
+ *
+ * <p>
+ * When enabled, framework bean memoizers and operation/child-context
memoizers are force-fired inside the
+ * constructor try/catch so startup-time configuration errors fail fast.
+ *
+ * <p>
+ * This setting is disabled by default.
+ *
+ * <ul class='values'>
+ * <li><js>"true"</js> - Force eager initialization during context
construction.
+ * <li><js>"false"</js> - Keep initialization lazy until first use
(default).
+ * </ul>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * Supports <a class="doclink"
href="https://juneau.apache.org/docs/topics/RestServerSvlVariables">SVL
Variables</a>
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String eagerInit() default "";
+
/**
* Supported content media types.
*
@@ -304,7 +329,7 @@ public @interface Rest {
* <p>
* Accepted values include {@code "allowedParserOptions"}, {@code
"allowedSerializerOptions"},
* {@code "allowedHeaderParams"}, {@code "allowedMethodHeaders"},
{@code "allowedMethodParams"},
- * {@code "disableContentParam"}, {@code "renderResponseStackTraces"},
{@code "clientVersionHeader"},
+ * {@code "disableContentParam"}, {@code "renderResponseStackTraces"},
{@code "eagerInit"}, {@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 bc91305b03..a9f287fec3 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
@@ -83,6 +83,7 @@ public class RestAnnotation {
private String allowedMethodParams = "";
private String clientVersionHeader = "";
private String config = "";
+ private String eagerInit = "";
private String debug = "";
private String debugDefault = "";
private String debugOn = "";
@@ -229,6 +230,17 @@ public class RestAnnotation {
return this;
}
+ /**
+ * Sets the {@link Rest#eagerInit()} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder eagerInit(String value) {
+ eagerInit = value;
+ return this;
+ }
+
/**
* Sets the {@link Rest#consumes()} property on this annotation.
*
@@ -705,6 +717,7 @@ public class RestAnnotation {
private final String allowedMethodParams;
private final String clientVersionHeader;
private final String config;
+ private final String eagerInit;
private final String debug;
private final String debugDefault;
private final String debugOn;
@@ -748,6 +761,7 @@ public class RestAnnotation {
children = copyOf(b.children);
clientVersionHeader = b.clientVersionHeader;
config = b.config;
+ eagerInit = b.eagerInit;
allowedParserOptions = copyOf(b.allowedParserOptions);
allowedSerializerOptions =
copyOf(b.allowedSerializerOptions);
noInherit = copyOf(b.noInherit);
@@ -833,6 +847,11 @@ public class RestAnnotation {
return config;
}
+ @Override /* Overridden from Rest */
+ public String eagerInit() {
+ return eagerInit;
+ }
+
@Override /* Overridden from Rest */
public String[] allowedParserOptions() {
return allowedParserOptions;
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestInject.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestInject.java
index a7fa2b4127..485ef91f1c 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestInject.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestInject.java
@@ -137,6 +137,33 @@ import org.apache.juneau.svl.*;
* {@link RestInject#methodScope()} annotation can be used to
apply to method-level only (when applicable).
* </ul>
*
+ * <h5 class='section'>Precedence (since 9.5.0):</h5>
+ * <p>
+ * {@code @RestInject} acts as a <i>programmable default</i>, analogous to
Spring's
+ * <c>@ConditionalOnMissingBean</c>. When a {@link RestContext}
resolves a framework-managed bean
+ * (<c>CallLogger</c>, <c>EncoderSet</c>, <c>SerializerSet</c>,
<c>ParserSet</c>, <c>ThrownStore</c>,
+ * <c>Config</c>, <c>VarResolver</c>, <c>HttpPartSerializer</c>,
<c>HttpPartParser</c>, etc.), the lookup
+ * walks the following tiers in order, returning the first hit:
+ * </p>
+ * <ol>
+ * <li><b>Overriding-parent bean store</b> — Spring beans (in
<c>juneau-rest-server-springboot</c> deployments,
+ * via <c>SpringBeanStore</c>), or any bean reachable through
{@link
org.apache.juneau.cp.BasicBeanStore.Builder#overridingParent(org.apache.juneau.cp.BasicBeanStore)
overridingParent(...)}.</li>
+ * <li><b>{@code @RestInject} method/field on the resource class</b> —
registered as a regular bean-store
+ * entry, beating the framework default.</li>
+ * <li><b>Memoizer-backed framework default</b> — built into {@link
RestContext} as a default supplier.</li>
+ * </ol>
+ * <p>
+ * In other words: a Spring <c>@Bean</c> of type <c>CallLogger</c>
wins over a {@code @RestInject CallLogger}
+ * method on the same servlet, which in turn wins over the framework's
built-in <c>BasicCallLogger</c>. Non-Spring
+ * deployments have an empty overriding-parent layer, so the chain
naturally collapses to
+ * {@code @RestInject > default}.
+ * </p>
+ * <p>
+ * Prior to 9.5 the order was {@code @RestInject > Spring > default}. See
the 9.5.0 release notes for migration
+ * guidance if you need to keep that legacy behavior (typically by
removing the Spring <c>@Bean</c> or marking the
+ * {@code @RestInject} method's type with a Spring-native override such as
<c>@Primary</c>).
+ * </p>
+ *
* <p>
* Any of the following types can be customized via injection:
* <table class='w800 styled'>
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BasicBeanStore_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BasicBeanStore_Test.java
index a838cd453a..f269dfba8f 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BasicBeanStore_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/inject/BasicBeanStore_Test.java
@@ -916,5 +916,196 @@ class BasicBeanStore_Test extends TestBase {
assertNotNull(result);
assertFalse(result.isEmpty());
}
+
+
//====================================================================================================
+ // addDefaultSupplier(...) and overriding-parent precedence
+
//====================================================================================================
+
+ @Test
+ void r01_addDefaultSupplier_unnamed_fallsBackWhenNoEntry() {
+ var store = new BasicBeanStore2(null);
+ var defaultBean = new TestBean("default");
+ store.addDefaultSupplier(TestBean.class, () -> defaultBean);
+
+ assertTrue(store.getBean(TestBean.class).isPresent());
+ assertSame(defaultBean, store.getBean(TestBean.class).get());
+ }
+
+ @Test
+ void r02_addDefaultSupplier_localEntryBeatsDefault() {
+ var store = new BasicBeanStore2(null);
+ var defaultBean = new TestBean("default");
+ var localBean = new TestBean("local");
+ store.addDefaultSupplier(TestBean.class, () -> defaultBean);
+ store.addBean(TestBean.class, localBean);
+
+ assertSame(localBean,
store.getBean(TestBean.class).orElseThrow());
+ }
+
+ @Test
+ void r03_addDefaultSupplier_regularParentBeatsDefault() {
+ var parent = new BasicBeanStore2(null);
+ parent.addBean(TestBean.class, new TestBean("parent"));
+
+ var store = new BasicBeanStore2(parent);
+ store.addDefaultSupplier(TestBean.class, () -> new
TestBean("default"));
+
+ assertEquals("parent",
store.getBean(TestBean.class).orElseThrow().getName());
+ }
+
+ @Test
+ void r04_addDefaultSupplier_named() {
+ var store = new BasicBeanStore2(null);
+ store.addDefaultSupplier(TestBean.class, () -> new
TestBean("dn"), "n1");
+
+ assertTrue(store.getBean(TestBean.class, "n1").isPresent());
+ assertEquals("dn", store.getBean(TestBean.class,
"n1").orElseThrow().getName());
+ assertFalse(store.getBean(TestBean.class).isPresent());
+ assertFalse(store.getBean(TestBean.class, "other").isPresent());
+ }
+
+ @Test
+ void r05_addDefaultSupplier_clearedByClear() {
+ var store = new BasicBeanStore2(null);
+ store.addDefaultSupplier(TestBean.class, () -> new
TestBean("d"));
+ assertTrue(store.getBean(TestBean.class).isPresent());
+
+ store.clear();
+ assertFalse(store.getBean(TestBean.class).isPresent());
+ }
+
+ @Test
+ void s01_overridingParent_beatsLocalEntry() {
+ var spring = new BasicBeanStore2(null);
+ spring.addBean(TestBean.class, new TestBean("spring"));
+
+ var store = new BasicBeanStore2(null, spring);
+ store.addBean(TestBean.class, new TestBean("local"));
+
+ assertEquals("spring",
store.getBean(TestBean.class).orElseThrow().getName());
+ }
+
+ @Test
+ void s02_overridingParent_beatsDefault() {
+ var spring = new BasicBeanStore2(null);
+ spring.addBean(TestBean.class, new TestBean("spring"));
+
+ var store = new BasicBeanStore2(null, spring);
+ store.addDefaultSupplier(TestBean.class, () -> new
TestBean("default"));
+
+ assertEquals("spring",
store.getBean(TestBean.class).orElseThrow().getName());
+ }
+
+ @Test
+ void s03_overridingParent_localEntryBeatsRegularParent() {
+ var regularParent = new BasicBeanStore2(null);
+ regularParent.addBean(TestBean.class, new
TestBean("regular-parent"));
+
+ var spring = new BasicBeanStore2(null);
+ // no spring binding for TestBean
+
+ var store = new BasicBeanStore2(regularParent, spring);
+ store.addBean(TestBean.class, new TestBean("local"));
+
+ assertEquals("local",
store.getBean(TestBean.class).orElseThrow().getName());
+ }
+
+ @Test
+ void s04_overridingParent_namedLookup() {
+ var spring = new BasicBeanStore2(null);
+ spring.addBean(TestBean.class, new TestBean("spring"),
"primary");
+
+ var store = new BasicBeanStore2(null, spring);
+ store.addBean(TestBean.class, new TestBean("local"), "primary");
+
+ assertEquals("spring", store.getBean(TestBean.class,
"primary").orElseThrow().getName());
+ assertFalse(store.getBean(TestBean.class, "other").isPresent());
+ }
+
+ @Test
+ void s05_fullPrecedenceOrder() {
+ // Set up: overriding parent (Spring), regular parent, local
entry, local default
+ var regularParent = new BasicBeanStore2(null);
+ regularParent.addBean(TestBean.class, new
TestBean("regular-parent"));
+
+ var spring = new BasicBeanStore2(null);
+ spring.addBean(TestBean.class, new TestBean("spring"));
+
+ var store = new BasicBeanStore2(regularParent, spring);
+ store.addBean(TestBean.class, new TestBean("local"));
+ store.addDefaultSupplier(TestBean.class, () -> new
TestBean("default"));
+
+ // Spring (overriding) wins.
+ assertEquals("spring",
store.getBean(TestBean.class).orElseThrow().getName());
+ }
+
+ @Test
+ void s06_precedenceOrder_noOverriding_noEntry() {
+ // Only regular parent and default — regular parent wins over
default.
+ var regularParent = new BasicBeanStore2(null);
+ regularParent.addBean(TestBean.class, new
TestBean("regular-parent"));
+
+ var store = new BasicBeanStore2(regularParent);
+ store.addDefaultSupplier(TestBean.class, () -> new
TestBean("default"));
+
+ assertEquals("regular-parent",
store.getBean(TestBean.class).orElseThrow().getName());
+ }
+
+ @Test
+ void s07_precedenceOrder_noOverridingMatch_fallsThroughToLocal() {
+ // Spring has no match, local entry should be returned.
+ var spring = new BasicBeanStore2(null);
+ // no binding for TestBean
+
+ var store = new BasicBeanStore2(null, spring);
+ store.addBean(TestBean.class, new TestBean("local"));
+
+ assertEquals("local",
store.getBean(TestBean.class).orElseThrow().getName());
+ }
+
+ @Test
+ void s08_overridingParent_getBeanSupplier() {
+ var spring = new BasicBeanStore2(null);
+ spring.addBean(TestBean.class, new TestBean("spring"));
+
+ var store = new BasicBeanStore2(null, spring);
+ store.addBean(TestBean.class, new TestBean("local"));
+
+ var supplier = store.getBeanSupplier(TestBean.class);
+ assertTrue(supplier.isPresent());
+ assertEquals("spring", supplier.get().get().getName());
+ }
+
+ @Test
+ void s09_overridingParent_hasBean() {
+ var spring = new BasicBeanStore2(null);
+ spring.addBean(TestBean.class, new TestBean("spring"));
+
+ var store = new BasicBeanStore2(null, spring);
+ assertTrue(store.hasBean(TestBean.class));
+ assertFalse(store.hasBean(AnotherBean.class));
+ }
+
+ @Test
+ void s10_overridingParent_getBeansOfType_overridesLocalNamed() {
+ var spring = new BasicBeanStore2(null);
+ spring.addBean(TestBean.class, new TestBean("spring-a"), "a");
+ spring.addBean(TestBean.class, new TestBean("spring-b"), "b");
+
+ var store = new BasicBeanStore2(null, spring);
+ store.addBean(TestBean.class, new TestBean("local-a"), "a");
+ store.addBean(TestBean.class, new TestBean("local-c"), "c");
+ store.addDefaultSupplier(TestBean.class, () -> new
TestBean("default-d"), "d");
+
+ var beans = store.getBeansOfType(TestBean.class);
+ // "a" is in both local and overriding parent — overriding wins.
+ assertEquals("spring-a", beans.get("a").getName());
+ // "b" only in overriding parent.
+ assertEquals("spring-b", beans.get("b").getName());
+ // "c" only local.
+ assertEquals("local-c", beans.get("c").getName());
+ // "d" only in defaults.
+ assertEquals("default-d", beans.get("d").getName());
+ }
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/cp/BeanStore_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/cp/BeanStore_Test.java
index 239b1ec617..f0706203e9 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cp/BeanStore_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cp/BeanStore_Test.java
@@ -723,6 +723,95 @@ class BeanStore_Test extends TestBase {
assertThrowsWithMessage(ExecutableException.class, "Could not
instantiate class "+D17.class.getName()+": Protected constructor found but
could not find prerequisites: Builder or Builder,Integer or Integer.",
()->BeanCreator.<D17>of(D17.class, bs).builder(Boolean.class, true).run());
}
+
//-----------------------------------------------------------------------------------------------------------------
+ // Default suppliers and overriding-parent precedence
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test void e01_addDefaultSupplier_unnamed_fallsBackWhenNoEntry() {
+ var bs = BasicBeanStore.create().build();
+ bs.addDefaultSupplier(String.class, () -> "default");
+ assertString("default", bs.getBean(String.class).orElseThrow());
+ }
+
+ @Test void e02_addDefaultSupplier_localEntryBeatsDefault() {
+ var bs = BasicBeanStore.create().build();
+ bs.addDefaultSupplier(String.class, () -> "default");
+ bs.addBean(String.class, "local");
+ assertString("local", bs.getBean(String.class).orElseThrow());
+ }
+
+ @Test void e03_addDefaultSupplier_regularParentBeatsDefault() {
+ var parent =
BasicBeanStore.create().build().addBean(String.class, "parent");
+ var bs = BasicBeanStore.create().parent(parent).build();
+ bs.addDefaultSupplier(String.class, () -> "default");
+ assertString("parent", bs.getBean(String.class).orElseThrow());
+ }
+
+ @Test void e04_addDefaultSupplier_named() {
+ var bs = BasicBeanStore.create().build();
+ bs.addDefaultSupplier(String.class, () -> "named-default",
"n1");
+ assertString("named-default", bs.getBean(String.class,
"n1").orElseThrow());
+ assertFalse(bs.getBean(String.class).isPresent());
+ assertFalse(bs.getBean(String.class, "other").isPresent());
+ }
+
+ @Test void e05_addDefaultSupplier_clearedByClear() {
+ var bs = BasicBeanStore.create().build();
+ bs.addDefaultSupplier(String.class, () -> "default");
+ assertTrue(bs.getBean(String.class).isPresent());
+ bs.clear();
+ assertFalse(bs.getBean(String.class).isPresent());
+ }
+
+ @Test void e06_overridingParent_beatsLocalEntry() {
+ var spring =
BasicBeanStore.create().build().addBean(String.class, "spring");
+ var bs =
BasicBeanStore.create().overridingParent(spring).build();
+ bs.addBean(String.class, "local");
+ assertString("spring", bs.getBean(String.class).orElseThrow());
+ }
+
+ @Test void e07_overridingParent_beatsDefault() {
+ var spring =
BasicBeanStore.create().build().addBean(String.class, "spring");
+ var bs =
BasicBeanStore.create().overridingParent(spring).build();
+ bs.addDefaultSupplier(String.class, () -> "default");
+ assertString("spring", bs.getBean(String.class).orElseThrow());
+ }
+
+ @Test void e08_overridingParent_localEntryBeatsRegularParent() {
+ var regularParent =
BasicBeanStore.create().build().addBean(String.class, "regular-parent");
+ var spring = BasicBeanStore.create().build();
+ var bs =
BasicBeanStore.create().parent(regularParent).overridingParent(spring).build();
+ bs.addBean(String.class, "local");
+ assertString("local", bs.getBean(String.class).orElseThrow());
+ }
+
+ @Test void e09_overridingParent_namedLookup() {
+ var spring =
BasicBeanStore.create().build().addBean(String.class, "spring", "primary");
+ var bs =
BasicBeanStore.create().overridingParent(spring).build();
+ bs.addBean(String.class, "local", "primary");
+ assertString("spring", bs.getBean(String.class,
"primary").orElseThrow());
+ }
+
+ @Test void e10_fullPrecedenceOrder() {
+ var regularParent =
BasicBeanStore.create().build().addBean(String.class, "regular-parent");
+ var spring =
BasicBeanStore.create().build().addBean(String.class, "spring");
+ var bs =
BasicBeanStore.create().parent(regularParent).overridingParent(spring).build();
+ bs.addBean(String.class, "local");
+ bs.addDefaultSupplier(String.class, () -> "default");
+ assertString("spring", bs.getBean(String.class).orElseThrow());
+ }
+
+ @Test void e11_hasBean_includesAllTiers() {
+ var spring =
BasicBeanStore.create().build().addBean(Integer.class, 1);
+ var bs =
BasicBeanStore.create().overridingParent(spring).build();
+ bs.addBean(String.class, "local");
+ bs.addDefaultSupplier(Long.class, () -> 99L);
+ assertTrue(bs.hasBean(Integer.class)); // overriding parent
+ assertTrue(bs.hasBean(String.class)); // local entry
+ assertTrue(bs.hasBean(Long.class)); // default
+ assertFalse(bs.hasBean(Double.class)); // nowhere
+ }
+
//-----------------------------------------------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------------------------------------------
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_Precedence_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_Precedence_Test.java
new file mode 100644
index 0000000000..5f371d30af
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_Precedence_Test.java
@@ -0,0 +1,128 @@
+/*
+ * 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.cp.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.logger.*;
+import org.apache.juneau.rest.mock.*;
+import org.apache.juneau.rest.stats.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates the 9.5 precedence model for framework-managed beans on a {@link
RestContext}:
+ * <ol>
+ * <li>Beans inherited from an "overriding parent" bean store (e.g.
Spring) win.
+ * <li>{@code @RestInject} factory methods on the resource win over
defaults.
+ * <li>Memoizer-backed framework defaults are the last-resort fallback.
+ * </ol>
+ *
+ * <p>
+ * Spring is simulated here by wiring a {@link BasicBeanStore} as the {@code
overridingParent} of the
+ * resource's bean store via {@link Rest#beanStore()} on a custom {@link
BasicBeanStore} subclass. This
+ * avoids pulling in spring-boot at test time but exercises the same code path
that
+ * {@code SpringRestServlet.createBeanStore(...)} drives at runtime.
+ */
+class RestContext_Precedence_Test extends TestBase {
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Marker beans
+
//-----------------------------------------------------------------------------------------------------------------
+
+ private static final CallLogger SPRING_LOGGER =
BasicCallLogger.create(BasicBeanStore.INSTANCE).build();
+ private static final CallLogger RESTINJECT_LOGGER =
BasicCallLogger.create(BasicBeanStore.INSTANCE).build();
+ private static final ThrownStore RESTINJECT_THROWN_STORE =
ThrownStore.create().build();
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Spring-substitute bean store (acts as the overriding parent layer)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ /** A bean store seeded with a "Spring" {@link CallLogger} via the
overriding-parent mechanism. */
+ public static class SpringLikeBeanStore extends BasicBeanStore {
+ protected SpringLikeBeanStore(Builder builder) {
+
super(builder.overridingParent(BasicBeanStore.create().build().addBean(CallLogger.class,
SPRING_LOGGER)));
+ }
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // 1. @RestInject beats default (no Spring layer present)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest
+ public static class A_RestInjectOnly {
+ @RestInject static CallLogger callLoggerCapture;
+ @RestInject public CallLogger callLogger() { return
RESTINJECT_LOGGER; }
+ }
+
+ @Test
+ void a01_restInject_beatsDefault() {
+ MockRestClient.buildLax(A_RestInjectOnly.class);
+ assertSame(RESTINJECT_LOGGER,
A_RestInjectOnly.callLoggerCapture);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // 2. Spring (overriding parent) beats @RestInject
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(beanStore=SpringLikeBeanStore.class)
+ public static class B_SpringWins {
+ @RestInject static CallLogger callLoggerCapture;
+ @RestInject public CallLogger callLogger() { return
RESTINJECT_LOGGER; }
+ }
+
+ @Test
+ void b01_spring_beatsRestInject() {
+ MockRestClient.buildLax(B_SpringWins.class);
+ assertSame(SPRING_LOGGER, B_SpringWins.callLoggerCapture,
"Spring (overriding parent) should win over @RestInject method");
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // 3. Spring (overriding parent) beats default (no @RestInject method)
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(beanStore=SpringLikeBeanStore.class)
+ public static class C_SpringOnly {
+ @RestInject static CallLogger callLoggerCapture;
+ }
+
+ @Test
+ void c01_spring_beatsDefault() {
+ MockRestClient.buildLax(C_SpringOnly.class);
+ assertSame(SPRING_LOGGER, C_SpringOnly.callLoggerCapture);
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // 4. With NO overriding-parent binding for the type, @RestInject still
wins over the default
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Rest(beanStore=SpringLikeBeanStore.class)
+ public static class D_PartialSpring {
+ @RestInject static CallLogger callLoggerCapture;
+ @RestInject static ThrownStore thrownStoreCapture;
+ @RestInject public ThrownStore thrownStore() { return
RESTINJECT_THROWN_STORE; }
+ }
+
+ @Test
+ void d01_partialSpring_restInjectStillBeatsDefaultForUnboundType() {
+ MockRestClient.buildLax(D_PartialSpring.class);
+ assertSame(SPRING_LOGGER, D_PartialSpring.callLoggerCapture);
// Spring binding wins
+ assertSame(RESTINJECT_THROWN_STORE,
D_PartialSpring.thrownStoreCapture); // @RestInject wins (Spring has no
binding)
+ }
+}