This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/main by this push:
     new 313a18800d Pull out common Injector code and add hierarchical lookup
313a18800d is described below

commit 313a18800d9c3f716b48e516736ced1f7e9b3d0b
Author: Matt Sicker <[email protected]>
AuthorDate: Sat Mar 18 19:33:18 2023 -0500

    Pull out common Injector code and add hierarchical lookup
    
    This is in preparation for breaking up the Injector API and DefaultInjector 
into more digestible chunks.
    
    - Extract TypeConverterFactory for simpler registration of type converters
    - Create DependencyChain interface for tracking the set of dependency keys 
during instance creation
    - Create HierarchicalMap for supporting hierarchical lookups of bindings
    - Make Binding and InjectionPoint public
    - Extract private inner classes into package-private classes
    
    Signed-off-by: Matt Sicker <[email protected]>
---
 .../logging/log4j/plugins/di/InjectorTest.java     |   7 +-
 .../plugins/convert/TypeConverterFactory.java      | 143 +++++++++
 .../apache/logging/log4j/plugins/di/Binding.java   |  49 ++-
 .../logging/log4j/plugins/di/BindingMap.java       |  74 -----
 .../logging/log4j/plugins/di/DefaultInjector.java  | 336 +++++----------------
 .../plugins/di/{Binding.java => DefaultScope.java} |  25 +-
 .../logging/log4j/plugins/di/DependencyChain.java  |  53 ++++
 .../logging/log4j/plugins/di/DependencyChains.java | 132 ++++++++
 .../logging/log4j/plugins/di/InjectionPoint.java   |  11 +-
 .../di/{Binding.java => SingletonScope.java}       |  28 +-
 .../log4j/plugins/internal/util/BeanUtils.java     |  99 ++++++
 .../internal/util/HierarchicalCollections.java     | 195 ++++++++++++
 .../util/{BeanUtils.java => HierarchicalMap.java}  |  27 +-
 pom.xml                                            |   2 +-
 14 files changed, 772 insertions(+), 409 deletions(-)

diff --git 
a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/di/InjectorTest.java
 
