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 7dca9ddfcdd71c0419b58e8e70ad6db8dbb597ad
Author: James Bognar <[email protected]>
AuthorDate: Fri May 8 15:48:05 2026 -0400

    refactor(inject): TODO-15 phase-3 batch 14 — migrate Map/Collection 
container creators to BeanInstantiator
    
    BeanInstantiator new opt-in flag: preferZeroArgConstructor().
    
    Default v2 constructor selection sorts public ctors by parameter count
    descending, picking the longest resolvable ctor (full-injection-friendly).
    That breaks for parameterized container types whose copy ctors take a raw
    Map / Collection parameter.  Concrete failure: TreeMap exposes
    TreeMap(Map<? extends K, ? extends V>) — ParameterInfo.canResolve() 
optimistically
    returns true because pt.isInjectCollectionType() matches Map.class, but
    ParameterInfo.resolveValue() only auto-resolves Map<String, T> (it requires
    typeArgs[0] == String.class), so injection throws "Could not resolve value 
for
    parameter Map<? extends K, ? extends V>" at invocation time.  Eight unit
    tests across xml/json/json5/uon/urlencoding/html/BeanMap exercised this.
    
    The asymmetry between canResolve (overly permissive) and resolveValue 
(strict)
    is a v2 reflection-layer bug worth fixing on its own, but the immediate need
    here is a way for container-creating callers to opt out of constructor
    injection entirely — they want "fresh empty container of this concrete 
type",
    not "inject all my dependencies".
    
    New flag preserves v2 default for everyone else and gives container callers 
a
    one-line opt-in:
        .preferZeroArgConstructor()
    When set, if beanSubType declares a public no-arg constructor, that ctor is
    used unconditionally; other ctors are ignored.  Matches legacy BeanCreator
    behavior for the same use case (legacy hasAllParams returned false for raw
    Map / Collection params since the legacy bean store didn't auto-resolve 
those
    types, so multi-arg container ctors were filtered out and the no-arg ctor 
won
    by default).
    
    Migrated three call sites:
    - BeanRegistry.addClass() — Collection / Map subtype creation (lines 147 / 
155)
    - BeanPropertyMeta.set() — propMap / propList property containers (lines 
1106 / 1165)
    
    cp.* import dropped from BeanPropertyMeta (no remaining legacy refs).
    
    Build clean; full juneau-utest passes (the 8 previously-failing
    *_trimEmptyMaps + BeanMap_Test#a03/a04_collection*Properties tests are 
green).
    
    Co-authored-by: Cursor <[email protected]>
---
 .../juneau/commons/inject/BeanInstantiator.java    | 85 +++++++++++++++++++---
 .../java/org/apache/juneau/BeanPropertyMeta.java   |  5 +-
 .../main/java/org/apache/juneau/BeanRegistry.java  |  6 +-
 3 files changed, 79 insertions(+), 17 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 907df8d040..4860665395 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
