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<?,?> <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;
