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