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 b1996be2ff6dec21b39d7ce4dd4aa65d9468c195 Author: James Bognar <[email protected]> AuthorDate: Fri May 8 15:39:21 2026 -0400 refactor(inject): TODO-15 phase-3 batch 13 — migrate findRestOperationArgs to BeanInstantiator BeanInstantiator new opt-in flag: factoryAbstainOnNull(). Default v2 behavior: when a static factory method matches and is invoked but returns null, BeanInstantiator falls through to constructor lookup as if the factory weren't there. That works for "factory method as alternate construction path" callers, but breaks the legacy BeanCreator pattern of using a null factory return as a deliberate "this implementation does not handle the input — try the next strategy" abstain signal. Classic example: every RestOpArg subclass exposes a static create(ParameterInfo) that returns null when the parameter isn't annotated with the marker the subclass handles (AttributeArg returns null when @Attr is absent, etc.) — falling through to AttributeArg's protected constructor would silently produce a bogus arg. New flag preserves v2 default for everyone else and gives callers that need legacy semantics a one-line opt-in: .factoryAbstainOnNull() When set, run() returns null immediately after invoking a factory method whose return value is null, skipping constructor lookup. Migrated RestContext.findRestOperationArgs: BeanCreator.of(RestOpArg.class, bs).type(c).run() -> BeanInstantiator.of(RestOpArg.class, bs) .beanSubType(c) .factoryMethodNames("getInstance", "create") .factoryAbstainOnNull() .run() factoryMethodNames adds "create" to the v2 default ({"getInstance"}) so RestOpArg subclasses' static create(ParameterInfo) factories are recognized (legacy BeanCreator hard-coded "create"/"builder"/"getInstance" as factory methods; v2 reserves "create"/"builder" for builder-type discovery by default). Build clean; juneau-utest passes; jetty-ftest passes (verified that AttributeArg / BodyArg / HeaderArg / etc. all resolve correctly again). Co-authored-by: Cursor <[email protected]> --- .../juneau/commons/inject/BeanInstantiator.java | 64 +++++++++++++++++++--- .../java/org/apache/juneau/rest/RestContext.java | 6 +- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BeanInstantiator.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BeanInstantiator.java index c0edc4023e..907df8d040 100644 --- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BeanInstantiator.java +++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/inject/BeanInstantiator.java @@ -269,6 +269,7 @@ public class BeanInstantiator<T> { private Supplier<? extends T> fallbackSupplier = null; private final String name; private boolean cached = false; + private boolean factoryAbstainOnNull = false; private Memoizer<ClassInfo> builderType = memoizer(() -> findBuilderType()); private Memoizer<List<ClassInfo>> builderTypes = memoizer(() -> findBuilderTypes()); @@ -712,6 +713,49 @@ public class BeanInstantiator<T> { return this; } + /** + * Treat a {@code null} return value from a static factory method as a deliberate "abstain" signal. + * + * <p> + * By default, when a static factory method (one of {@link #factoryMethodNames(String...)}) matches and is + * invoked, but returns {@code null}, {@link #run()} falls through to constructor lookup as if the factory + * method weren't there. This works well when the factory method is just an alternate construction path. + * + * <p> + * Some factory-method patterns instead use a {@code null} return value as a deliberate "this implementation + * does not handle the input — try the next strategy" signal, expecting the caller to interpret the {@code null} + * as a definitive answer. The classic example is a {@code RestOpArg} subclass whose + * {@code static create(ParameterInfo)} returns {@code null} when the parameter isn't annotated with the + * marker the subclass handles, so the caller can move on to the next subclass in the chain. Falling through + * to the (always-present) constructor in that case would silently materialize an arg that doesn't apply. + * + * <p> + * When this flag is enabled, a {@code null} return from the matching factory method is propagated out of + * {@link #run()} unchanged — constructor lookup is skipped. This matches legacy {@code BeanCreator} + * semantics for static {@code create()} / {@code builder()} / {@code getInstance()} methods. + * + * <h5 class='section'>Example:</h5> + * <p class='bjava'> + * <jc>// AttributeArg.create(ParameterInfo) returns null if @Attr is not present.</jc> + * <jc>// Without factoryAbstainOnNull, BeanInstantiator would then invoke the AttributeArg(ParameterInfo)</jc> + * <jc>// constructor and return a bogus instance.</jc> + * RestOpArg <jv>arg</jv> = BeanInstantiator + * .<jsm>of</jsm>(RestOpArg.<jk>class</jk>, <jv>store</jv>) + * .beanSubType(AttributeArg.<jk>class</jk>) + * .factoryMethodNames(<js>"getInstance"</js>, <js>"create"</js>) + * .factoryAbstainOnNull() + * .run(); + * </p> + * + * @return This object. + */ + public BeanInstantiator<T> factoryAbstainOnNull() { + try (var writeLock = lock.write()) { + factoryAbstainOnNull = true; + } + return this; + } + /** * Creates the bean. * @@ -1284,18 +1328,24 @@ public class BeanInstantiator<T> { log("Attempting Bean.factoryMethod()"); // If builder was detected but has no build method, pass it as extra bean for factory methods Object[] factoryMethodExtraBeans = builder2 != null ? new Object[]{builder2} : new Object[0]; - bean = beanSubType.getPublicMethods().stream() + var factoryMethod = beanSubType.getPublicMethods().stream() .filter(x -> x.isAll(STATIC, NOT_DEPRECATED, NOT_SYNTHETIC, NOT_BRIDGE)) .filter(x -> factoryMethodNames.contains(x.getNameSimple())) .filter(x -> x.hasReturnType(beanSubType)) .filter(x -> x.canResolveAllParameters(store2, factoryMethodExtraBeans)) .sorted(methodComparator) - .findFirst() - .map(x -> { - log("Found factory method: %s", x.getNameFull()); - return (T)beanType.cast(x.inject(store2, null, factoryMethodExtraBeans)); - }) - .orElse(null); + .findFirst(); + if (factoryMethod.isPresent()) { + log("Found factory method: %s", factoryMethod.get().getNameFull()); + bean = (T) beanType.cast(factoryMethod.get().inject(store2, null, factoryMethodExtraBeans)); + // When factoryAbstainOnNull is set, treat a null result from the factory method as a + // deliberate "abstain" signal and return null without falling through to constructor + // lookup. Used by callers like RestOpArg subclasses whose create(ParameterInfo) + // returns null when the parameter isn't annotated with the marker the subclass + // handles (e.g. AttributeArg.create() returns null when @Attr isn't present). + if (bean == null && factoryAbstainOnNull) + return null; + } // Look for Bean(). // Skip constructor invocation when beanSubType is abstract or an interface — Class.newInstance() would 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 d9731ef540..4a4ccd05bc 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 @@ -2697,7 +2697,11 @@ public class RestContext extends Context { bs.addBean(ParameterInfo.class, pi); for (var c : roa) { try { - ra[i] = BeanCreator.of(RestOpArg.class, bs).type(c).run(); + ra[i] = BeanInstantiator.of(RestOpArg.class, bs) + .beanSubType(c) + .factoryMethodNames("getInstance", "create") + .factoryAbstainOnNull() + .run(); if (nn(ra[i])) break; } catch (ExecutableException e) {
