You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by ma...@apache.org on 2022/01/17 01:51:01 UTC

[logging-log4j2] 02/02: Load TypeConverters via BeanManager

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

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

commit 35467f6d0ccd4f43c9d44b2d81b73213637c574a
Author: Matt Sicker <ma...@apache.org>
AuthorDate: Sun Jan 16 19:50:29 2022 -0600

    Load TypeConverters via BeanManager
    
    - Make BeanManager a service loader class
    - Add lazy loading Value<Class<?>> for PluginSource instances
    - Refactor TypeConverterRegistry to use BeanManager
    - Add ResolverUtil-based classpath scanning option for loading beans
    
    Signed-off-by: Matt Sicker <ma...@apache.org>
---
 .../log4j/plugin/processor/BeanProcessor.java      | 108 +++++++++----------
 .../log4j/plugin/processor/BeanProcessorTest.java  |  21 ++--
 log4j-plugins/src/main/java/module-info.java       |   3 +
 .../plugins/convert/TypeConverterRegistry.java     |  52 +++++++---
 .../log4j/plugins/di/model/DisposesMethod.java     |  29 +++---
 .../log4j/plugins/di/model/GenericPlugin.java      |  20 ++--
 .../log4j/plugins/di/model/InjectionTarget.java    |  23 ++---
 .../log4j/plugins/di/model/PluginModule.java       |  12 +--
 .../log4j/plugins/di/model/PluginSource.java       |   5 +-
 .../log4j/plugins/di/model/ProducerField.java      |  24 ++---
 .../log4j/plugins/di/model/ProducerMethod.java     |  33 +++---
 .../logging/log4j/plugins/spi/BeanManager.java     |  32 +++++-
 .../log4j/plugins/spi/InitializationException.java |   4 +
 .../log4j/plugins/spi/impl/DefaultBeanManager.java | 114 ++++++++++++++++++---
 .../logging/log4j/plugins/util/PluginLoader.java   |  60 +++++++++++
 .../logging/log4j/plugins/util/PluginUtil.java     |  15 +++
 .../logging/log4j/plugins/util/TypeUtil.java       |  11 +-
 ...rg.apache.logging.log4j.plugins.spi.BeanManager |  18 ++++
 18 files changed, 393 insertions(+), 191 deletions(-)

diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/BeanProcessor.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/BeanProcessor.java
index 3ab23ca..74d3c44 100644
--- a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/BeanProcessor.java
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/BeanProcessor.java
@@ -17,12 +17,10 @@
 
 package org.apache.logging.log4j.plugin.processor;
 
-import org.apache.logging.log4j.plugins.di.DependentScoped;
 import org.apache.logging.log4j.plugins.di.Disposes;
 import org.apache.logging.log4j.plugins.di.Inject;
 import org.apache.logging.log4j.plugins.di.Producer;
 import org.apache.logging.log4j.plugins.di.Qualifier;
-import org.apache.logging.log4j.plugins.di.ScopeType;
 
 import javax.annotation.processing.AbstractProcessor;
 import javax.annotation.processing.RoundEnvironment;
@@ -40,6 +38,7 @@ import javax.lang.model.element.TypeElement;
 import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.DeclaredType;
 import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
 import javax.lang.model.util.ElementKindVisitor9;
 import javax.lang.model.util.Elements;
 import javax.lang.model.util.SimpleTypeVisitor9;
@@ -56,7 +55,6 @@ import java.util.List;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 @SupportedAnnotationTypes({"org.apache.logging.log4j.plugins.*", "org.apache.logging.log4j.core.config.plugins.*"})
 @SupportedOptions({"pluginPackage", "pluginClassName"})
@@ -71,6 +69,9 @@ public class BeanProcessor extends AbstractProcessor {
         return SourceVersion.latestSupported();
     }
 
+    /**
+     * Collects fields and methods annotated with {@link Producer}-type annotations.
+     */
     private static class ProducerAnnotationVisitor extends ElementKindVisitor9<Void, Void> {
         private final List<ProducerMethodMirror> producerMethods = new ArrayList<>();
         private final List<ProducerFieldMirror> producerFields = new ArrayList<>();
@@ -88,6 +89,9 @@ public class BeanProcessor extends AbstractProcessor {
         }
     }
 
+    /**
+     * Collects {@link Disposes} methods (applied to a method argument).
+     */
     private static class DisposesAnnotationVisitor extends ElementKindVisitor9<Void, Void> {
         private final List<DisposesMirror> disposesParameters = new ArrayList<>();
 
@@ -98,6 +102,9 @@ public class BeanProcessor extends AbstractProcessor {
         }
     }
 
+    /**
+     * Collects {@link Inject}-annotated constructors, fields, and methods.
+     */
     private static class InjectAnnotationVisitor extends ElementKindVisitor9<Void, Void> {
         private final List<InjectionTargetMirror> injectableClasses = new ArrayList<>();
 
@@ -120,6 +127,10 @@ public class BeanProcessor extends AbstractProcessor {
         }
     }
 
+    /**
+     * Collects fields and method parameters annotated with {@link Qualifier}-type annotations that do not also include
+     * {@link Producer}-type annotations.
+     */
     private static class QualifiedAnnotationVisitor extends ElementKindVisitor9<Void, Void> {
         private final Predicate<AnnotationMirror> isProducerAnnotation;
         private final List<InjectionTargetMirror> injectableClasses = new ArrayList<>();
@@ -148,53 +159,29 @@ public class BeanProcessor extends AbstractProcessor {
         }
     }
 
