You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/05/11 19:44:47 UTC

(juneau) 06/19: refactor(inject): TODO-15 phase-3 batch 6 — drop @Rest(beanStore=X.class), migrate Spring to v2, lock in @RestInject > Spring precedence

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

commit 9a71bfaa95c9f4fe4a6d1ce2901d543790f7ec1d
Author: James Bognar <ja...@jamesbog-ltmjqf2.internal.salesforce.com>
AuthorDate: Fri May 8 14:10:50 2026 -0400

    refactor(inject): TODO-15 phase-3 batch 6 — drop @Rest(beanStore=X.class), migrate Spring to v2, lock in @RestInject > Spring precedence
    
    - SpringRestServlet.createBeanStore() now returns WritableBeanStore (instead of BasicBeanStore) and
      instantiates SpringBeanStore2 directly.
    - RestContext.createBeanStore() simplified: drop the @Rest(beanStore=X.class) Builder.type() handling
      and the legacy BasicBeanStore.create().overridingParent(...).build() chain.  New shape is just
      `new BasicBeanStore2(parentBs)`, optionally replaced by an @RestInject WritableBeanStore factory
      method on the resource.
    - @Rest.beanStore() annotation attribute removed (Rest.java, RestAnnotation.java Builder + materialized
      impl, DefaultConfig.java).  Tests updated; RestContext_Precedence_Test rewritten against the new
      factory-method-based mechanism.
    - RestOpContext.java line 1097 migrated from `BasicBeanStore.of((BasicBeanStore) bootstrap)…` to
      `new BasicBeanStore2(bootstrap)…` to fix a ClassCastException now that bootstrapBeanStore is a
      BasicBeanStore2 (forced by batch-5).
    - RestOpSessionArgs.create() resolves both WritableBeanStore and BeanStore parameter types instead
      of legacy BasicBeanStore.
    
    Precedence model lock-in:
    - Added BasicBeanStore2.getDefaultSupplier(Class[, String]) accessor that returns the locally-registered
      default supplier without walking the resolve chain.
    - RestContext line 1251 @RestInject method walk no longer skips framework types.  For framework types
      it promotes the existing memoizer-backed default supplier into a local-entry supplier (no
      double-invocation; both layers reference the same memoizer).  For non-framework types it keeps the
      createBeanFromMethod -> addBean flow.
    - Net effect: @RestInject method results uniformly take precedence over Spring/user-supplied bean-store
      bindings, which in turn take precedence over framework defaults.  RestContext_Precedence_Test
      documents and locks in this model with 5 scenarios.
    
    Co-authored-by: Cursor <cu...@cursor.com>
---
 .../juneau/commons/inject/BasicBeanStore2.java     |  41 +++++
 .../juneau/rest/springboot/SpringRestServlet.java  |  10 +-
 .../java/org/apache/juneau/rest/RestContext.java   |  68 +++++---
 .../java/org/apache/juneau/rest/RestOpContext.java |   7 +-
 .../org/apache/juneau/rest/annotation/Rest.java    |  17 --
 .../juneau/rest/annotation/RestAnnotation.java     |  19 ---
 .../apache/juneau/rest/arg/RestOpSessionArgs.java  |   7 +-
 .../apache/juneau/rest/config/DefaultConfig.java   |   1 -
 .../juneau/rest/RestContext_Builder_Test.java      |  42 +----
 .../juneau/rest/RestContext_Precedence_Test.java   | 182 ++++++++++++++++-----
 .../rest/annotation/RestAnnotation_Test.java       |   8 +-
 11 files changed, 248 insertions(+), 154 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 e90889ff92..729d617539 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
@@ -369,6 +369,47 @@ public class BasicBeanStore2 implements WritableBeanStore {
 		return nn(typeMap) && typeMap.containsKey(emptyIfNull(name));
 	}
 
