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 — 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 — 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",