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 <[email protected]>
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 <[email protected]>
---
 .../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",


Reply via email to