+	/**
+	 * Returns the default supplier registered locally for the specified unnamed bean type, or empty if none.
+	 *
+	 * <p>
+	 * Parent and overriding-parent stores are <i>not</i> consulted.  This returns the supplier itself,
+	 * unwrapped from any resolution chain &mdash; it lets callers promote a memoizer-backed default
+	 * supplier into a higher-precedence layer (e.g. a local entry) without re-invoking the underlying
+	 * factory.
+	 *
+	 * @param <T> The bean type.
+	 * @param beanType The bean type to look up.
+	 * @return The locally-registered default supplier, or {@link Optional#empty()} if not present.
+	 */
+	@SuppressWarnings("unchecked")
+	public <T> Optional<Supplier<T>> getDefaultSupplier(Class<T> beanType) {
+		return getDefaultSupplier(beanType, null);
+	}
+
+	/**
+	 * Returns the default supplier registered locally for the specified bean type and name, or empty if none.
+	 *
+	 * <p>
+	 * Parent and overriding-parent stores are <i>not</i> consulted.  This returns the supplier itself,
+	 * unwrapped from any resolution chain &mdash; it lets callers promote a memoizer-backed default
+	 * supplier into a higher-precedence layer (e.g. a local entry) without re-invoking the underlying
+	 * factory.
+	 *
+	 * @param <T> The bean type.
+	 * @param beanType The bean type to look up.
+	 * @param name The bean name.  Can be <jk>null</jk> for unnamed beans.
+	 * @return The locally-registered default supplier, or {@link Optional#empty()} if not present.
+	 */
+	@SuppressWarnings("unchecked")
+	public <T> Optional<Supplier<T>> getDefaultSupplier(Class<T> beanType, String name) {
+		var typeMap = defaults.get(beanType);
+		if (typeMap == null)
+			return opte();
+		var supplier = typeMap.get(emptyIfNull(name));
+		return supplier == null ? opte() : opt((Supplier<T>) supplier);
+	}
+
 	/**
 	 * Returns <jk>true</jk> if this store contains at least one unnamed bean of the specified type.
 	 *
diff --git a/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/springboot/SpringRestServlet.java b/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/springboot/SpringRestServlet.java
index ce10884950..7647f9126e 100644
--- a/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/springboot/SpringRestServlet.java
+++ b/juneau-rest/juneau-rest-server-springboot/src/main/java/org/apache/juneau/rest/springboot/SpringRestServlet.java
@@ -18,7 +18,7 @@ package org.apache.juneau.rest.springboot;
 
 import java.util.*;
 
-import org.apache.juneau.cp.*;
+import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.servlet.*;
 import org.springframework.beans.factory.annotation.*;
@@ -50,11 +50,11 @@ public abstract class SpringRestServlet extends RestServlet {
 	/**
 	 * Hook into Spring bean injection framework.
 	 *
-	 * @param parent Optional parent resource.
-	 * @return A BasicBeanStore that retrieves beans from the Spring Boot app context.
+	 * @param parent Optional parent resource bean store, used as a fallback after Spring's context.
+	 * @return A {@link WritableBeanStore} backed by Spring's {@link ApplicationContext}.
 	 */
 	@RestInject
-	public BasicBeanStore createBeanStore(Optional<BasicBeanStore> parent) {
-		return new SpringBeanStore(appContext, parent, this);
+	public WritableBeanStore createBeanStore(Optional<BeanStore> parent) {
+		return new SpringBeanStore2(appContext.orElse(null), parent.orElse(null));
 	}
 }
\ No newline at end of file
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 09601af34e..ba5f7d115e 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
@@ -18,6 +18,7 @@ package org.apache.juneau.rest;
 
 import org.apache.juneau.commons.http.MediaType;
 import org.apache.juneau.commons.inject.BasicBeanStore2;
+import org.apache.juneau.commons.inject.BeanStore;
 import org.apache.juneau.commons.inject.WritableBeanStore;
 
 import static jakarta.servlet.http.HttpServletResponse.*;