+    /**
+     * Collects generic {@code @Plugin}-annotated classes. Plugins with a single constructor are considered for
+     * {@link Inject} targets.
+     */
     private static class PluginAnnotationVisitor extends ElementKindVisitor9<Void, Void> {
-        private final List<GenericPluginMirror> plugins = new ArrayList<>();
+        private final List<InjectionTargetMirror> implicitPlugins = new ArrayList<>();
+        private final List<GenericPluginMirror> genericPlugins = new ArrayList<>();
 
         @Override
         public Void visitTypeAsClass(final TypeElement e, final Void unused) {
-            plugins.add(new GenericPluginMirror(e));
-            return null;
-        }
-    }
-
-    private static class ScopeTypeVisitor extends ElementKindVisitor9<TypeElement, Types> {
-        protected ScopeTypeVisitor(final TypeElement defaultValue) {
-            super(defaultValue);
-        }
-
-        @Override
-        public TypeElement visitType(final TypeElement e, final Types types) {
-            for (final AnnotationMirror annotationMirror : e.getAnnotationMirrors()) {
-                final DeclaredType annotationType = annotationMirror.getAnnotationType();
-                if (annotationType.getAnnotation(ScopeType.class) != null) {
-                    return (TypeElement) annotationType.asElement();
-                }
+            final var constructors = ElementFilter.constructorsIn(e.getEnclosedElements());
+            if (constructors.size() > 1) {
+                genericPlugins.add(new GenericPluginMirror(e));
+            } else {
+                implicitPlugins.add(new InjectionTargetMirror(e));
             }
-            return super.visitType(e, types);
-        }
-
-        @Override
-        public TypeElement visitVariableAsField(final VariableElement e, final Types types) {
-            return Stream.concat(e.getAnnotationMirrors().stream(), e.asType().getAnnotationMirrors().stream())
-                    .map(AnnotationMirror::getAnnotationType)
-                    .filter(type -> type.getAnnotation(ScopeType.class) != null)
-                    .findFirst()
-                    .map(type -> (TypeElement) type.asElement())
-                    .orElse(super.DEFAULT_VALUE);
-        }
-
-        @Override
-        public TypeElement visitExecutableAsMethod(final ExecutableElement e, final Types types) {
-            return Stream.concat(e.getAnnotationMirrors().stream(), e.getReturnType().getAnnotationMirrors().stream())
-                    .map(AnnotationMirror::getAnnotationType)
-                    .filter(type -> type.getAnnotation(ScopeType.class) != null)
-                    .findFirst()
-                    .map(type -> (TypeElement) type.asElement())
-                    .orElse(super.DEFAULT_VALUE);
+            return null;
         }
     }
 
+    /**
+     * Annotation scanning mirror of {@link org.apache.logging.log4j.plugins.di.model.PluginSource}.
+     */
     interface PluginSourceMirror<E extends Element> {
         E getElement();
 
@@ -364,6 +351,7 @@ public class BeanProcessor extends AbstractProcessor {
         mirrors.addAll(producesAnnotationVisitor.producerFields);
         mirrors.addAll(injectAnnotationVisitor.injectableClasses);
         mirrors.addAll(disposesAnnotationVisitor.disposesParameters);
+        mirrors.addAll(pluginAnnotationVisitor.implicitPlugins);
         mirrors.forEach(mirror -> {
             declaringTypes.add(mirror.getDeclaringElement());
             packageElements.add(elements.getPackageOf(mirror.getDeclaringElement()));
@@ -377,7 +365,7 @@ public class BeanProcessor extends AbstractProcessor {
                     packageElements.add(elements.getPackageOf(mirror.getDeclaringElement()));
                 });
 
-        pluginAnnotationVisitor.plugins.stream()
+        pluginAnnotationVisitor.genericPlugins.stream()
                 .filter(mirror -> !declaringTypes.contains(mirror.getDeclaringElement()))
                 .forEach(mirror -> {
                     mirrors.add(mirror);
@@ -418,12 +406,14 @@ public class BeanProcessor extends AbstractProcessor {
             out.println("import java.util.List;");
             out.println("import java.util.Set;");
             out.println();
-            out.println("public class " + className + " extends PluginModule {");
+            out.println("public class " + className + " implements PluginModule {");
             out.println();
-            out.println("  private static final List<PluginSource> PLUGINS = List.of(" + javaListOfPlugins(mirrors) + ");");
+            out.println("  private final ClassLoader classLoader = getClass().getClassLoader();");
+            out.println("  private final List<PluginSource> pluginSources = List.of(" + javaListOfPlugins(mirrors) + ");");
             out.println();
-            out.println("  public " + className + "() {");
-            out.println("    super(PLUGINS);");
+            out.println("  @Override");
+            out.println("  public List<PluginSource> getPluginSources() {");
+            out.println("    return pluginSources;");
             out.println("  }");
             out.println();
             out.println("}");
@@ -433,39 +423,35 @@ public class BeanProcessor extends AbstractProcessor {
     private String javaListOfPlugins(final List<PluginSourceMirror<?>> mirrors) {
         final Elements elements = processingEnv.getElementUtils();
         final Types types = processingEnv.getTypeUtils();
-        final var scopeTypeVisitor = new ScopeTypeVisitor(elements.getTypeElement(DependentScoped.class.getCanonicalName()));
         return mirrors.stream()
                 .sorted(Comparator.<PluginSourceMirror<?>, String>comparing(m -> m.getClass().getName())
                         .thenComparing(m -> elements.getBinaryName(m.getDeclaringElement()), CharSequence::compare))
                 .map(mirror -> {
                     final String declaringClassName = '"' + elements.getBinaryName(mirror.getDeclaringElement()).toString() + '"';
                     final String setOfImplementedInterfaces = javaSetOfImplementedInterfaces(mirror.getType());
-                    final String scopeTypeClassReference = mirror.getElement().accept(scopeTypeVisitor, types).getQualifiedName() + ".class";
                     if (mirror instanceof ProducerMethodMirror) {
-                        return "new ProducerMethod(" + declaringClassName + ", \"" +
+                        return "new ProducerMethod(classLoader, " + declaringClassName + ", \"" +
                                 mirror.getType().toString() + "\", \"" +
                                 mirror.getElement().getSimpleName() + "\", " +
-                                setOfImplementedInterfaces + ", " +
-                                scopeTypeClassReference + ")";
+                                setOfImplementedInterfaces + ")";
                     } else if (mirror instanceof ProducerFieldMirror) {
-                        return "new ProducerField(" + declaringClassName + ", \"" +
+                        return "new ProducerField(classLoader, " + declaringClassName + ", \"" +
                                 mirror.getElement().getSimpleName() + "\", " +
-                                setOfImplementedInterfaces + ", " +
-                                scopeTypeClassReference + ")";
+                                setOfImplementedInterfaces + ")";
                     } else if (mirror instanceof InjectionTargetMirror) {
-                        return "new InjectionTarget(" + declaringClassName + ", " +
-                                setOfImplementedInterfaces + ", " +
-                                scopeTypeClassReference + ")";
+                        return "new InjectionTarget(classLoader, " + declaringClassName + ", " +
+                                setOfImplementedInterfaces + ")";
                     } else if (mirror instanceof DisposesMirror) {
-                        return "new DisposesMethod(" + declaringClassName + ", \"" +
+                        return "new DisposesMethod(classLoader, " + declaringClassName + ", \"" +
                                 elements.getBinaryName((TypeElement) types.asElement(mirror.getElement().asType())) + "\")";
                     } else if (mirror instanceof GenericPluginMirror) {
-                        return "new GenericPlugin(" + declaringClassName + ", " + setOfImplementedInterfaces + ")";
+                        return "new GenericPlugin(classLoader, " + declaringClassName + ", " +
+                                setOfImplementedInterfaces + ")";
                     } else {
                         throw new UnsupportedOperationException(mirror.getClass().getName());
                     }
                 })
-                .collect(Collectors.joining(",\n", "\n", "\n"));
+                .collect(Collectors.joining(",\n    ", "\n    ", "\n  "));
     }
 
     private String javaSetOfImplementedInterfaces(final TypeMirror base) {
diff --git a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugin/processor/BeanProcessorTest.java b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugin/processor/BeanProcessorTest.java
index 624c803..c448467 100644
--- a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugin/processor/BeanProcessorTest.java
+++ b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugin/processor/BeanProcessorTest.java
@@ -18,11 +18,14 @@
 package org.apache.logging.log4j.plugin.processor;
 
 import org.apache.logging.log4j.plugins.di.model.DisposesMethod;
-import org.apache.logging.log4j.plugins.di.model.GenericPlugin;
 import org.apache.logging.log4j.plugins.di.model.InjectionTarget;
 import org.apache.logging.log4j.plugins.di.model.PluginModule;
 import org.apache.logging.log4j.plugins.di.model.ProducerField;
+import org.apache.logging.log4j.plugins.test.validation.ExampleBean;
 import org.apache.logging.log4j.plugins.test.validation.FakePlugin;
+import org.apache.logging.log4j.plugins.test.validation.ImplicitBean;
+import org.apache.logging.log4j.plugins.test.validation.ImplicitMethodBean;
+import org.apache.logging.log4j.plugins.test.validation.ProductionBean;
 import org.apache.logging.log4j.plugins.test.validation.plugins.Log4jModule;
 import org.junit.jupiter.api.Test;
 
@@ -34,18 +37,18 @@ class BeanProcessorTest {
         final PluginModule module = new Log4jModule();
         final var plugins = module.getPluginSources();
         assertTrue(plugins.stream().anyMatch(plugin -> plugin instanceof InjectionTarget &&
-                plugin.getDeclaringClassName().equals("org.apache.logging.log4j.plugins.test.validation.ExampleBean")));
+                plugin.getDeclaringClass().equals(ExampleBean.class)));
         assertTrue(plugins.stream().anyMatch(plugin -> plugin instanceof InjectionTarget &&
-                plugin.getDeclaringClassName().equals("org.apache.logging.log4j.plugins.test.validation.ImplicitBean")));
+                plugin.getDeclaringClass().equals(ImplicitBean.class)));
         assertTrue(plugins.stream().anyMatch(plugin -> plugin instanceof InjectionTarget &&
-                plugin.getDeclaringClassName().equals("org.apache.logging.log4j.plugins.test.validation.ImplicitMethodBean")));
+                plugin.getDeclaringClass().equals(ImplicitMethodBean.class)));
         assertTrue(plugins.stream().anyMatch(plugin -> plugin instanceof InjectionTarget &&
-                plugin.getDeclaringClassName().equals("org.apache.logging.log4j.plugins.test.validation.ProductionBean$Builder")));
+                plugin.getDeclaringClass().equals(ProductionBean.Builder.class)));
         assertTrue(plugins.stream().anyMatch(plugin -> plugin instanceof ProducerField &&
-                plugin.getDeclaringClassName().equals("org.apache.logging.log4j.plugins.test.validation.ProductionBean")));
+                plugin.getDeclaringClass().equals(ProductionBean.class)));
         assertTrue(plugins.stream().anyMatch(plugin -> plugin instanceof DisposesMethod &&
-                plugin.getDeclaringClassName().equals("org.apache.logging.log4j.plugins.test.validation.ProductionBean")));
-        assertTrue(plugins.stream().anyMatch(plugin -> plugin instanceof GenericPlugin &&
-                plugin.getDeclaringClassName().equals(FakePlugin.class.getName())));
+                plugin.getDeclaringClass().equals(ProductionBean.class)));
+        assertTrue(plugins.stream().anyMatch(plugin -> plugin instanceof InjectionTarget &&
+                plugin.getDeclaringClass().equals(FakePlugin.class)));
     }
 }