@@ -270,6 +270,7 @@ public class BeanInstantiator<T> {
        private final String name;
        private boolean cached = false;
        private boolean factoryAbstainOnNull = false;
+       private boolean preferZeroArgConstructor = false;
 
        private Memoizer<ClassInfo> builderType = memoizer(() -> 
findBuilderType());
        private Memoizer<List<ClassInfo>> builderTypes = memoizer(() -> 
findBuilderTypes());
@@ -756,6 +757,52 @@ public class BeanInstantiator<T> {
                return this;
        }
 
+       /**
+        * Prefer a zero-argument constructor when one exists, instead of 
injecting via the longest constructor.
+        *
+        * <p>
+        * By default, {@code BeanInstantiator} sorts public constructors by 
parameter count <i>descending</i>: the
+        * constructor with the most resolvable parameters wins, encouraging 
full constructor injection. This is the
+        * right default for most beans, but it doesn't fit every use case.
+        *
+        * <p>
+        * The classic mismatch is parameterized container types like {@link 
java.util.TreeMap}, which expose:
+        * <ul>
+        *      <li>{@code TreeMap()}
+        *      <li>{@code TreeMap(Comparator)}
+        *      <li>{@code TreeMap(Map<? extends K, ? extends V>)}
+        *      <li>{@code TreeMap(SortedMap)}
+        * </ul>
+        * The default selection picks the {@code Map} copy-constructor — but 
that {@code Map<? extends K, ? extends V>}
+        * parameter doesn't satisfy v2 inject-collection auto-resolution 
(which only handles {@code Map<String, T>}),
+        * so injection fails at runtime even though {@link 
ParameterInfo#canResolve(BeanStore, Object...)} optimistically
+        * said yes. Callers that just want "give me a fresh empty container" 
should use this flag.
+        *
+        * <p>
+        * When this flag is enabled, if {@code beanSubType} declares a public 
no-arg constructor, that constructor is
+        * used unconditionally and other constructors are ignored. This 
matches legacy {@code BeanCreator} behavior for
+        * the same use case (legacy filtered out the multi-arg ctors via 
{@code hasAllParams} returning false, since the
+        * legacy bean store didn't auto-resolve raw {@code Map}/{@code 
Collection} parameter types).
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <jc>// Instantiating an arbitrary Map subclass — prefer the 
no-arg ctor over copy-ctors.</jc>
+        *      Map&lt;?,?&gt; <jv>m</jv> = BeanInstantiator
+        *              .<jsm>of</jsm>(Map.<jk>class</jk>)
+        *              .beanSubType(treeMapClass)
+        *              .preferZeroArgConstructor()
+        *              .run();
+        * </p>
+        *
+        * @return This object.
+        */
+       public BeanInstantiator<T> preferZeroArgConstructor() {
+               try (var writeLock = lock.write()) {
+                       preferZeroArgConstructor = true;
+               }
+               return this;
+       }
+
        /**
         * Creates the bean.
         *
@@ -1353,19 +1400,35 @@ public class BeanInstantiator<T> {
                // fallthrough below (which honors the registered fallback 
supplier when present).
                if (bean == null && ! beanSubType.isInterface() && ! 
beanSubType.isAbstract()) {
                        log("Attempting Bean() constructor");
+                       // If preferZeroArgConstructor is set and a no-arg 
constructor exists, short-circuit to it.
+                       // This skips the longest-resolvable-constructor 
heuristic for callers that just want a fresh
+                       // empty instance — see preferZeroArgConstructor() 
Javadoc for the parameterized-container rationale.
+                       if (preferZeroArgConstructor) {
+                               var zeroArgCtor = 
beanSubType.getPublicConstructors().stream()
+                                       .filter(x -> x.isAll(NOT_DEPRECATED))
+                                       .filter(x -> 
x.isDeclaringClass(beanSubType))
+                                       .filter(x -> x.getParameterCount() == 0)
+                                       .findFirst();
+                               if (zeroArgCtor.isPresent()) {
+                                       log("Using zero-arg constructor 
(preferZeroArgConstructor): %s", zeroArgCtor.get().getNameFull());
+                                       bean = (T) 
beanType.cast(zeroArgCtor.get().inject(store2, enclosingInstance));
+                               }
+                       }
                        // If builder was detected but has no build method, 
pass it as extra bean for constructors
                        Object[] constructorExtraBeans = builder2 != null ? new 
Object[]{builder2} : new Object[0];
-                       bean = beanSubType.getPublicConstructors().stream()
-                               .filter(x -> x.isAll(NOT_DEPRECATED))
-                               .filter(x -> x.isDeclaringClass(beanSubType))
-                               .filter(x -> x.canResolveAllParameters(store2, 
enclosingInstance, constructorExtraBeans))
-                               .sorted(constructorComparator)
-                               .findFirst()
-                               .map(x -> {
-                                       log("Found constructor: %s", 
x.getNameFull());
-                                       return 
(T)beanType.cast(x.inject(store2, enclosingInstance, constructorExtraBeans));
-                               })
-                               .orElse(null);
+                       if (bean == null) {
+                               bean = 
beanSubType.getPublicConstructors().stream()
+                                       .filter(x -> x.isAll(NOT_DEPRECATED))
+                                       .filter(x -> 
x.isDeclaringClass(beanSubType))
+                                       .filter(x -> 
x.canResolveAllParameters(store2, enclosingInstance, constructorExtraBeans))
+                                       .sorted(constructorComparator)
+                                       .findFirst()
+                                       .map(x -> {
+                                               log("Found constructor: %s", 
x.getNameFull());
+                                               return 
(T)beanType.cast(x.inject(store2, enclosingInstance, constructorExtraBeans));
+                                       })
+                                       .orElse(null);
+                       }
                }
 
                if (bean != null) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
index f57bc58e0a..826eb376e2 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
@@ -39,7 +39,6 @@ import org.apache.juneau.commons.lang.*;
 import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.commons.reflect.ReflectionUtils;
 import org.apache.juneau.commons.inject.*;
-import org.apache.juneau.cp.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.serializer.*;
@@ -1103,7 +1102,7 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                                        }
                                } else {
                                        if (propMap == null) {
-                                               propMap = 
BeanCreator.of(Map.class).type(rawTypeMeta).run();
+                                               propMap = 
BeanInstantiator.of(Map.class).beanSubType(rawTypeMeta).preferZeroArgConstructor().run();
                                        } else {
                                                propMap.clear();
                                        }
@@ -1162,7 +1161,7 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                                        propList.clear();
                                } else {
                                        if (propList == null) {
-                                               propList = 
BeanCreator.of(Collection.class).type(rawTypeMeta).run();
+                                               propList = 
BeanInstantiator.of(Collection.class).beanSubType(rawTypeMeta).preferZeroArgConstructor().run();
                                                invokeSetter(bean, pName, 
propList);
                                        } else {
                                                propList.clear();
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanRegistry.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanRegistry.java
index a562e2aebe..655a8569bc 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanRegistry.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanRegistry.java
@@ -28,9 +28,9 @@ import java.util.concurrent.*;
 
 import org.apache.juneau.annotation.*;
 import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.commons.utils.*;
-import org.apache.juneau.cp.*;
 
 /**
  * A lookup table for resolving bean types by name.
@@ -144,7 +144,7 @@ public class BeanRegistry {
                try {
                        if (nn(ci) && nn(ci.inner())) {
                                if (ci.isAssignableTo(Collection.class)) {
-                                       Collection<?> cc = 
BeanCreator.of(Collection.class).type(ci).run();
+                                       Collection<?> cc = 
BeanInstantiator.of(Collection.class).beanSubType(ci).preferZeroArgConstructor().run();
                                        cc.forEach(x -> {
                                                if (x instanceof Class<?> x2)
                                                        addClass(info(x2));
@@ -152,7 +152,7 @@ public class BeanRegistry {
                                                        throw bex("Collection 
class ''{0}'' passed to BeanRegistry does not contain Class objects.", 
ci.getName());
                                        });
                                } else if (ci.isAssignableTo(Map.class)) {
-                                       Map<?,?> m = 
BeanCreator.of(Map.class).type(ci).run();
+                                       Map<?,?> m = 
BeanInstantiator.of(Map.class).beanSubType(ci).preferZeroArgConstructor().run();
                                        m.forEach((k, v) -> {
                                                var typeName = s(k);
                                                ClassMeta<?> val = null;

Reply via email to