@@ -334,27 +335,32 @@ public class RestContext extends Context {
 	/**
 	 * Creates the bean store for this context.
 	 *
+	 * <p>
+	 * 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 {@code addDefaultSupplier}, putting them at the
+	 * bottom of the resolution order.
+	 *
+	 * <p>
+	 * Resolution:
+	 * <ol>
+	 * 	<li>If the resource declares an {@code @RestInject} factory method returning a
+	 * 		{@link WritableBeanStore} (e.g. {@code SpringRestServlet.createBeanStore(Optional<BeanStore>)}),
+	 * 		that store is used directly.  Spring integration relies on this hook.
+	 * 	<li>Otherwise a fresh {@link BasicBeanStore2} is created with {@code parentBs} as its
+	 * 		overriding parent.
+	 * </ol>
+	 *
 	 * @param parentBs
 	 * 	The parent (bootstrap) bean store to layer onto, or {@code null} for root resources.
 	 * @param resource
 	 * 	The REST servlet/bean instance that this context is defined against.
-	 * @return A new bean store builder.
+	 * @return The bean store for this context.
 	 */
-	private BasicBeanStore.Builder createBeanStore(WritableBeanStore parentBs, Supplier<?> resource) {
-		// 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((BasicBeanStore) 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));
-
-		// Replace with bean from: @RestInject public [static] BasicBeanStore xxx(<args>)
-		var bs = v.get().build();
-		bs.createBeanFromMethod(BasicBeanStore.class, resource.get(), RestContext::isRestInjectMethod).ifPresent(v.get()::impl);
-
-		return v.get();
+	private WritableBeanStore createBeanStore(WritableBeanStore parentBs, Supplier<?> resource) {
+		var defaultBs = new BasicBeanStore2(parentBs);
+		return defaultBs.createBeanFromMethod(WritableBeanStore.class, resource.get(), RestContext::isRestInjectMethod)
+			.orElse(defaultBs);
 	}
 	private RestContext parentContext() { return parentContext; }
 	private RestOperations restOperations() { return restOperations.get(); }
@@ -1183,12 +1189,11 @@ public class RestContext extends Context {
 			// Determine the parent (bootstrap) store: inherited from parent resource if present.
 			WritableBeanStore parentBs = parentContext != null ? parentContext.bootstrapBeanStore : null;
 
-			// Build the initial beanStore; apply @Rest(beanStore) + optional @RestInject override.
+			// Build the initial beanStore; honor an optional @RestInject WritableBeanStore 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
 			WritableBeanStore bs = createBeanStore(parentBs, rs)
-				.build()
 				.addBean(ResourceSupplier.class, rs)
 				.addBean(ServletConfig.class, nn(builder.inner) ? builder.inner : builder)
 				.addBean(ServletContext.class, (nn(builder.inner) ? builder.inner : builder).getServletContext());
@@ -1238,17 +1243,32 @@ public class RestContext extends Context {
 				));
 			// @formatter:on
 
-			// 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
+			// Run @RestInject methods and register their results as LOCAL entries (level 2 of resolve()).
+			//
+			// For non-framework types: invoke the @RestInject method directly via createBeanFromMethod
+			// and store the result via addBean.
+			//
+			// For framework types (those with a default supplier registered above): the @RestInject
+			// scan already ran inside the corresponding memoizer body (see e.g. createCallLogger()),
+			// so re-invoking createBeanFromMethod here would create a SECOND instance and produce
+			// inconsistent state between the framework's memoizer-backed bean and the bean store's
+			// local entry.  Instead, PROMOTE the existing default supplier (which is memoizer-backed
+			// and resolves to the @RestInject value when one was supplied) into a local-entry supplier.
+			// Promoting at level 2 means @RestInject results win over a parent (Spring) at level 3.
+			//
+			// Net effect: @RestInject method results uniformly take precedence over Spring/parent
+			// bindings for both framework and user-defined types.  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 (beanStore.hasDefaultSupplier(rt, name))
+				// Skip the WritableBeanStore factory (already consumed by createBeanStore()).
+				if (WritableBeanStore.class.equals(rt) || BeanStore.class.equals(rt))
 					return;
+				if (beanStore instanceof BasicBeanStore2 bbs2 && bbs2.hasDefaultSupplier(rt, name)) {
+					bbs2.getDefaultSupplier(rt, name).ifPresent(sup -> beanStore.addSupplier(rt, sup, name));
+					return;
+				}
 				beanStore.createBeanFromMethod(rt, resource.get(), RestContext::isRestInjectMethod)
 					.ifPresent(y -> beanStore.addBean(rt, y, name));
 			});
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestOpContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestOpContext.java
index 39f9b2d2ba..7aefacc2ca 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestOpContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestOpContext.java
@@ -1094,13 +1094,12 @@ public class RestOpContext extends Context implements Comparable<RestOpContext>
 			mi = MethodInfo.of(method).accessible();
 
 			// @formatter:off
-			var bs = (BasicBeanStore) (opBeanStore = BasicBeanStore.of((BasicBeanStore) context.getBootstrapBeanStore())
+			var bs = new BasicBeanStore2(context.getBootstrapBeanStore())
 				.addBean(RestOpContext.class, this)
 				.addBean(Method.class, method)
-				.addBean(AnnotationWorkList.class, appliedAnnotations));
+				.addBean(AnnotationWorkList.class, appliedAnnotations);
+			opBeanStore = bs;
 			// @formatter:on