diff --git a/log4j-plugins/src/main/java/module-info.java b/log4j-plugins/src/main/java/module-info.java
index 6e5d976..62fe441 100644
--- a/log4j-plugins/src/main/java/module-info.java
+++ b/log4j-plugins/src/main/java/module-info.java
@@ -34,5 +34,8 @@ module org.apache.logging.log4j.plugins {
 
     uses org.apache.logging.log4j.plugins.processor.PluginService;
     uses org.apache.logging.log4j.plugins.di.model.PluginModule;
+    uses org.apache.logging.log4j.plugins.spi.BeanManager;
+    provides org.apache.logging.log4j.plugins.spi.BeanManager with
+            org.apache.logging.log4j.plugins.spi.impl.DefaultBeanManager;
 
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterRegistry.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterRegistry.java
index 9bb2eb6..f3301ce 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterRegistry.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterRegistry.java
@@ -17,20 +17,24 @@
 package org.apache.logging.log4j.plugins.convert;
 
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.plugins.util.PluginManager;
-import org.apache.logging.log4j.plugins.util.PluginType;
+import org.apache.logging.log4j.plugins.di.model.PluginSource;
+import org.apache.logging.log4j.plugins.spi.Bean;
+import org.apache.logging.log4j.plugins.spi.BeanManager;
+import org.apache.logging.log4j.plugins.util.PluginLoader;
 import org.apache.logging.log4j.plugins.util.TypeUtil;
-import org.apache.logging.log4j.util.ReflectionUtil;
 import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.LoaderUtil;
 
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.UnknownFormatConversionException;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
 
 /**
  * Registry for {@link TypeConverter} plugins.
@@ -40,6 +44,7 @@ import java.util.concurrent.ConcurrentMap;
 public class TypeConverterRegistry {
 
     private static final Logger LOGGER = StatusLogger.getLogger();
+    private static final String DEFAULT_CONVERTERS_PACKAGE = "org.apache.logging.log4j.core.config.plugins.convert";
     private static volatile TypeConverterRegistry INSTANCE;
     private static final Object INSTANCE_LOCK = new Object();
 
@@ -107,22 +112,37 @@ public class TypeConverterRegistry {
 
     private TypeConverterRegistry() {
         LOGGER.trace("TypeConverterRegistry initializing.");
-        final PluginManager manager = new PluginManager(TypeConverters.CATEGORY);
-        manager.collectPlugins();
-        loadKnownTypeConverters(manager.getPlugins().values());
+        // TODO: this should eventually be refactored to load TypeConverter instances on demand via BeanManager
+        //  (can be done after AbstractConfigurationBinder is removed; can then combine TypeConverters with rest of beans)
+        loadKnownTypeConverters();
         registerPrimitiveTypes();
     }
 
-    private void loadKnownTypeConverters(final Collection<PluginType<?>> knownTypes) {
-        for (final PluginType<?> knownType : knownTypes) {
-            final Class<?> clazz = knownType.getPluginClass();
-            if (TypeConverter.class.isAssignableFrom(clazz)) {
-                @SuppressWarnings("rawtypes")
-                final Class<? extends TypeConverter> pluginClass =  clazz.asSubclass(TypeConverter.class);
-                final Type conversionType = getTypeConverterSupportedType(pluginClass);
-                final TypeConverter<?> converter = ReflectionUtil.instantiate(pluginClass);
-                registerConverter(conversionType, converter);
-            }
+    private void loadKnownTypeConverters() {
+        final BeanManager beanManager = BeanManager.getInstance();
+        final Set<PluginSource> typeConverterPlugins = PluginLoader.loadPluginSourcesFromMainClassLoader()
+                .stream()
+                .filter(pluginSource -> pluginSource.getImplementedInterfaces().contains(TypeConverter.class))
+                .collect(Collectors.toSet());
+        final Collection<Bean<?>> typeConverterBeans;
+        if (typeConverterPlugins.isEmpty()) {
+            // retry with a classpath scan
+            LOGGER.warn("Unable to load PluginModule metadata; falling back to scan of classpath");
+            typeConverterBeans = beanManager.scanAndLoadBeans(LoaderUtil.getClassLoader(), DEFAULT_CONVERTERS_PACKAGE)
+                    .stream()
+                    .filter(bean -> bean.hasMatchingType(TypeConverter.class))
+                    .collect(Collectors.toSet());
+        } else {
+            typeConverterBeans = beanManager.loadBeansFromPluginSources(typeConverterPlugins);
+        }
+        beanManager.validateBeans(typeConverterBeans);
+        final var initializationContext = beanManager.createInitializationContext(null);
+        for (final Bean<?> typeConverterBean : typeConverterBeans) {
+            final Bean<TypeConverter<?>> bean = TypeUtil.cast(typeConverterBean);
+            final Class<? extends TypeConverter<?>> pluginClass = TypeUtil.cast(bean.getDeclaringClass());
+            final Type conversionType = getTypeConverterSupportedType(pluginClass);
+            final TypeConverter<?> converter = beanManager.getValue(bean, initializationContext);
+            registerConverter(conversionType, converter);
         }
     }
 
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/DisposesMethod.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/DisposesMethod.java
index 02175cb..2dbfca1 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/DisposesMethod.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/DisposesMethod.java
@@ -17,37 +17,32 @@
 
 package org.apache.logging.log4j.plugins.di.model;
 
-import org.apache.logging.log4j.plugins.di.Disposes;
+import org.apache.logging.log4j.plugins.util.PluginUtil;
+import org.apache.logging.log4j.plugins.util.Value;
 
-import java.lang.annotation.Annotation;
 import java.util.Set;
 
 public class DisposesMethod implements PluginSource {
-    private final String declaringClassName;
-    private final String disposesTypeName;
+    private final Value<Class<?>> disposesType;
+    private final Value<Class<?>> declaringClass;
 
-    public DisposesMethod(final String declaringClassName, final String disposesTypeName) {
-        this.declaringClassName = declaringClassName;
-        this.disposesTypeName = disposesTypeName;
+    public DisposesMethod(
+            final ClassLoader classLoader, final String declaringClassName, final String disposesTypeName) {
+        disposesType = PluginUtil.lazyLoadClass(classLoader, disposesTypeName);
+        declaringClass = PluginUtil.lazyLoadClass(classLoader, declaringClassName);
     }
 
     @Override
-    public String getDeclaringClassName() {
-        return declaringClassName;
+    public Class<?> getDeclaringClass() {
+        return declaringClass.get();
     }
 
-    public String getDisposesTypeName() {
-        return disposesTypeName;
+    public Class<?> getDisposesType() {
+        return disposesType.get();
     }
 
     @Override
     public Set<Class<?>> getImplementedInterfaces() {
         return Set.of();
     }
-
-    @Override
-    public Class<? extends Annotation> getScopeType() {
-        // not a real scope
-        return Disposes.class;
-    }
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/GenericPlugin.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/GenericPlugin.java
index 3bff77c..27b95a0 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/GenericPlugin.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/GenericPlugin.java
@@ -17,32 +17,28 @@
 
 package org.apache.logging.log4j.plugins.di.model;
 
-import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.util.PluginUtil;
+import org.apache.logging.log4j.plugins.util.Value;
 
-import java.lang.annotation.Annotation;
 import java.util.Set;
 
 public class GenericPlugin implements PluginSource {
-    private final String declaringClassName;
+    private final Value<Class<?>> declaringClass;
     private final Set<Class<?>> implementedInterfaces;
 
-    public GenericPlugin(final String declaringClassName, final Set<Class<?>> implementedInterfaces) {
-        this.declaringClassName = declaringClassName;
+    public GenericPlugin(
+            final ClassLoader classLoader, final String declaringClassName, final Set<Class<?>> implementedInterfaces) {
+        declaringClass = PluginUtil.lazyLoadClass(classLoader, declaringClassName);
         this.implementedInterfaces = implementedInterfaces;
     }
 
     @Override
-    public String getDeclaringClassName() {
-        return declaringClassName;
+    public Class<?> getDeclaringClass() {
+        return declaringClass.get();
     }
 
     @Override
     public Set<Class<?>> getImplementedInterfaces() {
         return implementedInterfaces;
     }
-
-    @Override
-    public Class<? extends Annotation> getScopeType() {
-        return Plugin.class;
-    }
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/InjectionTarget.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/InjectionTarget.java
index a6676e5..20b76ba 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/InjectionTarget.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/InjectionTarget.java
@@ -17,33 +17,28 @@
 
 package org.apache.logging.log4j.plugins.di.model;
 
-import java.lang.annotation.Annotation;
+import org.apache.logging.log4j.plugins.util.PluginUtil;
+import org.apache.logging.log4j.plugins.util.Value;
+
 import java.util.Set;
 
 public class InjectionTarget implements PluginSource {
-    private final String className;
+    private final Value<Class<?>> declaringClass;
     private final Set<Class<?>> implementedInterfaces;
-    private final Class<? extends Annotation> scopeType;
 
-    public InjectionTarget(final String className, final Set<Class<?>> implementedInterfaces,
-                           final Class<? extends Annotation> scopeType) {
-        this.className = className;
+    public InjectionTarget(
+            final ClassLoader classLoader, final String className, final Set<Class<?>> implementedInterfaces) {
+        declaringClass = PluginUtil.lazyLoadClass(classLoader, className);
         this.implementedInterfaces = implementedInterfaces;
-        this.scopeType = scopeType;
     }
 
     @Override
-    public String getDeclaringClassName() {
-        return className;
+    public Class<?> getDeclaringClass() {
+        return declaringClass.get();
     }
 
     @Override
     public Set<Class<?>> getImplementedInterfaces() {
         return implementedInterfaces;
     }
-
-    @Override
-    public Class<? extends Annotation> getScopeType() {
-        return scopeType;
-    }
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/PluginModule.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/PluginModule.java
index 70b7093..acbce13 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/PluginModule.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/PluginModule.java
@@ -19,14 +19,6 @@ package org.apache.logging.log4j.plugins.di.model;
 
 import java.util.List;
 
-public abstract class PluginModule {
-    private final List<PluginSource> pluginSources;
-
-    protected PluginModule(final List<PluginSource> pluginSources) {
-        this.pluginSources = pluginSources;
-    }
-
-    public List<PluginSource> getPluginSources() {
-        return pluginSources;
-    }
+public interface PluginModule {
+    List<PluginSource> getPluginSources();
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/PluginSource.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/PluginSource.java
index c144f63..dd7325e 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/PluginSource.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/PluginSource.java
@@ -17,13 +17,10 @@
 
 package org.apache.logging.log4j.plugins.di.model;
 
-import java.lang.annotation.Annotation;
 import java.util.Set;
 
 public interface PluginSource {
-    String getDeclaringClassName();
+    Class<?> getDeclaringClass();
 
     Set<Class<?>> getImplementedInterfaces();
-
-    Class<? extends Annotation> getScopeType();
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/ProducerField.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/ProducerField.java
index acfc542..58f3d22 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/ProducerField.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/ProducerField.java
@@ -17,26 +17,27 @@
 
 package org.apache.logging.log4j.plugins.di.model;
 
-import java.lang.annotation.Annotation;
+import org.apache.logging.log4j.plugins.util.PluginUtil;
+import org.apache.logging.log4j.plugins.util.Value;
+
 import java.util.Set;
 
 public class ProducerField implements PluginSource {
-    private final String declaringClassName;
+    private final Value<Class<?>> declaringClass;
     private final String fieldName;
     private final Set<Class<?>> implementedInterfaces;
-    private final Class<? extends Annotation> scopeType;
 
-    public ProducerField(final String declaringClassName, final String fieldName,
-                         final Set<Class<?>> implementedInterfaces, final Class<? extends Annotation> scopeType) {
-        this.declaringClassName = declaringClassName;
+    public ProducerField(
+            final ClassLoader classLoader, final String declaringClassName, final String fieldName,
+            final Set<Class<?>> implementedInterfaces) {
+        declaringClass = PluginUtil.lazyLoadClass(classLoader, declaringClassName);
         this.fieldName = fieldName;
         this.implementedInterfaces = implementedInterfaces;
-        this.scopeType = scopeType;
     }
 
     @Override
-    public String getDeclaringClassName() {
-        return declaringClassName;
+    public Class<?> getDeclaringClass() {
+        return declaringClass.get();
     }
 
     public String getFieldName() {
@@ -47,9 +48,4 @@ public class ProducerField implements PluginSource {
     public Set<Class<?>> getImplementedInterfaces() {
         return implementedInterfaces;
     }
-
-    @Override
-    public Class<? extends Annotation> getScopeType() {
-        return scopeType;
-    }
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/ProducerMethod.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/ProducerMethod.java
index 5c836b4..5d253f5 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/ProducerMethod.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/model/ProducerMethod.java
@@ -17,33 +17,33 @@
 
 package org.apache.logging.log4j.plugins.di.model;
 
-import java.lang.annotation.Annotation;
+import org.apache.logging.log4j.plugins.util.PluginUtil;
+import org.apache.logging.log4j.plugins.util.Value;
+
 import java.util.Set;
 
 public class ProducerMethod implements PluginSource {
-    private final String declaringClassName;
-    private final String returnTypeClassName;
+    private final Value<Class<?>> declaringClass;
+    private final Value<Class<?>> returnType;
     private final String methodName;
     private final Set<Class<?>> implementedInterfaces;
-    private final Class<? extends Annotation> scopeType;
 
-    public ProducerMethod(final String declaringClassName, final String returnTypeClassName,
-                          final String methodName, final Set<Class<?>> implementedInterfaces,
-                          final Class<? extends Annotation> scopeType) {
-        this.declaringClassName = declaringClassName;
-        this.returnTypeClassName = returnTypeClassName;
+    public ProducerMethod(
+            final ClassLoader classLoader, final String declaringClassName, final String returnTypeClassName,
+            final String methodName, final Set<Class<?>> implementedInterfaces) {
+        declaringClass = PluginUtil.lazyLoadClass(classLoader, declaringClassName);
+        returnType = PluginUtil.lazyLoadClass(classLoader, returnTypeClassName);
         this.methodName = methodName;
         this.implementedInterfaces = implementedInterfaces;
-        this.scopeType = scopeType;
     }
 
     @Override
-    public String getDeclaringClassName() {
-        return declaringClassName;
+    public Class<?> getDeclaringClass() {
+        return declaringClass.get();
     }
 
-    public String getReturnTypeClassName() {
-        return returnTypeClassName;
+    public Class<?> getReturnType() {
+        return returnType.get();
     }
 
     public String getMethodName() {
@@ -54,9 +54,4 @@ public class ProducerMethod implements PluginSource {
     public Set<Class<?>> getImplementedInterfaces() {
         return implementedInterfaces;
     }
-
-    @Override
-    public Class<? extends Annotation> getScopeType() {
-        return scopeType;
-    }
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/BeanManager.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/BeanManager.java
index 05b8f13..6945403 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/BeanManager.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/BeanManager.java
@@ -20,6 +20,7 @@ package org.apache.logging.log4j.plugins.spi;
 import org.apache.logging.log4j.plugins.di.Inject;
 import org.apache.logging.log4j.plugins.di.Producer;
 import org.apache.logging.log4j.plugins.di.Qualifier;
+import org.apache.logging.log4j.plugins.di.model.PluginSource;
 import org.apache.logging.log4j.plugins.name.AnnotatedElementNameProvider;
 import org.apache.logging.log4j.plugins.util.AnnotationUtil;
 import org.apache.logging.log4j.util.Strings;
@@ -35,14 +36,23 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
+import java.util.ServiceLoader;
 
 /**
  * Central SPI for injecting and managing beans and their instances.
  */
-// TODO: move this whole API to log4j-plugins
 public interface BeanManager extends AutoCloseable {
 
     /**
+     * Gets the configured BeanManager service for loading and unloading beans.
+     */
+    static BeanManager getInstance() {
+        return ServiceLoader.load(BeanManager.class, BeanManager.class.getClassLoader())
+                .findFirst()
+                .orElseThrow(() -> new InitializationException("No BeanManager service available"));
+    }
+
+    /**
      * Loads beans from the given classes. This looks for injectable classes and producers in the provided classes,
      * loads them into this manager, and returns the loaded beans.
      *
@@ -52,6 +62,24 @@ public interface BeanManager extends AutoCloseable {
     Collection<Bean<?>> loadBeans(final Collection<Class<?>> beanClasses);
 
     /**
+     * Scans the classpath in a given package for beans and loads bean metadata from discovered classes.
+     *
+     * @param classLoader ClassLoader to use for loading classes from package or null to use the default context ClassLoader
+     * @param packageName package name to begin search for beans inside
+     * @return beans loaded from the given package
+     */
+    Collection<Bean<?>> scanAndLoadBeans(final ClassLoader classLoader, final String packageName);
+
+    /**
+     * Loads beans from the given {@link PluginSource} instances. PluginSources are generated by the plugins annotation
+     * processor to help avoid classpath scanning at runtime.
+     *
+     * @param pluginSources collection of PluginSource instances to load bean metadata from
+     * @return beans loaded from the given sources
+     */
+    Collection<Bean<?>> loadBeansFromPluginSources(final Collection<PluginSource> pluginSources);
+
+    /**
      * Creates a bean for an injectable bean class.
      */
     <T> Bean<T> createBean(final Class<T> beanClass);
@@ -255,6 +283,8 @@ public interface BeanManager extends AutoCloseable {
     @Override
     void close();
 
+    // TODO: API to register ScopeContexts
+    // TODO: Configuration APIs
     // TODO: integrate with constraint validators
     // TODO: integrate with TypeConverters
     // TODO: need some sort of default value strategy to bridge over @PluginAttribute and optional injected values
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/InitializationException.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/InitializationException.java
index d547f8d..8c11613 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/InitializationException.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/InitializationException.java
@@ -18,6 +18,10 @@
 package org.apache.logging.log4j.plugins.spi;
 
 public class InitializationException extends InjectionException {
+    public InitializationException(final String message) {
+        super(message);
+    }
+
     public InitializationException(final String message, final Throwable cause) {
         super(message, cause);
     }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/impl/DefaultBeanManager.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/impl/DefaultBeanManager.java
index 78b9439..90b3e0d 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/impl/DefaultBeanManager.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/spi/impl/DefaultBeanManager.java
@@ -17,6 +17,15 @@
 
 package org.apache.logging.log4j.plugins.spi.impl;
 
+import org.apache.logging.log4j.plugins.di.DependentScoped;
+import org.apache.logging.log4j.plugins.di.Disposes;
+import org.apache.logging.log4j.plugins.di.Producer;
+import org.apache.logging.log4j.plugins.di.Provider;
+import org.apache.logging.log4j.plugins.di.ScopeType;
+import org.apache.logging.log4j.plugins.di.SingletonScoped;
+import org.apache.logging.log4j.plugins.di.model.PluginSource;
+import org.apache.logging.log4j.plugins.name.AnnotatedElementAliasesProvider;
+import org.apache.logging.log4j.plugins.name.AnnotatedElementNameProvider;
 import org.apache.logging.log4j.plugins.spi.AmbiguousBeanException;
 import org.apache.logging.log4j.plugins.spi.Bean;
 import org.apache.logging.log4j.plugins.spi.BeanManager;
@@ -30,17 +39,11 @@ import org.apache.logging.log4j.plugins.spi.ResolutionException;
 import org.apache.logging.log4j.plugins.spi.ScopeContext;
 import org.apache.logging.log4j.plugins.spi.UnsatisfiedBeanException;
 import org.apache.logging.log4j.plugins.spi.ValidationException;
-import org.apache.logging.log4j.plugins.di.DependentScoped;
-import org.apache.logging.log4j.plugins.di.Disposes;
-import org.apache.logging.log4j.plugins.di.Producer;
-import org.apache.logging.log4j.plugins.di.Provider;
-import org.apache.logging.log4j.plugins.di.ScopeType;
-import org.apache.logging.log4j.plugins.di.SingletonScoped;
-import org.apache.logging.log4j.plugins.name.AnnotatedElementAliasesProvider;
-import org.apache.logging.log4j.plugins.name.AnnotatedElementNameProvider;
 import org.apache.logging.log4j.plugins.util.AnnotationUtil;
 import org.apache.logging.log4j.plugins.util.LazyValue;
+import org.apache.logging.log4j.plugins.util.ResolverUtil;
 import org.apache.logging.log4j.plugins.util.TypeUtil;
+import org.apache.logging.log4j.util.Strings;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.AnnotatedElement;
@@ -51,18 +54,20 @@ import java.lang.reflect.Parameter;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.lang.reflect.TypeVariable;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -70,20 +75,25 @@ public class DefaultBeanManager implements BeanManager {
 
     private final Injector injector = new Injector(this);
 
-    private final Collection<Bean<?>> enabledBeans = new HashSet<>();
+    private final Collection<Bean<?>> enabledBeans = ConcurrentHashMap.newKeySet();
     private final Map<Type, Collection<Bean<?>>> beansByType = new ConcurrentHashMap<>();
     private final Collection<DisposesMethod> disposesMethods = new ArrayList<>();
     private final Map<Class<? extends Annotation>, ScopeContext> scopes = new LinkedHashMap<>();
 
     public DefaultBeanManager() {
         // TODO: need a better way to register scope contexts
+        // TODO: need ThreadLocalScopeContext for LoggerContext (~ContextSelector) and ConfigurationContext scopes
+        //  (can potentially modify LoggerContext[Factory] to set these thread local values on construction et al.
         scopes.put(DependentScoped.class, new DependentScopeContext());
         scopes.put(SingletonScoped.class, new DefaultScopeContext(SingletonScoped.class));
     }
 
     @Override
     public Collection<Bean<?>> loadBeans(final Collection<Class<?>> beanClasses) {
-        final Collection<Bean<?>> loadedBeans = new HashSet<>();
+        if (beanClasses.isEmpty()) {
+            return Set.of();
+        }
+        final Collection<Bean<?>> loadedBeans = new LinkedHashSet<>();
         for (final Class<?> beanClass : beanClasses) {
             final Bean<?> bean = isInjectable(beanClass) ? createBean(beanClass) : null;
             loadDisposerMethods(beanClass, bean);
@@ -111,6 +121,32 @@ public class DefaultBeanManager implements BeanManager {
     }
 
     @Override
+    public Collection<Bean<?>> scanAndLoadBeans(final ClassLoader classLoader, final String packageName) {
+        if (Strings.isBlank(packageName)) {
+            return Set.of();
+        }
+        final ResolverUtil resolver = new ResolverUtil();
+        if (classLoader != null) {
+            resolver.setClassLoader(classLoader);
+        }
+        resolver.findInPackage(new BeanTest(this::isInjectable), packageName);
+        return loadBeans(resolver.getClasses());
+    }
+
+    @Override
+    public Collection<Bean<?>> loadBeansFromPluginSources(final Collection<PluginSource> pluginSources) {
+        if (pluginSources.isEmpty()) {
+            return Set.of();
+        }
+        // TODO: enhance PluginSource metadata to support lazy loading of classes in respective beans for implemented interfaces
+        final Set<Class<?>> beanClasses = pluginSources.stream()
+                .map(PluginSource::getDeclaringClass)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        return loadBeans(beanClasses);
+    }
+
+    @Override
     public <T> Bean<T> createBean(final Class<T> beanClass) {
         final Collection<Type> types = TypeUtil.getTypeClosure(beanClass);
         final String name = AnnotatedElementNameProvider.getName(beanClass);
@@ -172,12 +208,11 @@ public class DefaultBeanManager implements BeanManager {
     }
 
     private void loadDisposerMethods(final Class<?> beanClass, final Bean<?> bean) {
-        for (final Method method : beanClass.getDeclaredMethods()) {
+        for (final Method method : beanClass.getMethods()) {
             for (final Parameter parameter : method.getParameters()) {
                 if (parameter.isAnnotationPresent(Disposes.class)) {
                     final String name = AnnotatedElementNameProvider.getName(parameter);
                     final Collection<String> aliases = AnnotatedElementAliasesProvider.getAliases(parameter);
-                    method.setAccessible(true);
                     disposesMethods.add(new DisposesMethod(parameter.getParameterizedType(), name, aliases, bean, method));
                 }
             }
@@ -423,6 +458,43 @@ public class DefaultBeanManager implements BeanManager {
         }
     }
 
+    private static class BeanTest implements ResolverUtil.Test {
+        private final Predicate<Class<?>> isBeanClass;
+
+        private BeanTest(final Predicate<Class<?>> isBeanClass) {
+            this.isBeanClass = isBeanClass;
+        }
+
+        @Override
+        public boolean matches(final Class<?> type) {
+            if (isBeanClass.test(type)) {
+                return true;
+            }
+            for (final Annotation annotation : type.getAnnotations()) {
+                final String name = annotation.annotationType().getName();
+                if (name.startsWith("org.apache.logging.log4j.") && name.endsWith(".plugins.Plugin")) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean matches(final URI resource) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean doesMatchClass() {
+            return true;
+        }
+
+        @Override
+        public boolean doesMatchResource() {
+            return false;
+        }
+    }
+
     private static class DisposesMethod {
         private final Type type;
         private final String name;
@@ -455,6 +527,22 @@ public class DefaultBeanManager implements BeanManager {
         private boolean matchesName(final String name) {
             return this.name.equalsIgnoreCase(name) || aliases.stream().anyMatch(name::equalsIgnoreCase);
         }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o)
+                return true;
+            if (o == null || getClass() != o.getClass())
+                return false;
+            final DisposesMethod that = (DisposesMethod) o;
+            return type.equals(that.type) && name.equals(that.name) && aliases.equals(that.aliases) && Objects.equals(
+                    declaringBean, that.declaringBean) && disposesMethod.equals(that.disposesMethod);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(type, name, aliases, declaringBean, disposesMethod);
+        }
     }
 
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginLoader.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginLoader.java
new file mode 100644
index 0000000..3815c26
--- /dev/null
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginLoader.java
@@ -0,0 +1,60 @@
+/*
+ * 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.util;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.plugins.di.model.PluginModule;
+import org.apache.logging.log4j.plugins.di.model.PluginSource;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.LoaderUtil;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ServiceLoader;
+
+public class PluginLoader {
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    public static List<PluginSource> loadPluginSourcesFromMainClassLoader() {
+        final List<PluginSource> sources = new ArrayList<>();
+        for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
+            sources.addAll(loadPluginSources(classLoader));
+        }
+        return sources;
+    }
+
+    public static List<PluginSource> loadPluginSources(final ClassLoader classLoader) {
+        final long startTime = System.nanoTime();
+        final ServiceLoader<PluginModule> serviceLoader = ServiceLoader.load(PluginModule.class, classLoader);
+        final List<PluginSource> sources = new ArrayList<>();
+        for (final PluginModule module : serviceLoader) {
+            sources.addAll(module.getPluginSources());
+        }
+        final int numPlugins = sources.size();
+        LOGGER.debug(() -> {
+            final long endTime = System.nanoTime();
+            final DecimalFormat numFormat = new DecimalFormat("#0.000000");
+            return "Took " + numFormat.format((endTime - startTime) * 1e-9) +
+                    " seconds to load " + numPlugins +
+                    " plugins from " + classLoader;
+        });
+        return sources;
+    }
+
+}
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginUtil.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginUtil.java
index feb7deb..1bcb952 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginUtil.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginUtil.java
@@ -18,6 +18,7 @@ package org.apache.logging.log4j.plugins.util;
 
 import org.apache.logging.log4j.plugins.PluginFactory;
 
+import java.lang.ref.WeakReference;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -93,4 +94,18 @@ public final class PluginUtil {
         throw new IllegalStateException("no factory method found for class " + pluginClass);
     }
 
+    public static Value<Class<?>> lazyLoadClass(final ClassLoader classLoader, final String className) {
+        final var classLoaderRef = new WeakReference<>(classLoader);
+        return WeakLazyValue.forSupplier(() -> {
+            final ClassLoader loader = classLoaderRef.get();
+            if (loader != null) {
+                try {
+                    return loader.loadClass(className);
+                } catch (ClassNotFoundException ignored) {
+                }
+            }
+            return null;
+        });
+    }
+
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/TypeUtil.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/TypeUtil.java
index adde765..685f882 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/TypeUtil.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/TypeUtil.java
@@ -16,7 +16,6 @@
  */
 package org.apache.logging.log4j.plugins.util;
 
-import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Array;
 import java.lang.reflect.Field;
 import java.lang.reflect.GenericArrayType;
@@ -29,9 +28,11 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * Utility class for working with Java {@link Type}s and derivatives. This class is adapted heavily from the
@@ -535,4 +536,12 @@ public final class TypeUtil {
         return new ParameterizedTypeImpl(null, rawType, typeArguments);
     }
 
+    public static Set<Class<?>> getImplementedInterfaces(final Class<?> type) {
+        final Set<Class<?>> interfaces = new LinkedHashSet<>(List.of(type.getInterfaces()));
+        for (Class<?> superclass = type.getSuperclass(); superclass != null; superclass = superclass.getSuperclass()) {
+            interfaces.addAll(List.of(superclass.getInterfaces()));
+        }
+        return interfaces;
+    }
+
 }
diff --git a/log4j-plugins/src/main/resources/META-INF/services/org.apache.logging.log4j.plugins.spi.BeanManager b/log4j-plugins/src/main/resources/META-INF/services/org.apache.logging.log4j.plugins.spi.BeanManager
new file mode 100644
index 0000000..98c32ea
--- /dev/null
+++ b/log4j-plugins/src/main/resources/META-INF/services/org.apache.logging.log4j.plugins.spi.BeanManager
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.apache.logging.log4j.plugins.spi.impl.DefaultBeanManager