b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/di/InjectorTest.java
index 5166d69346..f3903acb94 100644
--- 
a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/di/InjectorTest.java
+++ 
b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/di/InjectorTest.java
@@ -289,7 +289,7 @@ class InjectorTest {
     void unknownInstanceError() {
         final Key<UnknownInstance> key = new Key<>() {};
         assertThatThrownBy(() -> DI.createInjector().getInstance(key))
-                .hasMessage("No @Inject constructors or no-arg constructor 
found for " + key);
+                .hasMessage("No @Inject constructor or default constructor 
found for " + key);
     }
 
     @Test
@@ -483,7 +483,10 @@ class InjectorTest {
         final Injector injector = DI.createInjector(new UppercaseBundle());
         final Function<String, String> function = 
injector.getInstance(Keys.SUBSTITUTOR_KEY);
         assertThat(function.apply("foo")).isEqualTo("FOO");
-        assertThatThrownBy(() -> 
injector.getInstance(Function.class)).hasMessageContaining("No @Inject 
constructors");
+        final Key<Function<String, String>> unqualifiedFunctionKey = new 
Key<>() {};
+        assertThatThrownBy(() -> injector.getInstance(unqualifiedFunctionKey))
+                .isInstanceOf(NotInjectableException.class)
+                .hasMessage("No @Inject constructor or default constructor 
found for " + unqualifiedFunctionKey);
     }
 
     @Namespace("Test")
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
new file mode 100644
index 0000000000..8332258268
--- /dev/null
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.plugins.convert;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+import java.util.UnknownFormatConversionException;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.plugins.Inject;
+import org.apache.logging.log4j.plugins.Singleton;
+import org.apache.logging.log4j.plugins.util.TypeUtil;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.EnglishEnums;
+
+@Singleton
+public class TypeConverterFactory {
+    private static final Logger LOGGER = StatusLogger.getLogger();
+    private final Map<Type, TypeConverter<?>> typeConverters = new 
ConcurrentHashMap<>();
+
+    @Inject
+    public TypeConverterFactory(@TypeConverters List<TypeConverter<?>> 
typeConverters) {
+        typeConverters.forEach(converter -> 
registerTypeConverter(getTypeConverterSupportedType(converter.getClass()), 
converter));
+        registerTypeConverter(Boolean.class, Boolean::valueOf);
+        registerTypeAlias(Boolean.class, Boolean.TYPE);
+        registerTypeConverter(Byte.class, Byte::valueOf);
+        registerTypeAlias(Byte.class, Byte.TYPE);
+        registerTypeConverter(Character.class, s -> {
+            if (s.length() != 1) {
+                throw new IllegalArgumentException("Character string must be 
of length 1: " + s);
+            }
+            return s.toCharArray()[0];
+        });
+        registerTypeAlias(Character.class, Character.TYPE);
+        registerTypeConverter(Double.class, Double::valueOf);
+        registerTypeAlias(Double.class, Double.TYPE);
+        registerTypeConverter(Float.class, Float::valueOf);
+        registerTypeAlias(Float.class, Float.TYPE);
+        registerTypeConverter(Integer.class, Integer::valueOf);
+        registerTypeAlias(Integer.class, Integer.TYPE);
+        registerTypeConverter(Long.class, Long::valueOf);
+        registerTypeAlias(Long.class, Long.TYPE);
+        registerTypeConverter(Short.class, Short::valueOf);
+        registerTypeAlias(Short.class, Short.TYPE);
+        registerTypeConverter(String.class, s -> s);
+    }
+
+    public TypeConverter<?> getTypeConverter(final Type type) {
+        final TypeConverter<?> primary = typeConverters.get(type);
+        // cached type converters
+        if (primary != null) {
+            return primary;
+        }
+        // dynamic enum support
+        if (type instanceof Class<?>) {
+            final Class<?> clazz = (Class<?>) type;
+            if (clazz.isEnum()) {
+                return registerTypeConverter(type, s -> 
EnglishEnums.valueOf(clazz.asSubclass(Enum.class), s));
+            }
+        }
+        // look for compatible converters
+        for (final Map.Entry<Type, TypeConverter<?>> entry : 
typeConverters.entrySet()) {
+            final Type key = entry.getKey();
+            if (TypeUtil.isAssignable(type, key)) {
+                LOGGER.debug("Found compatible TypeConverter<{}> for type 
[{}].", key, type);
+                final TypeConverter<?> value = entry.getValue();
+                return registerTypeConverter(type, value);
+            }
+        }
+        throw new UnknownFormatConversionException(type.toString());
+    }
+
+    private void registerTypeAlias(final Type knownType, final Type aliasType) 
{
+        final TypeConverter<?> converter = typeConverters.get(knownType);
+        if (converter != null) {
+            typeConverters.put(aliasType, converter);
+        } else {
+            LOGGER.error("Cannot locate type converter for {}", knownType);
+        }
+    }
+
+    private TypeConverter<?> registerTypeConverter(final Type type, final 
TypeConverter<?> converter) {
+        final TypeConverter<?> conflictingConverter = typeConverters.get(type);
+        if (conflictingConverter != null) {
+            final boolean overridable;
+            if (converter instanceof Comparable) {
+                @SuppressWarnings("unchecked") final 
Comparable<TypeConverter<?>> comparableConverter =
+                        (Comparable<TypeConverter<?>>) converter;
+                overridable = 
comparableConverter.compareTo(conflictingConverter) < 0;
+            } else if (conflictingConverter instanceof Comparable) {
+                @SuppressWarnings("unchecked") final 
Comparable<TypeConverter<?>> comparableConflictingConverter =
+                        (Comparable<TypeConverter<?>>) conflictingConverter;
+                overridable = 
comparableConflictingConverter.compareTo(converter) > 0;
+            } else {
+                overridable = false;
+            }
+            if (overridable) {
+                LOGGER.debug(
+                        "Replacing TypeConverter [{}] for type [{}] with [{}] 
after comparison.",
+                        conflictingConverter, type, converter);
+                typeConverters.put(type, converter);
+                return converter;
+            } else {
+                LOGGER.warn(
+                        "Ignoring TypeConverter [{}] for type [{}] that 
conflicts with [{}], since they are not comparable.",
+                        converter, type, conflictingConverter);
+                return conflictingConverter;
+            }
+        } else {
+            typeConverters.put(type, converter);
+            return converter;
+        }
+    }
+
+    private static Type getTypeConverterSupportedType(final Class<?> clazz) {
+        for (final Type type : clazz.getGenericInterfaces()) {
+            if (type instanceof ParameterizedType) {
+                final ParameterizedType parameterizedType = 
(ParameterizedType) type;
+                if (parameterizedType.getRawType() == TypeConverter.class) {
+                    return parameterizedType.getActualTypeArguments()[0];
+                }
+            }
+        }
+        return Void.TYPE;
+    }
+}
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java
index 88e707fd9d..080d3f5cf6 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java
@@ -14,29 +14,60 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-
 package org.apache.logging.log4j.plugins.di;
 
 import java.util.function.Supplier;
 
-class Binding<T> {
+import org.apache.logging.log4j.util.Lazy;
+
+/**
+ * Bindings combine a {@link Key} with a factory {@link Supplier} to get 
instances of the key's type.
+ *
+ * @param <T> type of key to bind instance factory
+ */
+public final class Binding<T> implements Supplier<T> {
     private final Key<T> key;
-    private final Supplier<T> supplier;
+    private final Supplier<? extends T> factory;
 
-    private Binding(final Key<T> key, final Supplier<T> supplier) {
+    private Binding(final Key<T> key, final Supplier<? extends T> factory) {
         this.key = key;
-        this.supplier = supplier;
+        this.factory = factory;
     }
 
     public Key<T> getKey() {
         return key;
     }
 
-    public Supplier<T> getSupplier() {
-        return supplier;
+    @Override
+    public T get() {
+        return factory.get();
+    }
+
+    public static <T> DSL<T> from(final Key<T> key) {
+        return new DSL<>(key);
     }
 
-    public static <T> Binding<T> bind(final Key<T> key, final Supplier<T> 
supplier) {
-        return new Binding<>(key, supplier);
+    public static <T> DSL<T> from(final Class<T> type) {
+        return new DSL<>(Key.forClass(type));
+    }
+
+    public static class DSL<T> {
+        private final Key<T> key;
+
+        private DSL(final Key<T> key) {
+            this.key = key;
+        }
+
+        public Binding<T> to(final Supplier<? extends T> factory) {
+            return new Binding<>(key, factory);
+        }
+
+        public Binding<T> toSingleton(final Supplier<? extends T> factory) {
+            return new Binding<>(key, Lazy.lazy(factory));
+        }
+
+        public Binding<T> toInstance(final T instance) {
+            return new Binding<>(key, () -> instance);
+        }
     }
 }
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/BindingMap.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/BindingMap.java
deleted file mode 100644
index 21462eaaef..0000000000
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/BindingMap.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.plugins.di;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Supplier;
-
-import org.apache.logging.log4j.util.Cast;
-
-class BindingMap {
-    private final Map<Key<?>, Binding<?>> bindings;
-
-    BindingMap() {
-        bindings = new ConcurrentHashMap<>();
-    }
-
-    BindingMap(final BindingMap bindings) {
-        this.bindings = new ConcurrentHashMap<>(bindings.bindings);
-    }
-
-    public <T> Binding<T> get(final Key<T> key) {
-        return Cast.cast(bindings.get(key));
-    }
-
-    public <T> Binding<T> get(final Key<T> key, final Collection<String> 
aliases) {
-        var binding = bindings.get(key);
-        if (binding == null) {
-            for (final String alias : aliases) {
-                binding = bindings.get(key.withName(alias));
-                if (binding != null) {
-                    break;
-                }
-            }
-        }
-        return Cast.cast(binding);
-    }
-
-    public <T> void put(final Key<T> key, final Supplier<T> factory) {
-        bindings.put(key, Binding.bind(key, factory));
-    }
-
-    /**
-     * @see org.apache.logging.log4j.plugins.util.OrderedComparator
-     */
-    public <T> Supplier<T> merge(final Key<T> key, final Supplier<T> factory) {
-        final Binding<?> newBinding = bindings.merge(key, Binding.bind(key, 
factory), (oldBinding, binding) ->
-                oldBinding.getKey().getOrder() <= binding.getKey().getOrder() 
? oldBinding : binding);
-        return Cast.cast(newBinding.getSupplier());
-    }
-
-    public <T> void bindIfAbsent(final Key<T> key, final Supplier<T> factory) {
-        bindings.putIfAbsent(key, Binding.bind(key, factory));
-    }
-
-    public void remove(final Key<?> key) {
-        bindings.remove(key);
-    }
-}
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultInjector.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultInjector.java
index 0cde8a2e79..0c0f6264c1 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultInjector.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultInjector.java
@@ -23,7 +23,6 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.Executable;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
 import java.lang.reflect.Parameter;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
@@ -37,23 +36,21 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.UnknownFormatConversionException;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.plugins.FactoryType;
-import org.apache.logging.log4j.plugins.Inject;
 import org.apache.logging.log4j.plugins.Node;
 import org.apache.logging.log4j.plugins.PluginException;
-import org.apache.logging.log4j.plugins.QualifierType;
 import org.apache.logging.log4j.plugins.ScopeType;
 import org.apache.logging.log4j.plugins.Singleton;
 import org.apache.logging.log4j.plugins.condition.Conditional;
 import org.apache.logging.log4j.plugins.convert.TypeConverter;
-import org.apache.logging.log4j.plugins.convert.TypeConverters;
+import org.apache.logging.log4j.plugins.convert.TypeConverterFactory;
+import org.apache.logging.log4j.plugins.internal.util.BeanUtils;
+import org.apache.logging.log4j.plugins.internal.util.HierarchicalMap;
 import org.apache.logging.log4j.plugins.model.PluginNamespace;
 import org.apache.logging.log4j.plugins.model.PluginRegistry;
 import org.apache.logging.log4j.plugins.model.PluginType;
@@ -66,7 +63,6 @@ import 
org.apache.logging.log4j.plugins.validation.ConstraintValidator;
 import org.apache.logging.log4j.plugins.visit.NodeVisitor;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Cast;
-import org.apache.logging.log4j.util.EnglishEnums;
 import org.apache.logging.log4j.util.Lazy;
 import org.apache.logging.log4j.util.ServiceRegistry;
 import org.apache.logging.log4j.util.StringBuilders;
@@ -76,21 +72,20 @@ class DefaultInjector implements Injector {
     private static final Set<Class<?>> COLLECTION_INJECTION_TYPES = Set.of(
             Collection.class, Iterable.class, List.class, Map.class, 
Optional.class, Set.class, Stream.class);
 
-    private final BindingMap bindingMap;
-    private final Map<Class<? extends Annotation>, Scope> scopes = new 
ConcurrentHashMap<>();
-    private final Map<Type, TypeConverter<?>> typeConverters = new 
ConcurrentHashMap<>();
+    private final HierarchicalMap<Key<?>, Binding<?>> bindings;
+    private final HierarchicalMap<Class<? extends Annotation>, Scope> scopes;
     private ReflectionAccessor accessor = object -> object.setAccessible(true);
 
     DefaultInjector() {
-        bindingMap = new BindingMap();
-        bindingMap.put(KEY, () -> this);
+        bindings = HierarchicalMap.newRootMap();
+        bindings.put(KEY, Binding.from(KEY).toInstance(this));
+        scopes = HierarchicalMap.newRootMap();
         scopes.put(Singleton.class, new SingletonScope());
     }
 
     DefaultInjector(final DefaultInjector original) {
-        bindingMap = new BindingMap(original.bindingMap);
-        scopes.putAll(original.scopes);
-        typeConverters.putAll(original.typeConverters);
+        bindings = original.bindings.newChildMap();
+        scopes = original.scopes.newChildMap();
         accessor = original.accessor;
     }
 
@@ -115,46 +110,17 @@ class DefaultInjector implements Injector {
 
     @Override
     public <T> Supplier<T> getFactory(final Key<T> key) {
-        return getFactory(key, Set.of(), null, Set.of());
+        return getFactory(key, Set.of(), null, DependencyChain.empty());
     }
 
     @Override
     public TypeConverter<?> getTypeConverter(final Type type) {
-        if (typeConverters.isEmpty()) {
-            synchronized (typeConverters) {
-                if (typeConverters.isEmpty()) {
-                    LOGGER.trace("Initializing type converters");
-                    initializeTypeConverters();
-                }
-            }
-        }
-        final TypeConverter<?> primary = typeConverters.get(type);
-        // cached type converters
-        if (primary != null) {
-            return primary;
-        }
-        // dynamic enum support
-        if (type instanceof Class<?>) {
-            final Class<?> clazz = (Class<?>) type;
-            if (clazz.isEnum()) {
-                return registerTypeConverter(type, s -> 
EnglishEnums.valueOf(clazz.asSubclass(Enum.class), s));
-            }
-        }
-        // look for compatible converters
-        for (final Map.Entry<Type, TypeConverter<?>> entry : 
typeConverters.entrySet()) {
-            final Type key = entry.getKey();
-            if (TypeUtil.isAssignable(type, key)) {
-                LOGGER.debug("Found compatible TypeConverter<{}> for type 
[{}].", key, type);
-                final TypeConverter<?> value = entry.getValue();
-                return registerTypeConverter(type, value);
-            }
-        }
-        throw new UnknownFormatConversionException(type.toString());
+        return getInstance(TypeConverterFactory.class).getTypeConverter(type);
     }
 
     @Override
     public void injectMembers(final Object instance) {
-        injectMembers(Key.forClass(instance.getClass()), null, instance, 
Set.of(), null);
+        injectMembers(Key.forClass(instance.getClass()), null, instance, 
DependencyChain.empty(), null);
     }
 
     @Override
@@ -198,24 +164,24 @@ class DefaultInjector implements Injector {
 
     @Override
     public <T> Injector registerBinding(final Key<T> key, final Supplier<? 
extends T> factory) {
-        bindingMap.put(key, factory::get);
+        bindings.put(key, Binding.from(key).to(factory));
         return this;
     }
 
     @Override
     public <T> Injector registerBindingIfAbsent(final Key<T> key, final 
Supplier<? extends T> factory) {
-        bindingMap.bindIfAbsent(key, factory::get);
+        bindings.putIfAbsent(key, Binding.from(key).to(factory));
         return this;
     }
 
     @Override
     public void removeBinding(final Key<?> key) {
-        bindingMap.remove(key);
+        bindings.remove(key);
     }
 
     @Override
     public boolean hasBinding(final Key<?> key) {
-        return bindingMap.get(key) != null;
+        return bindings.containsKey(key);
     }
 
     @Override
@@ -224,7 +190,7 @@ class DefaultInjector implements Injector {
     }
 
     private <T> Supplier<T> getFactory(
-            final InjectionPoint<T> point, final Node node, final Set<Key<?>> 
chain, final StringBuilder debugLog) {
+            final InjectionPoint<T> point, final Node node, final 
DependencyChain chain, final StringBuilder debugLog) {
         final AnnotatedElement element = point.getElement();
         final Key<? extends NodeVisitor> visitorKey = 
NodeVisitor.keyFor(element);
         final NodeVisitor visitor = visitorKey != null ? 
getInstance(visitorKey) : null;
@@ -238,14 +204,23 @@ class DefaultInjector implements Injector {
         final Key<T> key = point.getKey();
         final Collection<String> aliases = point.getAliases();
         final Key<T> suppliedType = key.getSuppliedType();
-        return suppliedType != null ? getFactory(suppliedType, aliases, node, 
Set.of()) : getFactory(key, aliases, node, chain);
+        return suppliedType != null
+                ? getFactory(suppliedType, aliases, node, 
DependencyChain.empty())
+                : getFactory(key, aliases, node, chain);
     }
 
     private <T> Supplier<T> getFactory(
-            final Key<T> key, final Collection<String> aliases, final Node 
node, final Set<Key<?>> chain) {
-        final Binding<T> existing = bindingMap.get(key, aliases);
+            final Key<T> key, final Collection<String> aliases, final Node 
node, final DependencyChain chain) {
+        final Binding<?> existing = bindings.get(key);
         if (existing != null) {
-            return existing.getSupplier();
+            return Cast.cast(existing);
+        }
+        for (final String alias : aliases) {
+            final Key<T> keyAlias = key.withName(alias);
+            final Binding<?> existingAlias = bindings.get(keyAlias);
+            if (existingAlias != null) {
+                return Cast.cast(existingAlias);
+            }
         }
 
         final Class<T> rawType = key.getRawType();
@@ -255,7 +230,7 @@ class DefaultInjector implements Injector {
         if (rawType == PluginNamespace.class && !key.getNamespace().isEmpty()) 
{
             final Key<PluginNamespace> pluginNamespaceKey = Cast.cast(key);
             final Supplier<PluginNamespace> pluginNamespaceFactory = 
createPluginNamespaceFactory(pluginNamespaceKey);
-            return Cast.cast(bindingMap.merge(pluginNamespaceKey, 
pluginNamespaceFactory));
+            return Cast.cast(merge(pluginNamespaceKey, 
pluginNamespaceFactory));
         }
 
         // @Namespace Collection<T>/Map<String, T>/Stream<T>/etc. injection
@@ -264,23 +239,23 @@ class DefaultInjector implements Injector {
                 final Key<Stream<T>> streamKey = Cast.cast(key);
                 final Supplier<Stream<T>> streamFactory =
                         () -> 
streamPluginInstancesFromNamespace(key.getParameterizedTypeArgument(0));
-                return Cast.cast(bindingMap.merge(streamKey, streamFactory));
+                return Cast.cast(merge(streamKey, streamFactory));
             } else if (Set.class.isAssignableFrom(rawType)) {
                 final Key<Set<T>> setKey = Cast.cast(key);
                 final Supplier<Set<T>> setFactory = () -> 
getPluginSet(key.getParameterizedTypeArgument(0));
-                return Cast.cast(bindingMap.merge(setKey, setFactory));
+                return Cast.cast(merge(setKey, setFactory));
             } else if (Map.class.isAssignableFrom(rawType)) {
                 final Key<Map<String, T>> mapKey = Cast.cast(key);
                 final Supplier<Map<String, T>> mapFactory = () -> 
getPluginMap(key.getParameterizedTypeArgument(1));
-                return Cast.cast(bindingMap.merge(mapKey, mapFactory));
+                return Cast.cast(merge(mapKey, mapFactory));
             } else if (Iterable.class.isAssignableFrom(rawType)) {
                 final Key<Iterable<T>> iterableKey = Cast.cast(key);
                 final Supplier<Iterable<T>> iterableFactory = () -> 
getPluginList(key.getParameterizedTypeArgument(0));
-                return Cast.cast(bindingMap.merge(iterableKey, 
iterableFactory));
+                return Cast.cast(merge(iterableKey, iterableFactory));
             } else if (Optional.class.isAssignableFrom(rawType)) {
                 final Key<Optional<T>> optionalKey = Cast.cast(key);
                 final Supplier<Optional<T>> optionalFactory = () -> 
getOptionalPlugin(key.getParameterizedTypeArgument(0));
-                return Cast.cast(bindingMap.merge(optionalKey, 
optionalFactory));
+                return Cast.cast(merge(optionalKey, optionalFactory));
             } else {
                 throw new InjectException("Cannot inject plugins into " + key);
             }
@@ -291,17 +266,23 @@ class DefaultInjector implements Injector {
             final Key<Optional<T>> optionalKey = Cast.cast(key);
             final Supplier<Optional<T>> optionalFactory = () ->
                     getOptionalInstance(key.getParameterizedTypeArgument(0), 
aliases, node, chain);
-            return Cast.cast(bindingMap.merge(optionalKey, optionalFactory));
+            return Cast.cast(merge(optionalKey, optionalFactory));
         }
 
         // default namespace generic T injection
         final Supplier<T> instanceSupplier = () -> {
             final StringBuilder debugLog = new StringBuilder();
-            final T instance = Cast.cast(getInjectableInstance(key, node, 
chain, debugLog));
+            final T instance = getInjectableInstance(key, node, chain, 
debugLog);
             injectMembers(key, node, instance, chain, debugLog);
             return instance;
         };
-        return bindingMap.merge(key, scope.get(key, instanceSupplier));
+        return merge(key, scope.get(key, instanceSupplier));
+    }
+
+    private <T> Supplier<T> merge(final Key<T> key, final Supplier<T> factory) 
{
+        final Binding<?> binding = bindings.merge(key, 
Binding.from(key).to(factory), (oldValue, value) ->
+                oldValue.getKey().getOrder() <= value.getKey().getOrder() ? 
oldValue : value);
+        return Cast.cast(binding);
     }
 
     private Supplier<PluginNamespace> createPluginNamespaceFactory(final 
Key<PluginNamespace> key) {
@@ -364,7 +345,7 @@ class DefaultInjector implements Injector {
     }
 
     private <T> Optional<T> getOptionalInstance(
-            final Key<T> key, final Collection<String> aliases, final Node 
node, final Set<Key<?>> chain) {
+            final Key<T> key, final Collection<String> aliases, final Node 
node, final DependencyChain chain) {
         try {
             return Optional.ofNullable(getFactory(key, aliases, node, 
chain).get());
         } catch (final PluginException e) {
@@ -372,11 +353,11 @@ class DefaultInjector implements Injector {
         }
     }
 
-    private Object getInjectableInstance(
-            final Key<?> key, final Node node, final Set<Key<?>> chain, final 
StringBuilder debugLog) {
-        final Class<?> rawType = key.getRawType();
+    private <T> T getInjectableInstance(
+            final Key<T> key, final Node node, final DependencyChain chain, 
final StringBuilder debugLog) {
+        final Class<T> rawType = key.getRawType();
         validate(rawType, key.getName(), rawType);
-        final Constructor<?> constructor = getInjectableConstructor(key, 
chain);
+        final Constructor<T> constructor = 
BeanUtils.getInjectableConstructor(key, chain);
         final List<InjectionPoint<?>> points = 
InjectionPoint.fromExecutable(constructor);
         final var args = getArguments(key, node, points, chain, debugLog);
         return accessor.newInstance(constructor, args);
@@ -400,77 +381,8 @@ class DefaultInjector implements Injector {
         }
     }
 
-    private void initializeTypeConverters() {
-        final List<TypeConverter<?>> converters = getPluginList(new 
@TypeConverters Key<>() {});
-        converters.forEach(converter -> 
registerTypeConverter(getTypeConverterSupportedType(converter.getClass()), 
converter));
-        registerTypeConverter(Boolean.class, Boolean::valueOf);
-        registerTypeAlias(Boolean.class, Boolean.TYPE);
-        registerTypeConverter(Byte.class, Byte::valueOf);
-        registerTypeAlias(Byte.class, Byte.TYPE);
-        registerTypeConverter(Character.class, s -> {
-            if (s.length() != 1) {
-                throw new IllegalArgumentException("Character string must be 
of length 1: " + s);
-            }
-            return s.toCharArray()[0];
-        });
-        registerTypeAlias(Character.class, Character.TYPE);
-        registerTypeConverter(Double.class, Double::valueOf);
-        registerTypeAlias(Double.class, Double.TYPE);
-        registerTypeConverter(Float.class, Float::valueOf);
-        registerTypeAlias(Float.class, Float.TYPE);
-        registerTypeConverter(Integer.class, Integer::valueOf);
-        registerTypeAlias(Integer.class, Integer.TYPE);
-        registerTypeConverter(Long.class, Long::valueOf);
-        registerTypeAlias(Long.class, Long.TYPE);
-        registerTypeConverter(Short.class, Short::valueOf);
-        registerTypeAlias(Short.class, Short.TYPE);
-        registerTypeConverter(String.class, s -> s);
-    }
-
-    private TypeConverter<?> registerTypeConverter(final Type type, final 
TypeConverter<?> converter) {
-        final TypeConverter<?> conflictingConverter = typeConverters.get(type);
-        if (conflictingConverter != null) {
-            final boolean overridable;
-            if (converter instanceof Comparable) {
-                @SuppressWarnings("unchecked") final 
Comparable<TypeConverter<?>> comparableConverter =
-                        (Comparable<TypeConverter<?>>) converter;
-                overridable = 
comparableConverter.compareTo(conflictingConverter) < 0;
-            } else if (conflictingConverter instanceof Comparable) {
-                @SuppressWarnings("unchecked") final 
Comparable<TypeConverter<?>> comparableConflictingConverter =
-                        (Comparable<TypeConverter<?>>) conflictingConverter;
-                overridable = 
comparableConflictingConverter.compareTo(converter) > 0;
-            } else {
-                overridable = false;
-            }
-            if (overridable) {
-                LOGGER.debug(
-                        "Replacing TypeConverter [{}] for type [{}] with [{}] 
after comparison.",
-                        conflictingConverter, type, converter);
-                typeConverters.put(type, converter);
-                return converter;
-            } else {
-                LOGGER.warn(
-                        "Ignoring TypeConverter [{}] for type [{}] that 
conflicts with [{}], since they are not comparable.",
-                        converter, type, conflictingConverter);
-                return conflictingConverter;
-            }
-        } else {
-            typeConverters.put(type, converter);
-            return converter;
-        }
-    }
-
-    private void registerTypeAlias(final Type knownType, final Type aliasType) 
{
-        final TypeConverter<?> converter = typeConverters.get(knownType);
-        if (converter != null) {
-            typeConverters.put(aliasType, converter);
-        } else {
-            LOGGER.error("Cannot locate type converter for {}", knownType);
-        }
-    }
-
     private void injectMembers(
-            final Key<?> key, final Node node, final Object instance, final 
Set<Key<?>> chain, final StringBuilder debugLog) {
+            final Key<?> key, final Node node, final Object instance, final 
DependencyChain chain, final StringBuilder debugLog) {
         injectFields(key.getRawType(), node, instance, debugLog);
         injectMethods(key, node, instance, chain, debugLog);
     }
@@ -478,7 +390,7 @@ class DefaultInjector implements Injector {
     private void injectFields(final Class<?> rawType, final Node node, final 
Object instance, final StringBuilder debugLog) {
         for (Class<?> clazz = rawType; clazz != Object.class; clazz = 
clazz.getSuperclass()) {
             for (final Field field : clazz.getDeclaredFields()) {
-                if (isInjectable(field)) {
+                if (BeanUtils.isInjectable(field)) {
                     injectField(field, node, instance, debugLog);
                 }
             }
@@ -487,7 +399,7 @@ class DefaultInjector implements Injector {
 
     private <T> void injectField(final Field field, final Node node, final 
Object instance, final StringBuilder debugLog) {
         final InjectionPoint<T> point = InjectionPoint.forField(field);
-        final Supplier<T> factory = getFactory(point, node, Set.of(), 
debugLog);
+        final Supplier<T> factory = getFactory(point, node, 
DependencyChain.empty(), debugLog);
         final Key<T> key = point.getKey();
         final Object value = key.getRawType() == Supplier.class ? factory : 
factory.get();
         if (value != null) {
@@ -500,12 +412,12 @@ class DefaultInjector implements Injector {
     }
 
     private void injectMethods(
-            final Key<?> key, final Node node, final Object instance, final 
Set<Key<?>> chain, final StringBuilder debugLog) {
+            final Key<?> key, final Node node, final Object instance, final 
DependencyChain chain, final StringBuilder debugLog) {
         final Class<?> rawType = key.getRawType();
         final List<Method> injectMethodsWithNoArgs = new ArrayList<>();
         for (Class<?> clazz = rawType; clazz != Object.class; clazz = 
clazz.getSuperclass()) {
             for (final Method method : clazz.getDeclaredMethods()) {
-                if (isInjectable(method)) {
+                if (BeanUtils.isInjectable(method)) {
                     accessor.makeAccessible(method, instance);
                     if (method.getParameterCount() == 0) {
                         injectMethodsWithNoArgs.add(method);
@@ -544,7 +456,7 @@ class DefaultInjector implements Injector {
             final Object instance = getInjectablePluginInstance(node, 
debugLog);
             if (instance instanceof Supplier<?>) {
                 // configure plugin builder class and obtain plugin from that
-                injectMembers(Key.forClass(instance.getClass()), node, 
instance, Set.of(), debugLog);
+                injectMembers(Key.forClass(instance.getClass()), node, 
instance, DependencyChain.empty(), debugLog);
                 node.setObject(((Supplier<?>) instance).get());
             } else {
                 // usually created via static plugin factory method, but 
otherwise assume this is the final plugin instance
@@ -567,12 +479,12 @@ class DefaultInjector implements Injector {
                 .allMatch(condition -> condition.matches(key, rawType))) {
             return null;
         }
-        final Executable factory = getInjectablePluginFactory(rawType);
+        final Executable factory = BeanUtils.getInjectableFactory(rawType);
         final List<InjectionPoint<?>> points = 
InjectionPoint.fromExecutable(factory);
         if (!factory.canAccess(null)) {
             accessor.makeAccessible(factory);
         }
-        final var args = getArguments(key, node, points, Set.of(), debugLog);
+        final var args = getArguments(key, node, points, 
DependencyChain.empty(), debugLog);
         if (factory instanceof Method) {
             return accessor.invokeMethod((Method) factory, null, args);
         } else {
@@ -593,7 +505,7 @@ class DefaultInjector implements Injector {
                         final var bindings = createMethodBindings(bundle, 
method);
                         if (!bindings.isEmpty()) {
                             providerMethods.add(method);
-                            bindings.forEach(binding -> 
bindingMap.merge(binding.getKey(), binding.getSupplier()));
+                            bindings.forEach(binding -> 
merge(binding.getKey(), binding));
                         }
                     }
                 });
@@ -611,7 +523,7 @@ class DefaultInjector implements Injector {
             return List.of();
         }
         final List<InjectionPoint<?>> points = 
InjectionPoint.fromExecutable(method);
-        final var argumentFactories = getArgumentFactories(primaryKey, null, 
points, Set.of(primaryKey), null);
+        final var argumentFactories = getArgumentFactories(primaryKey, null, 
points, DependencyChain.of(primaryKey), null);
         final Supplier<T> unscoped = () -> {
             final var args = argumentFactories.entrySet()
                     .stream()
@@ -628,15 +540,15 @@ class DefaultInjector implements Injector {
         final Supplier<T> factory = getScopeForMethod(method).get(primaryKey, 
unscoped);
         final Collection<String> aliases = Keys.getAliases(method);
         final List<Binding<T>> bindings = new ArrayList<>(1 + aliases.size());
-        bindings.add(Binding.bind(primaryKey, factory));
+        bindings.add(Binding.from(primaryKey).to(factory));
         for (final String alias : aliases) {
-            bindings.add(Binding.bind(primaryKey.withName(alias), factory));
+            bindings.add(Binding.from(primaryKey.withName(alias)).to(factory));
         }
         return bindings;
     }
 
     private Object[] getArguments(
-            final Key<?> key, final Node node, final List<InjectionPoint<?>> 
points, final Set<Key<?>> chain,
+            final Key<?> key, final Node node, final List<InjectionPoint<?>> 
points, final DependencyChain chain,
             final StringBuilder debugLog) {
         return getArgumentFactories(key, node, points, chain, debugLog)
                 .entrySet()
@@ -652,7 +564,7 @@ class DefaultInjector implements Injector {
     }
 
     private Map<Parameter, Supplier<?>> getArgumentFactories(
-            final Key<?> key, final Node node, final List<InjectionPoint<?>> 
points, final Set<Key<?>> chain,
+            final Key<?> key, final Node node, final List<InjectionPoint<?>> 
points, final DependencyChain chain,
             final StringBuilder debugLog) {
         final Map<Parameter, Supplier<?>> argFactories = new LinkedHashMap<>();
         for (final InjectionPoint<?> point : points) {
@@ -661,14 +573,9 @@ class DefaultInjector implements Injector {
             if (parameterKey.getRawType().equals(Supplier.class)) {
                 argFactories.put(parameter, () -> getFactory(point, node, 
chain, debugLog));
             } else {
-                final var newChain = chain(chain, key);
-                if (newChain.contains(parameterKey)) {
-                    final StringBuilder sb = new StringBuilder("Circular 
dependency encountered: ");
-                    for (final Key<?> chainKey : newChain) {
-                        sb.append(chainKey).append(" -> ");
-                    }
-                    sb.append(parameterKey);
-                    throw new InjectException(sb.toString());
+                final var newChain = chain.withDependency(key);
+                if (newChain.hasDependency(parameterKey)) {
+                    throw new CircularDependencyException(parameterKey, 
newChain);
                 }
                 argFactories.put(parameter, () -> getFactory(point, node, 
newChain, debugLog).get());
             }
@@ -707,18 +614,6 @@ class DefaultInjector implements Injector {
         ((ConstraintValidator) validator).initialize(annotation);
     }
 
-    private static Type getTypeConverterSupportedType(final Class<?> clazz) {
-        for (final Type type : clazz.getGenericInterfaces()) {
-            if (type instanceof ParameterizedType) {
-                final ParameterizedType parameterizedType = 
(ParameterizedType) type;
-                if (parameterizedType.getRawType() == TypeConverter.class) {
-                    return parameterizedType.getActualTypeArguments()[0];
-                }
-            }
-        }
-        return Void.TYPE;
-    }
-
     private static void verifyAttributesConsumed(final Node node) {
         final Map<String, String> attrs = node.getAttributes();
         if (!attrs.isEmpty()) {
@@ -751,99 +646,4 @@ class DefaultInjector implements Injector {
             }
         }
     }
-
-    private static Set<Key<?>> chain(final Set<Key<?>> chain, final Key<?> 
newKey) {
-        if (chain == null || chain.isEmpty()) {
-            return Set.of(newKey);
-        }
-        final var newChain = new LinkedHashSet<>(chain);
-        newChain.add(newKey);
-        return newChain;
-    }
-
-    private static Executable getInjectablePluginFactory(final Class<?> 
pluginClass) {
-        return Stream.of(pluginClass.getDeclaredMethods())
-                .filter(method -> Modifier.isStatic(method.getModifiers()) &&
-                        AnnotationUtil.isMetaAnnotationPresent(method, 
FactoryType.class))
-                
.min(Comparator.comparingInt(Method::getParameterCount).thenComparing(Method::getReturnType,
 (c1, c2) -> {
-                    if (c1.equals(c2)) {
-                        return 0;
-                    } else if (Supplier.class.isAssignableFrom(c1)) {
-                        return -1;
-                    } else if (Supplier.class.isAssignableFrom(c2)) {
-                        return 1;
-                    } else {
-                        return c1.getName().compareTo(c2.getName());
-                    }
-                }))
-                .map(Executable.class::cast)
-                .orElseGet(() -> 
getInjectableConstructor(Key.forClass(pluginClass), Set.of()));
-    }
-
-    private static <T> Constructor<T> getInjectableConstructor(final Key<T> 
key, final Set<Key<?>> chain) {
-        final Class<T> rawType = key.getRawType();
-        final List<Constructor<?>> injectConstructors = 
Stream.of(rawType.getDeclaredConstructors())
-                .filter(constructor -> 
constructor.isAnnotationPresent(Inject.class))
-                .collect(Collectors.toList());
-        if (injectConstructors.size() > 1) {
-            throw new InjectException("Multiple @Inject constructors found in 
" + rawType);
-        }
-        if (injectConstructors.size() == 1) {
-            return Cast.cast(injectConstructors.get(0));
-        }
-        try {
-            return rawType.getDeclaredConstructor();
-        } catch (final NoSuchMethodException ignored) {
-        }
-        try {
-            return rawType.getConstructor();
-        } catch (final NoSuchMethodException ignored) {
-        }
-        final List<Key<?>> keys = new ArrayList<>(chain);
-        keys.add(0, key);
-        final String prefix = chain.isEmpty() ? "" : "chain ";
-        final String keysToString =
-                prefix + 
keys.stream().map(Key::toString).collect(Collectors.joining(" -> "));
-        throw new InjectException(
-                "No @Inject constructors or no-arg constructor found for " + 
keysToString);
-    }
-
-    private static boolean isInjectable(final Field field) {
-        return field.isAnnotationPresent(Inject.class) || 
AnnotationUtil.isMetaAnnotationPresent(field, QualifierType.class);
-    }
-
-    private static boolean isInjectable(final Method method) {
-        return method.isAnnotationPresent(Inject.class) ||
-                !AnnotationUtil.isMetaAnnotationPresent(method, 
FactoryType.class) &&
-                        Stream.of(method.getParameters()).anyMatch(
-                                parameter -> 
AnnotationUtil.isMetaAnnotationPresent(parameter, QualifierType.class));
-    }
-
-    private static class SingletonScope implements Scope {
-        private final Map<Key<?>, Supplier<?>> singletonProviders = new 
ConcurrentHashMap<>();
-
-        @Override
-        public <T> Supplier<T> get(final Key<T> key, final Supplier<T> 
unscoped) {
-            return Cast.cast(singletonProviders.computeIfAbsent(key, ignored 
-> Lazy.lazy(unscoped)::value));
-        }
-
-        @Override
-        public String toString() {
-            return "[SingletonScope; size=" + singletonProviders.size() + "]";
-        }
-    }
-
-    private enum DefaultScope implements Scope {
-        INSTANCE;
-
-        @Override
-        public <T> Supplier<T> get(final Key<T> key, final Supplier<T> 
unscoped) {
-            return unscoped;
-        }
-
-        @Override
-        public String toString() {
-            return "[Unscoped]";
-        }
-    }
 }
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultScope.java
similarity index 65%
copy from 
log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java
copy to 
log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultScope.java
index 88e707fd9d..5699c7f8b3 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultScope.java
@@ -14,29 +14,20 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-
 package org.apache.logging.log4j.plugins.di;
 
 import java.util.function.Supplier;
 
-class Binding<T> {
-    private final Key<T> key;
-    private final Supplier<T> supplier;
-
-    private Binding(final Key<T> key, final Supplier<T> supplier) {
-        this.key = key;
-        this.supplier = supplier;
-    }
-
-    public Key<T> getKey() {
-        return key;
-    }
+enum DefaultScope implements Scope {
+    INSTANCE;
 
-    public Supplier<T> getSupplier() {
-        return supplier;
+    @Override
+    public <T> Supplier<T> get(final Key<T> key, final Supplier<T> unscoped) {
+        return unscoped;
     }
 
-    public static <T> Binding<T> bind(final Key<T> key, final Supplier<T> 
supplier) {
-        return new Binding<>(key, supplier);
+    @Override
+    public String toString() {
+        return "[Unscoped]";
     }
 }
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DependencyChain.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DependencyChain.java
new file mode 100644
index 0000000000..2751d05dbe
--- /dev/null
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DependencyChain.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.plugins.di;
+
+/**
+ * Immutable chain of {@linkplain Key dependency keys} assembled while 
constructing dependency-injected
+ * instances.
+ */
+public interface DependencyChain extends Iterable<Key<?>> {
+    /**
+     * Indicates if the given key is contained in this dependency chain.
+     */
+    boolean hasDependency(final Key<?> key);
+
+    /**
+     * Returns a new dependency chain containing the provided key as an 
additional dependency.
+     * If the given key already exists, then it will not be added to the chain.
+     */
+    DependencyChain withDependency(final Key<?> key);
+
+    /**
+     * Indicates whether this dependency chain is empty.
+     */
+    boolean isEmpty();
+
+    /**
+     * Returns an empty dependency chain.
+     */
+    static DependencyChain empty() {
+        return DependencyChains.EMPTY;
+    }
+
+    /**
+     * Returns a dependency chain containing a single key.
+     */
+    static DependencyChain of(final Key<?> key) {
+        return DependencyChains.singleton(key);
+    }
+}
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DependencyChains.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DependencyChains.java
new file mode 100644
index 0000000000..750ce81724
--- /dev/null
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DependencyChains.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.plugins.di;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Objects;
+
+class DependencyChains {
+    static final DependencyChain EMPTY = new EmptyChain();
+
+    static DependencyChain singleton(final Key<?> key) {
+        return new LinkedChain(key);
+    }
+
+    private static final class EmptyChain implements DependencyChain {
+        @Override
+        public boolean hasDependency(final Key<?> key) {
+            return false;
+        }
+
+        @Override
+        public DependencyChain withDependency(final Key<?> key) {
+            return new LinkedChain(key);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return true;
+        }
+
+        @Override
+        public Iterator<Key<?>> iterator() {
+            return Collections.emptyIterator();
+        }
+
+        @Override
+        public int hashCode() {
+            return 1;
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            return this == o || o != null && getClass() == o.getClass();
+        }
+    }
+
+    private static final class LinkedChain implements DependencyChain {
+        private final Key<?> head;
+        private final LinkedChain tail;
+
+        private LinkedChain(final Key<?> head) {
+            this(head, null);
+        }
+
+        private LinkedChain(final Key<?> head, final LinkedChain tail) {
+            this.head = head;
+            this.tail = tail;
+        }
+
+        @Override
+        public boolean hasDependency(final Key<?> key) {
+            return key.equals(head) || tail != null && tail.hasDependency(key);
+        }
+
+        @Override
+        public LinkedChain withDependency(final Key<?> key) {
+            if (key.equals(head)) {
+                return this;
+            }
+            final LinkedChain newTail = tail != null ? 
tail.withDependency(key) : new LinkedChain(key);
+            return new LinkedChain(head, newTail);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+
+        @Override
+        public Iterator<Key<?>> iterator() {
+            return new Iter(this);
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            final LinkedChain keys = (LinkedChain) o;
+            return head.equals(keys.head) && Objects.equals(tail, keys.tail);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(head, tail);
+        }
+
+        private static class Iter implements Iterator<Key<?>> {
+            private LinkedChain current;
+
+            private Iter(final LinkedChain current) {
+                this.current = current;
+            }
+
+            @Override
+            public boolean hasNext() {
+                return current != null;
+            }
+
+            @Override
+            public Key<?> next() {
+                final Key<?> head = current.head;
+                current = current.tail;
+                return head;
+            }
+        }
+    }
+}
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/InjectionPoint.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/InjectionPoint.java
index 56e3aebc9f..0c0b8c48ed 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/InjectionPoint.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/InjectionPoint.java
@@ -14,7 +14,6 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-
 package org.apache.logging.log4j.plugins.di;
 
 import java.lang.reflect.AnnotatedElement;
@@ -27,13 +26,13 @@ import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-class InjectionPoint<T> {
+public class InjectionPoint<T> {
     private final Key<T> key;
     private final Collection<String> aliases;
     private final Member member;
     private final AnnotatedElement element;
 
-    InjectionPoint(final Key<T> key, final Collection<String> aliases, final 
Member member, final AnnotatedElement element) {
+    private InjectionPoint(final Key<T> key, final Collection<String> aliases, 
final Member member, final AnnotatedElement element) {
         this.key = key;
         this.aliases = aliases;
         this.member = member;
@@ -56,19 +55,19 @@ class InjectionPoint<T> {
         return element;
     }
 
-    static <T> InjectionPoint<T> forField(final Field field) {
+    public static <T> InjectionPoint<T> forField(final Field field) {
         final Key<T> key = Key.forField(field);
         final Collection<String> aliases = Keys.getAliases(field);
         return new InjectionPoint<>(key, aliases, field, field);
     }
 
-    static <T> InjectionPoint<T> forParameter(final Executable executable, 
final Parameter parameter) {
+    public static <T> InjectionPoint<T> forParameter(final Executable 
executable, final Parameter parameter) {
         final Key<T> key = Key.forParameter(parameter);
         final Collection<String> aliases = Keys.getAliases(parameter);
         return new InjectionPoint<>(key, aliases, executable, parameter);
     }
 
-    static List<InjectionPoint<?>> fromExecutable(final Executable executable) 
{
+    public static List<InjectionPoint<?>> fromExecutable(final Executable 
executable) {
         return Stream.of(executable.getParameters())
                 .map(parameter -> forParameter(executable, parameter))
                 .collect(Collectors.toList());
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/SingletonScope.java
similarity index 59%
copy from 
log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java
copy to 
log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/SingletonScope.java
index 88e707fd9d..f8c9cdaa7f 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/Binding.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/SingletonScope.java
@@ -14,29 +14,25 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-
 package org.apache.logging.log4j.plugins.di;
 
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Supplier;
 
-class Binding<T> {
-    private final Key<T> key;
-    private final Supplier<T> supplier;
-
-    private Binding(final Key<T> key, final Supplier<T> supplier) {
-        this.key = key;
-        this.supplier = supplier;
-    }
+import org.apache.logging.log4j.util.Cast;
+import org.apache.logging.log4j.util.Lazy;
 
-    public Key<T> getKey() {
-        return key;
-    }
+class SingletonScope implements Scope {
+    private final Map<Key<?>, Supplier<?>> singletonProviders = new 
ConcurrentHashMap<>();
 
-    public Supplier<T> getSupplier() {
-        return supplier;
+    @Override
+    public <T> Supplier<T> get(final Key<T> key, final Supplier<T> unscoped) {
+        return Cast.cast(singletonProviders.computeIfAbsent(key, ignored -> 
Lazy.lazy(unscoped)::value));
     }
 
-    public static <T> Binding<T> bind(final Key<T> key, final Supplier<T> 
supplier) {
-        return new Binding<>(key, supplier);
+    @Override
+    public String toString() {
+        return "[SingletonScope; size=" + singletonProviders.size() + "]";
     }
 }
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/BeanUtils.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/BeanUtils.java
index 2b98f3bdf2..f8078a57ec 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/BeanUtils.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/BeanUtils.java
@@ -16,9 +16,33 @@
  */
 package org.apache.logging.log4j.plugins.internal.util;
 
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.logging.log4j.plugins.FactoryType;
+import org.apache.logging.log4j.plugins.Inject;
+import org.apache.logging.log4j.plugins.QualifierType;
+import org.apache.logging.log4j.plugins.di.AmbiguousInjectConstructorException;
+import org.apache.logging.log4j.plugins.di.DependencyChain;
+import org.apache.logging.log4j.plugins.di.Key;
+import org.apache.logging.log4j.plugins.di.NotInjectableException;
+import org.apache.logging.log4j.plugins.util.AnnotationUtil;
+import org.apache.logging.log4j.util.Cast;
+import org.apache.logging.log4j.util.InternalApi;
+
 /**
  * Utility methods.
  */
+@InternalApi
 public final class BeanUtils {
     private BeanUtils() {
     }
@@ -34,4 +58,79 @@ public final class BeanUtils {
         chars[0] = Character.toLowerCase(chars[0]);
         return new String(chars);
     }
+
+    public static Executable getInjectableFactory(final Class<?> clazz) {
+        return Stream.of(clazz.getDeclaredMethods())
+                .filter(method -> Modifier.isStatic(method.getModifiers()) &&
+                        AnnotationUtil.isMetaAnnotationPresent(method, 
FactoryType.class))
+                
.min(Comparator.comparingInt(Method::getParameterCount).thenComparing(Method::getReturnType,
 (c1, c2) -> {
+                    if (c1.equals(c2)) {
+                        return 0;
+                    } else if (Supplier.class.isAssignableFrom(c1)) {
+                        return -1;
+                    } else if (Supplier.class.isAssignableFrom(c2)) {
+                        return 1;
+                    } else {
+                        return c1.getName().compareTo(c2.getName());
+                    }
+                }))
+                .map(Executable.class::cast)
+                .orElseGet(() -> getInjectableConstructor(clazz));
+    }
+
+    public static <T> Constructor<T> getInjectableConstructor(final Class<T> 
clazz) {
+        final Constructor<T> constructor = findInjectableConstructor(clazz);
+        if (constructor == null) {
+            throw new NotInjectableException(clazz);
+        }
+        return constructor;
+    }
+
+    public static <T> Constructor<T> getInjectableConstructor(final Key<T> 
key, final DependencyChain chain) {
+        final Constructor<T> constructor = 
findInjectableConstructor(key.getRawType());
+        if (constructor == null) {
+            throw new NotInjectableException(key, chain);
+        }
+        return constructor;
+    }
+
+    public static boolean isInjectable(final Field field) {
+        return field.isAnnotationPresent(Inject.class) || 
AnnotationUtil.isMetaAnnotationPresent(field, QualifierType.class);
+    }
+
+    public static boolean isInjectable(final Method method) {
+        if (method.isAnnotationPresent(Inject.class)) {
+            return true;
+        }
+        if (!AnnotationUtil.isMetaAnnotationPresent(method, 
FactoryType.class)) {
+            for (final Parameter parameter : method.getParameters()) {
+                if (AnnotationUtil.isMetaAnnotationPresent(parameter, 
QualifierType.class)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static <T> Constructor<T> findInjectableConstructor(final Class<T> 
clazz) {
+        final List<Constructor<?>> constructors = 
Stream.of(clazz.getDeclaredConstructors())
+                .filter(constructor -> 
constructor.isAnnotationPresent(Inject.class))
+                .collect(Collectors.toList());
+        final int size = constructors.size();
+        if (size > 1) {
+            throw new AmbiguousInjectConstructorException(clazz);
+        }
+        if (size == 1) {
+            return Cast.cast(constructors.get(0));
+        }
+        try {
+            return clazz.getDeclaredConstructor();
+        } catch (final NoSuchMethodException ignored) {
+        }
+        try {
+            return clazz.getConstructor();
+        } catch (final NoSuchMethodException ignored) {
+        }
+        return null;
+    }
 }
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/HierarchicalCollections.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/HierarchicalCollections.java
new file mode 100644
index 0000000000..ad47c27427
--- /dev/null
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/HierarchicalCollections.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.plugins.internal.util;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+class HierarchicalCollections {
+    static <K, V> HierarchicalMap<K, V> newRootMap() {
+        return new RootMap<>();
+    }
+
+    private static class RootMap<K, V> extends AbstractMap<K, V> implements 
HierarchicalMap<K, V> {
+        private final Map<K, V> map = new LinkedHashMap<>();
+
+        @Override
+        public Set<Entry<K, V>> entrySet() {
+            return map.entrySet();
+        }
+
+        @Override
+        public V put(final K key, final V value) {
+            return map.put(key, value);
+        }
+
+        @Override
+        public boolean containsKey(final Object key) {
+            return map.containsKey(key);
+        }
+
+        @Override
+        public boolean containsLocalKey(final K key) {
+            return map.containsKey(key);
+        }
+
+        @Override
+        public Set<K> keySet() {
+            return map.keySet();
+        }
+
+        @Override
+        public HierarchicalMap<K, V> newChildMap() {
+            return new ChildMap<>(this);
+        }
+    }
+
+    private static class ChildMap<K, V> extends RootMap<K, V> {
+        private final Map<K, V> parent;
+
+        private ChildMap(final Map<K, V> parent) {
+            this.parent = parent;
+        }
+
+        @Override
+        public Set<Entry<K, V>> entrySet() {
+            return new HierarchicalSet<>(super.entrySet(), parent.entrySet());
+        }
+
+        @Override
+        public boolean containsKey(final Object key) {
+            return super.containsKey(key) || parent.containsKey(key);
+        }
+
+        @Override
+        public Set<K> keySet() {
+            return new HierarchicalSet<>(super.keySet(), parent.keySet());
+        }
+
+        @Override
+        public HierarchicalMap<K, V> newChildMap() {
+            return new ChildMap<>(this);
+        }
+    }
+
+    private static class HierarchicalSet<E> extends AbstractSet<E> {
+        private final Set<E> delegate;
+        private final Set<E> parent;
+
+        private HierarchicalSet(final Set<E> delegate, final Set<E> parent) {
+            this.delegate = delegate;
+            this.parent = parent;
+        }
+
+        @Override
+        public Iterator<E> iterator() {
+            return new HierarchicalIterator<>(delegate.iterator(), 
parent.iterator());
+        }
+
+        @Override
+        public int size() {
+            return delegate.size() + parent.size();
+        }
+
+        @Override
+        public boolean contains(final Object o) {
+            return delegate.contains(o) || parent.contains(o);
+        }
+    }
+
+    private static class HierarchicalIterator<E> implements Iterator<E> {
+        private final Iterator<E> delegate;
+        private final Iterator<E> parent;
+        private State state;
+
+        private HierarchicalIterator(final Iterator<E> delegate, final 
Iterator<E> parent) {
+            this.delegate = delegate;
+            this.parent = parent;
+            this.state = State.ITERATING_DELEGATE;
+        }
+
+        @Override
+        public boolean hasNext() {
+            switch (state) {
+                case ITERATING_DELEGATE:
+                    if (delegate.hasNext()) {
+                        return true;
+                    }
+                    state = State.ITERATING_PARENT;
+
+                case ITERATING_PARENT:
+                    if (parent.hasNext()) {
+                        return true;
+                    }
+                    state = State.DONE;
+
+                case DONE:
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public E next() {
+            switch (state) {
+                case ITERATING_DELEGATE:
+                    if (delegate.hasNext()) {
+                        return delegate.next();
+                    }
+                    state = State.ITERATING_PARENT;
+
+                case ITERATING_PARENT:
+                    if (parent.hasNext()) {
+                        return parent.next();
+                    }
+                    state = State.DONE;
+
+                case DONE:
+                default:
+                    throw new NoSuchElementException("Completed iteration");
+            }
+        }
+
+        @Override
+        public void remove() {
+            switch (state) {
+                case ITERATING_DELEGATE:
+                    delegate.remove();
+                    break;
+
+                case ITERATING_PARENT:
+                    parent.remove();
+                    break;
+
+                case DONE:
+                default:
+                    throw new IllegalStateException("Completed iteration");
+            }
+        }
+
+        private enum State {
+            ITERATING_DELEGATE,
+            ITERATING_PARENT,
+            DONE
+        }
+    }
+}
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/BeanUtils.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/HierarchicalMap.java
similarity index 64%
copy from 
log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/BeanUtils.java
copy to 
log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/HierarchicalMap.java
index 2b98f3bdf2..f6db5d9d91 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/BeanUtils.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/internal/util/HierarchicalMap.java
@@ -16,22 +16,17 @@
  */
 package org.apache.logging.log4j.plugins.internal.util;
 
-/**
- * Utility methods.
- */
-public final class BeanUtils {
-    private BeanUtils() {
-    }
+import java.util.Map;
+
+import org.apache.logging.log4j.util.InternalApi;
+
+@InternalApi
+public interface HierarchicalMap<K, V> extends Map<K, V> {
+    boolean containsLocalKey(final K key);
+
+    HierarchicalMap<K, V> newChildMap();
 
-    public static String decapitalize(String string) {
-        if (string.isEmpty()) {
-            return string;
-        }
-        char[] chars = string.toCharArray();
-        if (chars.length >= 2 && Character.isUpperCase(chars[0]) && 
Character.isUpperCase(chars[1])) {
-            return string;
-        }
-        chars[0] = Character.toLowerCase(chars[0]);
-        return new String(chars);
+    static <K, V> HierarchicalMap<K, V> newRootMap() {
+        return HierarchicalCollections.newRootMap();
     }
 }
diff --git a/pom.xml b/pom.xml
index 28d806a62a..786d2c52aa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1700,10 +1700,10 @@
         </configuration>
         <executions>
           <execution>
-            <phase>validate</phase>
             <goals>
               <goal>validate</goal>
             </goals>
+            <phase>validate</phase>
           </execution>
         </executions>
       </plugin>

Reply via email to