-			bs.addBean(WritableBeanStore.class, bs);
-			bs.addBean(BasicBeanStore.class, bs);
 
 			bs.add(BeanContext.class, getBeanContext());
 			bs.add(RestConverter[].class, getConverters());
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 000482d33a..5c9312a78d 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
@@ -140,23 +140,6 @@ public @interface Rest {
 	 */
 	String allowedMethodParams() default "";
 
-	/**
-	 * The resolver used for resolving instances of child resources and various other beans including:
-	 * <ul>
-	 * 	<li>{@link CallLogger}
-	 * 	<li>{@link SwaggerProvider}
-	 * 	<li>{@link FileFinder}
-	 * 	<li>{@link StaticFiles}
-	 * </ul>
-	 *
-	 * <p>
-	 * Note that the <c>SpringRestServlet</c> classes uses the <c>SpringBeanStore</c> class to allow for any
-	 * Spring beans to be injected into your REST resources.
-	 *
-	 * @return The annotation value.
-	 */
-	Class<? extends BasicBeanStore> beanStore() default BasicBeanStore.Void.class;
-
 	/**
 	 * Specifies the logger to use for logging of HTTP requests and responses.
 	 *
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 a9f287fec3..182e434c7c 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
@@ -71,7 +71,6 @@ public class RestAnnotation {
 		private Class<? extends RestGuard>[] guards = new Class[0];
 		private Class<? extends SwaggerProvider> swaggerProvider = SwaggerProvider.Void.class;
 		private Class<? extends RestOpArg>[] restOpArgs = new Class[0];
-		private Class<? extends BasicBeanStore> beanStore = BasicBeanStore.Void.class;
 		private Class<? extends DebugEnablement> debugEnablement = DebugEnablement.Void.class;
 		private Class<? extends Serializer>[] serializers = new Class[0];
 		private Class<?>[] children = {};
@@ -155,17 +154,6 @@ public class RestAnnotation {
 			return this;
 		}
 
-		/**
-		 * Sets the {@link Rest#beanStore()} property on this annotation.
-		 *
-		 * @param value The new value for this property.
-		 * @return This object.
-		 */
-		public Builder beanStore(Class<? extends BasicBeanStore> value) {
-			beanStore = value;
-			return this;
-		}
-
 		/**
 		 * Instantiates a new {@link Rest @Rest} object initialized with this builder.
 		 *
@@ -705,7 +693,6 @@ public class RestAnnotation {
 		private final Class<? extends RestGuard>[] guards;
 		private final Class<? extends SwaggerProvider> swaggerProvider;
 		private final Class<? extends RestOpArg>[] restOpArgs;
-		private final Class<? extends BasicBeanStore> beanStore;
 		private final Class<? extends DebugEnablement> debugEnablement;
 		private final Class<? extends Serializer>[] serializers;
 		private final Class<?>[] children;
@@ -756,7 +743,6 @@ public class RestAnnotation {
 			allowedHeaderParams = b.allowedHeaderParams;
 			allowedMethodHeaders = b.allowedMethodHeaders;
 			allowedMethodParams = b.allowedMethodParams;
-			beanStore = b.beanStore;
 			callLogger = b.callLogger;
 			children = copyOf(b.children);
 			clientVersionHeader = b.clientVersionHeader;
@@ -822,11 +808,6 @@ public class RestAnnotation {
 			return allowedMethodParams;
 		}
 
-		@Override /* Overridden from Rest */
-		public Class<? extends BasicBeanStore> beanStore() {
-			return beanStore;
-		}
-
 		@Override /* Overridden from Rest */
 		public Class<? extends CallLogger> callLogger() {
 			return callLogger;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/RestOpSessionArgs.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/RestOpSessionArgs.java
index fbc47ad8f9..8cef03a781 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/RestOpSessionArgs.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/arg/RestOpSessionArgs.java
@@ -17,8 +17,8 @@
 package org.apache.juneau.rest.arg;
 
 import org.apache.juneau.commons.function.*;
+import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.commons.reflect.*;
-import org.apache.juneau.cp.*;
 import org.apache.juneau.rest.*;
 import org.apache.juneau.rest.annotation.*;
 
@@ -26,7 +26,8 @@ import org.apache.juneau.rest.annotation.*;
  * Resolves method parameters on {@link RestOp}-annotated Java methods of types found on the {@link RestOpSession} object.
  *
  * <ul class='javatree'>
- * 	<li class='jc'>{@link BasicBeanStore}
+ * 	<li class='jic'>{@link BeanStore}
+ * 	<li class='jic'>{@link WritableBeanStore}
  * 	<li class='jc'>{@link RestOpSession}
  * </ul>
  *
@@ -43,7 +44,7 @@ public class RestOpSessionArgs extends SimpleRestOperationArg {
 	 * @return A new arg, or <jk>null</jk> if the parameter type is not one of the supported types.
 	 */
 	public static RestOpSessionArgs create(ParameterInfo paramInfo) {
-		if (paramInfo.isType(BasicBeanStore.class))
+		if (paramInfo.isType(WritableBeanStore.class) || paramInfo.isType(BeanStore.class))
 			return new RestOpSessionArgs(RestOpSession::getBeanStore);
 		if (paramInfo.isType(RestOpSession.class))
 			return new RestOpSessionArgs(x -> x);
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/DefaultConfig.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/DefaultConfig.java
index cc965b4667..4041497b6f 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/DefaultConfig.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/DefaultConfig.java
@@ -122,7 +122,6 @@ import org.apache.juneau.serializer.annotation.*;
 	title="$S{j.title,$E{J_TITLE,}}",
 
 	// Injectable/overridable beans.
-	beanStore=BasicBeanStore.Void.class,  // Defaults to BasicBeanStore.
 	callLogger=CallLogger.Void.class,  // Defaults to BasicCallLogger.
 	debugEnablement=DebugEnablement.Void.class,  // Defaults to BasicDefaultEnablement.
 	staticFiles=StaticFiles.Void.class,  // Defaults to BasicStaticFiles.
diff --git a/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_Builder_Test.java b/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_Builder_Test.java
index 66e62ba2ad..6c7fda93aa 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_Builder_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_Builder_Test.java
@@ -20,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.annotation.Named;
-import org.apache.juneau.cp.*;
+import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.client.*;
 import org.apache.juneau.rest.config.*;
@@ -37,50 +37,20 @@ class RestContext_Builder_Test extends TestBase {
 
 	@Rest
 	public static class A1 {
-		@RestInject static BasicBeanStore beanStore;
+		@RestInject static WritableBeanStore beanStore;
 	}
 
 	@Test void a01_createBeanStore_default() {
 		MockRestClient.buildLax(A1.class);
-		assertEquals("BasicBeanStore", A1.beanStore.getClass().getSimpleName());
-	}
-
-	public static class MyBeanStore extends BasicBeanStore {
-		protected MyBeanStore(Builder builder) {
-			super(builder.parent(BasicBeanStore.create().build().addBean(A.class, new A())));
-		}
-	}
-
-	@Rest(beanStore=MyBeanStore.class)
-	public static class A2 {
-		@RestInject static BasicBeanStore beanStore;
-	}
-
-	@Test void a02_createBeanStore_annotation() {
-		MockRestClient.buildLax(A2.class);
-		assertNotNull(A2.beanStore.getBean(A.class));
-	}
-
-	@Rest
-	public static class A3 {
-		@RestInject static BasicBeanStore beanStore;
-
-		@RestInject BasicBeanStore.Builder beanStore(BasicBeanStore.Builder b) {
-			return b.type(MyBeanStore.class);
-		}
-	}
-
-	@Test void a03_createBeanStore_restBean1() {
-		MockRestClient.buildLax(A3.class);
-		assertNotNull(A3.beanStore.getBean(A.class));
+		assertEquals("BasicBeanStore2", A1.beanStore.getClass().getSimpleName());
 	}
 
 	@Rest
 	public static class A4 {
-		@RestInject static BasicBeanStore beanStore;
+		@RestInject static WritableBeanStore beanStore;
 
-		@RestInject BasicBeanStore beanStore() {
-			return BasicBeanStore.create().type(MyBeanStore.class).build();
+		@RestInject WritableBeanStore beanStore() {
+			return new BasicBeanStore2(null).addBean(A.class, new A());
 		}
 	}
 
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
index 5f371d30af..a901561f6a 100644
--- 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
@@ -18,7 +18,11 @@ package org.apache.juneau.rest;
 
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.util.*;
+import java.util.function.*;
+
 import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.cp.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.logger.*;
@@ -27,18 +31,30 @@ 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}:
+ * Validates the 9.5 bean-store precedence model on a {@link RestContext}.
+ *
+ * <p>
+ * Resolution order, top-to-bottom:
  * <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.
+ * 	<li>{@code @RestInject} factory methods on the resource.  For non-framework types these are
+ * 		registered as local entries directly; for framework types (e.g. {@link CallLogger},
+ * 		{@link ThrownStore}) the per-bean memoizer captures the {@code @RestInject} value, and
+ * 		{@code RestContext} promotes the memoizer-backed supplier into a local entry so that
+ * 		{@code @RestInject} uniformly wins.
+ * 	<li>User-supplied bean store from
+ * 		{@code @RestInject WritableBeanStore createBeanStore(...)}, including its
+ * 		{@link org.apache.juneau.rest.springboot.SpringBeanStore2}-style fallback to a backing
+ * 		{@code ApplicationContext}.  Consulted only when no {@code @RestInject} factory method
+ * 		exists for the type.
+ * 	<li>Memoizer-backed framework defaults (e.g. {@link BasicCallLogger}).  Fire only when neither
+ * 		a per-resource {@code @RestInject} method nor a user-supplied bean-store binding exists.
  * </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.
+ * Net effect: <b>{@code @RestInject} factory methods on the resource take precedence over
+ * Spring/user-supplied bindings, which in turn take precedence over framework defaults.</b>
+ * Spring/user-supplied bindings act as drop-in overrides for any type the resource doesn't
+ * customize via {@code @RestInject}, with the framework filling in defaults for anything else.
  */
 class RestContext_Precedence_Test extends TestBase {
 
@@ -51,78 +67,166 @@ class RestContext_Precedence_Test extends TestBase {
 	private static final ThrownStore RESTINJECT_THROWN_STORE = ThrownStore.create().build();
 
 	//-----------------------------------------------------------------------------------------------------------------
-	// Spring-substitute bean store (acts as the overriding parent layer)
+	// Spring-substitute bean store
+	//
+	// Simulates SpringBeanStore2: holds bindings in an internal "Spring app context" map that's
+	// only consulted from getBean(...) AFTER super.getBean(...) returns empty.  This places the
+	// "Spring" layer at the BOTTOM of the resolution chain, below local entries and below
+	// memoizer-backed default suppliers \u2014 exactly where real Spring sits.
 	//-----------------------------------------------------------------------------------------------------------------
 
-	/** 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)));
+	@SuppressWarnings("unchecked")
+	public static class SpringLikeBeanStore extends BasicBeanStore2 {
+
+		private final Map<Class<?>, Object> springBindings = new HashMap<>();
+
+		public SpringLikeBeanStore(BeanStore parent) {
+			super(parent);
 		}
+
+		public <T> SpringLikeBeanStore bindSpring(Class<T> type, T instance) {
+			springBindings.put(type, instance);
+			return this;
+		}
+
+		@Override
+		public <T> Optional<T> getBean(Class<T> beanType) {
+			return getBean(beanType, null);
+		}
+
+		@Override
+		public <T> Optional<T> getBean(Class<T> beanType, String name) {
+			var o = super.getBean(beanType, name);
+			if (o.isPresent())
+				return o;
+			return springLookup(beanType, name);
+		}
+
+		@Override
+		public <T> Optional<Supplier<T>> getBeanSupplier(Class<T> beanType) {
+			return getBeanSupplier(beanType, null);
+		}
+
+		@Override
+		public <T> Optional<Supplier<T>> getBeanSupplier(Class<T> beanType, String name) {
+			var o = super.getBeanSupplier(beanType, name);
+			if (o.isPresent())
+				return o;
+			var bound = springLookup(beanType, name).orElse(null);
+			return bound == null ? Optional.empty() : Optional.of(() -> bound);
+		}
+
+		private <T> Optional<T> springLookup(Class<T> beanType, String name) {
+			// "Spring" bindings are unnamed only — anything else falls through.
+			if (name != null && !name.isEmpty())
+				return Optional.empty();
+			return Optional.ofNullable((T) springBindings.get(beanType));
+		}
+	}
+
+	private static SpringLikeBeanStore springLikeBeanStore() {
+		return new SpringLikeBeanStore(null).bindSpring(CallLogger.class, SPRING_LOGGER);
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
-	// 1. @RestInject beats default (no Spring layer present)
+	// 1. @RestInject beats the memoizer-backed framework default.
 	//-----------------------------------------------------------------------------------------------------------------
 
 	@Rest
-	public static class A_RestInjectOnly {
+	public static class A_RestInjectBeatsDefault {
 		@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);
+		MockRestClient.buildLax(A_RestInjectBeatsDefault.class);
+		assertSame(RESTINJECT_LOGGER, A_RestInjectBeatsDefault.callLoggerCapture);
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
-	// 2. Spring (overriding parent) beats @RestInject
+	// 2. @RestInject beats Spring (Spring at fallback layer).
 	//-----------------------------------------------------------------------------------------------------------------
 
-	@Rest(beanStore=SpringLikeBeanStore.class)
-	public static class B_SpringWins {
+	@Rest
+	public static class B_RestInjectBeatsSpring {
 		@RestInject static CallLogger callLoggerCapture;
+		@RestInject public WritableBeanStore createBeanStore() { return springLikeBeanStore(); }
 		@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");
+	void b01_restInject_beatsSpring() {
+		MockRestClient.buildLax(B_RestInjectBeatsSpring.class);
+		assertSame(RESTINJECT_LOGGER, B_RestInjectBeatsSpring.callLoggerCapture, "@RestInject method should win over Spring fallback");
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
-	// 3. Spring (overriding parent) beats default (no @RestInject method)
+	// 3. Spring beats the framework default for framework bean types (when no @RestInject method exists).
+	//
+	// Without an @RestInject CallLogger method, the framework's memoizer-backed default supplier sits
+	// at level 4 of resolve(), below the user-supplied bean store (parent at level 3).  So Spring
+	// overrides the framework default.  This is intentional: if the user wired a CallLogger into Spring,
+	// they meant it to be used in preference to the auto-configured BasicCallLogger.
 	//-----------------------------------------------------------------------------------------------------------------
 
-	@Rest(beanStore=SpringLikeBeanStore.class)
-	public static class C_SpringOnly {
+	@Rest
+	public static class C_SpringBeatsDefault {
 		@RestInject static CallLogger callLoggerCapture;
+		@RestInject public WritableBeanStore createBeanStore() { return springLikeBeanStore(); }
 	}
 
 	@Test
-	void c01_spring_beatsDefault() {
-		MockRestClient.buildLax(C_SpringOnly.class);
-		assertSame(SPRING_LOGGER, C_SpringOnly.callLoggerCapture);
+	void c01_spring_beatsDefault_forFrameworkBean() {
+		MockRestClient.buildLax(C_SpringBeatsDefault.class);
+		assertSame(SPRING_LOGGER, C_SpringBeatsDefault.callLoggerCapture, "User-supplied bean store binding should win over framework default when no @RestInject is declared for the type");
 	}
 
 	//-----------------------------------------------------------------------------------------------------------------
-	// 4. With NO overriding-parent binding for the type, @RestInject still wins over the default
+	// 4. Spring fallback fills in for non-framework bean types when nothing else has a binding.
 	//-----------------------------------------------------------------------------------------------------------------
 
-	@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; }
+	public static class CustomBean {
+		final String marker;
+		CustomBean(String marker) { this.marker = marker; }
+	}
+
+	private static final CustomBean SPRING_CUSTOM = new CustomBean("from-spring");
+
+	private static SpringLikeBeanStore springLikeBeanStoreWithCustomBean() {
+		return new SpringLikeBeanStore(null)
+			.bindSpring(CallLogger.class, SPRING_LOGGER)
+			.bindSpring(CustomBean.class, SPRING_CUSTOM);
+	}
+
+	@Rest
+	public static class D_SpringFallbackForUserBean {
+		@RestInject static CustomBean customBeanCapture;
+		@RestInject public WritableBeanStore createBeanStore() { return springLikeBeanStoreWithCustomBean(); }
+	}
+
+	@Test
+	void d01_spring_fillsInForUserBean() {
+		MockRestClient.buildLax(D_SpringFallbackForUserBean.class);
+		assertSame(SPRING_CUSTOM, D_SpringFallbackForUserBean.customBeanCapture, "Spring fallback should provide CustomBean since framework has no default for it and there is no @RestInject method");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// 5. @RestInject for a non-framework bean type beats Spring fallback.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	private static final CustomBean RESTINJECT_CUSTOM = new CustomBean("from-restinject");
+
+	@Rest
+	public static class E_RestInjectBeatsSpringForUserBean {
+		@RestInject static CustomBean customBeanCapture;
+		@RestInject public WritableBeanStore createBeanStore() { return springLikeBeanStoreWithCustomBean(); }
+		@RestInject public CustomBean customBean() { return RESTINJECT_CUSTOM; }
 	}
 
 	@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)
+	void e01_restInject_beatsSpring_forUserBean() {
+		MockRestClient.buildLax(E_RestInjectBeatsSpringForUserBean.class);
+		assertSame(RESTINJECT_CUSTOM, E_RestInjectBeatsSpringForUserBean.customBeanCapture, "@RestInject should win over Spring fallback for user-defined bean types too");
 	}
 }
diff --git a/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/RestAnnotation_Test.java b/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/RestAnnotation_Test.java
index bf9feaa1c6..aba4a74a33 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/RestAnnotation_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/annotation/RestAnnotation_Test.java
@@ -53,7 +53,6 @@ class RestAnnotation_Test extends TestBase {
 		.allowedHeaderParams("b")
 		.allowedMethodHeaders("c")
 		.allowedMethodParams("d")
-		.beanStore(BasicBeanStore.class)
 		.callLogger(CallLogger.class)
 		.children(RestAnnotation_Test.class)
 		.clientVersionHeader("e")
@@ -104,7 +103,6 @@ class RestAnnotation_Test extends TestBase {
 		.allowedHeaderParams("b")
 		.allowedMethodHeaders("c")
 		.allowedMethodParams("d")
-		.beanStore(BasicBeanStore.class)
 		.callLogger(CallLogger.class)
 		.children(RestAnnotation_Test.class)
 		.clientVersionHeader("e")
@@ -149,8 +147,8 @@ class RestAnnotation_Test extends TestBase {
 
 	@Test void a01_basic() {
 		assertBean(a1,
-			"allowedHeaderParams,allowedMethodHeaders,allowedMethodParams,allowedParserOptions,allowedSerializerOptions,beanStore,callLogger,children,clientVersionHeader,config,consumes,converters,debug,debugEnablement,debugOn,defaultAccept,defaultCharset,defaultContentType,defaultRequestAttributes,defaultRequestHeaders,defaultResponseHeaders,description,disableContentParam,encoders,guards,maxInput,messages,noInherit,parsers,partParser,partSerializer,path,produces,renderResponseStackTraces,respon [...]
-			"b,c,d,[e1],[e2],BasicBeanStore,CallLogger,[RestAnnotation_Test],e,f,[g],[RestConverter],h,DebugEnablement,i,j,k,l,[m],[n],[o],[p],a,[Encoder],[RestGuard],q,r,[e3],[Parser],HttpPartParser,HttpPartSerializer,t,[u],v,[ResponseProcessor],[RestOpArg],w,x,[Serializer],y,StaticFiles,{{[],,,},[],{[],},{[],,},[],[],[],[],},BasicSwaggerProvider,[z],aa,bb,cc,dd");
+			"allowedHeaderParams,allowedMethodHeaders,allowedMethodParams,allowedParserOptions,allowedSerializerOptions,callLogger,children,clientVersionHeader,config,consumes,converters,debug,debugEnablement,debugOn,defaultAccept,defaultCharset,defaultContentType,defaultRequestAttributes,defaultRequestHeaders,defaultResponseHeaders,description,disableContentParam,encoders,guards,maxInput,messages,noInherit,parsers,partParser,partSerializer,path,produces,renderResponseStackTraces,responseProcesso [...]
+			"b,c,d,[e1],[e2],CallLogger,[RestAnnotation_Test],e,f,[g],[RestConverter],h,DebugEnablement,i,j,k,l,[m],[n],[o],[p],a,[Encoder],[RestGuard],q,r,[e3],[Parser],HttpPartParser,HttpPartSerializer,t,[u],v,[ResponseProcessor],[RestOpArg],w,x,[Serializer],y,StaticFiles,{{[],,,},[],{[],},{[],,},[],[],[],[],},BasicSwaggerProvider,[z],aa,bb,cc,dd");
 	}
 
 	@Test void a02_testEquivalency() {
@@ -181,7 +179,6 @@ class RestAnnotation_Test extends TestBase {
 		allowedHeaderParams="b",
 		allowedMethodHeaders="c",
 		allowedMethodParams="d",
-		beanStore=BasicBeanStore.class,
 		callLogger=CallLogger.class,
 		children=RestAnnotation_Test.class,
 		clientVersionHeader="e",
@@ -234,7 +231,6 @@ class RestAnnotation_Test extends TestBase {
 		allowedHeaderParams="b",
 		allowedMethodHeaders="c",
 		allowedMethodParams="d",
-		beanStore=BasicBeanStore.class,
 		callLogger=CallLogger.class,
 		children=RestAnnotation_Test.class,
 		clientVersionHeader="e",