diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index a9c423c9d96..3875cd61bc5 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -869,6 +869,9 @@
                 -Alog4j.docgen.version=${project.version}
                 -Alog4j.docgen.description=${project.description}
                 -Alog4j.docgen.typeFilter.excludePattern=${log4j.docgen.typeFilter.excludePattern}
+                
+                -Alog4j.graalvm.groupId=${project.groupId}
+                -Alog4j.graalvm.artifactId=${project.artifactId}
               
             
 
diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java
new file mode 100644
index 00000000000..0f67a29f898
--- /dev/null
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java
@@ -0,0 +1,355 @@
+/*
+ * 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.plugin.processor;
+
+import aQute.bnd.annotation.Resolution;
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedOptions;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.SimpleElementVisitor8;
+import javax.lang.model.util.SimpleTypeVisitor8;
+import javax.tools.Diagnostic;
+import javax.tools.StandardLocation;
+import org.apache.logging.log4j.plugin.processor.internal.Annotations;
+import org.apache.logging.log4j.plugin.processor.internal.ReachabilityMetadata;
+import org.apache.logging.log4j.util.Strings;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Java annotation processor that generates GraalVM metadata.
+ * 
+ *     Note: The annotations listed here must also be classified by the {@link Annotations} helper.
+ * 
+ */
+@ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL)
+@SupportedAnnotationTypes({
+    "org.apache.logging.log4j.plugins.Factory",
+    "org.apache.logging.log4j.plugins.PluginFactory",
+    "org.apache.logging.log4j.plugins.SingletonFactory",
+    "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory",
+    "org.apache.logging.log4j.core.config.plugins.PluginFactory",
+    "org.apache.logging.log4j.plugins.Inject",
+    "org.apache.logging.log4j.plugins.Named",
+    "org.apache.logging.log4j.plugins.PluginAttribute",
+    "org.apache.logging.log4j.plugins.PluginBuilderAttribute",
+    "org.apache.logging.log4j.plugins.PluginElement",
+    "org.apache.logging.log4j.plugins.PluginNode",
+    "org.apache.logging.log4j.plugins.PluginValue",
+    "org.apache.logging.log4j.core.config.plugins.PluginAttribute",
+    "org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute",
+    "org.apache.logging.log4j.core.config.plugins.PluginConfiguration",
+    "org.apache.logging.log4j.core.config.plugins.PluginElement",
+    "org.apache.logging.log4j.core.config.plugins.PluginLoggerContext",
+    "org.apache.logging.log4j.core.config.plugins.PluginNode",
+    "org.apache.logging.log4j.core.config.plugins.PluginValue",
+    "org.apache.logging.log4j.plugins.Plugin",
+    "org.apache.logging.log4j.core.config.plugins.Plugin",
+    "org.apache.logging.log4j.plugins.condition.Conditional",
+    "org.apache.logging.log4j.plugins.validation.Constraint"
+})
+@SupportedOptions({"log4j.graalvm.groupId", "log4j.graalvm.artifactId"})
+public class GraalVmProcessor extends AbstractProcessor {
+
+    static final String GROUP_ID = "log4j.graalvm.groupId";
+    static final String ARTIFACT_ID = "log4j.graalvm.artifactId";
+    private static final String LOCATION_PREFIX = "META-INF/native-image/log4j-generated/";
+    private static final String LOCATION_SUFFIX = "/reflect-config.json";
+    private static final String PROCESSOR_NAME = GraalVmProcessor.class.getSimpleName();
+
+    private final Map reachableTypes = new HashMap<>();
+    private final List processedElements = new ArrayList<>();
+    private Annotations annotationUtil;
+
+    @Override
+    public synchronized void init(ProcessingEnvironment processingEnv) {
+        super.init(processingEnv);
+        this.annotationUtil = new Annotations(processingEnv.getElementUtils());
+    }
+
+    @Override
+    public SourceVersion getSupportedSourceVersion() {
+        return SourceVersion.latest();
+    }
+
+    @Override
+    public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
+        Messager messager = processingEnv.getMessager();
+        for (TypeElement annotation : annotations) {
+            Annotations.Type annotationType = annotationUtil.classifyAnnotation(annotation);
+            for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
+                switch (annotationType) {
+                    case INJECT:
+                        processInject(element);
+                        break;
+                    case PLUGIN:
+                        processPlugin(element);
+                        break;
+                    case META_ANNOTATION_STRATEGY:
+                        processMetaAnnotationStrategy(element, annotation);
+                        break;
+                    case QUALIFIER:
+                        processQualifier(element);
+                        break;
+                    case FACTORY:
+                        processFactory(element);
+                        break;
+                    case UNKNOWN:
+                        messager.printMessage(
+                                Diagnostic.Kind.WARNING,
+                                String.format(
+                                        "The annotation type `%s` is not handled by %s", annotation, PROCESSOR_NAME),
+                                annotation);
+                }
+                processedElements.add(element);
+            }
+        }
+        // Write the result file
+        if (roundEnv.processingOver() && !reachableTypes.isEmpty()) {
+            writeReachabilityMetadata();
+        }
+        // Do not claim the annotations to allow other annotation processors to run
+        return false;
+    }
+
+    private void processInject(Element element) {
+        if (element instanceof ExecutableElement executableElement) {
+            var parent = safeCast(executableElement.getEnclosingElement(), TypeElement.class);
+            addMethod(parent, executableElement);
+        } else if (element instanceof VariableElement variableElement) {
+            var parent = safeCast(variableElement.getEnclosingElement(), TypeElement.class);
+            addField(parent, variableElement);
+        }
+    }
+
+    private void processPlugin(Element element) {
+        TypeElement typeElement = safeCast(element, TypeElement.class);
+        for (Element child : typeElement.getEnclosedElements()) {
+            if (child instanceof ExecutableElement executableChild) {
+                if (executableChild.getModifiers().contains(Modifier.PUBLIC)) {
+                    switch (executableChild.getSimpleName().toString()) {
+                        // 1. All public constructors.
+                        case "":
+                            addMethod(typeElement, executableChild);
+                            break;
+                        // 2. Static `newInstance` method used in, e.g. `PatternConverter` classes.
+                        case "newInstance":
+                            if (executableChild.getModifiers().contains(Modifier.STATIC)) {
+                                addMethod(typeElement, executableChild);
+                            }
+                            break;
+                        // 3. Other factory methods are annotated, so we don't deal with them here.
+                        default:
+                    }
+                }
+            }
+        }
+    }
+
+    private void processMetaAnnotationStrategy(Element element, TypeElement annotation) {
+        // Add the metadata for the public constructors
+        processPlugin(annotationUtil.getAnnotationClassValue(element, annotation));
+    }
+
+    private void processQualifier(Element element) {
+        if (element.getKind() == ElementKind.FIELD) {
+            addField(
+                    safeCast(element.getEnclosingElement(), TypeElement.class),
+                    safeCast(element, VariableElement.class));
+        }
+    }
+
+    private void processFactory(Element element) {
+        addMethod(
+                safeCast(element.getEnclosingElement(), TypeElement.class), safeCast(element, ExecutableElement.class));
+    }
+
+    private void writeReachabilityMetadata() {
+        // Compute the reachability metadata
+        ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
+        try {
+            ReachabilityMetadata.writeReflectConfig(reachableTypes.values(), arrayOutputStream);
+        } catch (IOException e) {
+            String message = String.format(
+                    "%s: an error occurred while generating reachability metadata: %s", PROCESSOR_NAME, e.getMessage());
+            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message);
+            return;
+        }
+        byte[] data = arrayOutputStream.toByteArray();
+
+        Map options = processingEnv.getOptions();
+        String reachabilityMetadataPath = getReachabilityMetadataPath(
+                options.get(GROUP_ID), options.get(ARTIFACT_ID), Integer.toHexString(Arrays.hashCode(data)));
+        Messager messager = processingEnv.getMessager();
+        messager.printMessage(
+                Diagnostic.Kind.NOTE,
+                String.format(
+                        "%s: writing GraalVM metadata for %d Java classes to `%s`.",
+                        PROCESSOR_NAME, reachableTypes.size(), reachabilityMetadataPath));
+        try (OutputStream output = processingEnv
+                .getFiler()
+                .createResource(
+                        StandardLocation.CLASS_OUTPUT,
+                        Strings.EMPTY,
+                        reachabilityMetadataPath,
+                        processedElements.toArray(Element[]::new))
+                .openOutputStream()) {
+            output.write(data);
+        } catch (IOException e) {
+            String message = String.format(
+                    "%s: unable to write reachability metadata to file `%s`", PROCESSOR_NAME, reachabilityMetadataPath);
+            messager.printMessage(Diagnostic.Kind.ERROR, message);
+            throw new IllegalArgumentException(message, e);
+        }
+    }
+
+    /**
+     * Returns the path to the reachability metadata file.
+     * 
+     *     If the groupId or artifactId is not specified, a warning is printed and a fallback folder name is used.
+     *     The fallback folder name should be reproducible, but unique enough to avoid conflicts.
+     * 
+     *
+     * @param groupId The group ID of the plugin.
+     * @param artifactId The artifact ID of the plugin.
+     * @param fallbackFolderName The fallback folder name to use if groupId or artifactId is not specified.
+     */
+    String getReachabilityMetadataPath(
+            @Nullable String groupId, @Nullable String artifactId, String fallbackFolderName) {
+        if (groupId == null || artifactId == null) {
+            String message = String.format(
+                    "The `%1$s` annotation processor is missing the recommended `%2$s` and `%3$s` options.%n"
+                            + "To follow the GraalVM recommendations, please add the following options to your build tool:%n"
+                            + "  -A%2$s=%n"
+                            + "  -A%3$s=%n",
+                    PROCESSOR_NAME, GROUP_ID, ARTIFACT_ID);
+            processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message);
+            return LOCATION_PREFIX + fallbackFolderName + LOCATION_SUFFIX;
+        }
+        return LOCATION_PREFIX + groupId + '/' + artifactId + LOCATION_SUFFIX;
+    }
+
+    private void addField(TypeElement parent, VariableElement element) {
+        ReachabilityMetadata.Type reachableType =
+                reachableTypes.computeIfAbsent(toString(parent), ReachabilityMetadata.Type::new);
+        reachableType.addField(
+                new ReachabilityMetadata.Field(element.getSimpleName().toString()));
+    }
+
+    private void addMethod(TypeElement parent, ExecutableElement element) {
+        ReachabilityMetadata.Type reachableType =
+                reachableTypes.computeIfAbsent(toString(parent), ReachabilityMetadata.Type::new);
+        ReachabilityMetadata.Method method =
+                new ReachabilityMetadata.Method(element.getSimpleName().toString());
+        element.getParameters().stream().map(v -> toString(v.asType())).forEach(method::addParameterType);
+        reachableType.addMethod(method);
+    }
+
+    private  T safeCast(Element element, Class type) {
+        if (type.isInstance(element)) {
+            return type.cast(element);
+        }
+        // This should never happen, unless annotations start appearing on unexpected elements.
+        String msg = String.format(
+                "Unexpected type of element `%s`: expecting `%s` but found `%s`",
+                element, type.getName(), element.getClass().getName());
+        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element);
+        throw new IllegalStateException(msg);
+    }
+
+    /**
+     * Returns the fully qualified name of a type.
+     *
+     * @param type A Java type.
+     */
+    private String toString(TypeMirror type) {
+        return type.accept(
+                new SimpleTypeVisitor8() {
+                    @Override
+                    protected String defaultAction(final TypeMirror e, @Nullable Void unused) {
+                        return e.toString();
+                    }
+
+                    @Override
+                    public String visitArray(final ArrayType t, @Nullable Void unused) {
+                        return visit(t.getComponentType(), unused) + "[]";
+                    }
+
+                    @Override
+                    public @Nullable String visitDeclared(final DeclaredType t, final Void unused) {
+                        return safeCast(t.asElement(), TypeElement.class)
+                                .getQualifiedName()
+                                .toString();
+                    }
+                },
+                null);
+    }
+
+    /**
+     * Returns the fully qualified name of the element corresponding to a {@link DeclaredType}.
+     *
+     * @param element A Java language element.
+     */
+    private String toString(Element element) {
+        return element.accept(
+                new SimpleElementVisitor8() {
+                    @Override
+                    public String visitPackage(PackageElement e, @Nullable Void unused) {
+                        return e.getQualifiedName().toString();
+                    }
+
+                    @Override
+                    public String visitType(TypeElement e, @Nullable Void unused) {
+                        Element parent = e.getEnclosingElement();
+                        String separator = parent.getKind() == ElementKind.PACKAGE ? "." : "$";
+                        return visit(parent, unused)
+                                + separator
+                                + e.getSimpleName().toString();
+                    }
+
+                    @Override
+                    protected String defaultAction(Element e, @Nullable Void unused) {
+                        return "";
+                    }
+                },
+                null);
+    }
+}
diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
index 8293bac51bf..ff41a4fdc9b 100644
--- a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
@@ -25,26 +25,24 @@
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import javax.annotation.processing.AbstractProcessor;
 import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
 import javax.annotation.processing.Processor;
 import javax.annotation.processing.RoundEnvironment;
 import javax.annotation.processing.SupportedAnnotationTypes;
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.AnnotationValue;
-import javax.lang.model.element.Element;
-import javax.lang.model.element.Name;
 import javax.lang.model.element.TypeElement;
-import javax.lang.model.util.SimpleElementVisitor8;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.Elements;
 import javax.tools.Diagnostic.Kind;
 import javax.tools.FileObject;
 import javax.tools.JavaFileObject;
@@ -52,10 +50,10 @@
 import org.apache.logging.log4j.LoggingException;
 import org.apache.logging.log4j.plugins.Configurable;
 import org.apache.logging.log4j.plugins.Namespace;
-import org.apache.logging.log4j.plugins.Node;
 import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginAliases;
 import org.apache.logging.log4j.plugins.model.PluginEntry;
+import org.apache.logging.log4j.plugins.model.PluginIndex;
 import org.apache.logging.log4j.util.Strings;
 import org.jspecify.annotations.NullMarked;
 
@@ -74,7 +72,7 @@
  * 
  */
 @NullMarked
-@SupportedAnnotationTypes({"org.apache.logging.log4j.plugins.*", "org.apache.logging.log4j.core.config.plugins.*"})
+@SupportedAnnotationTypes("org.apache.logging.log4j.plugins.Plugin")
 @ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL)
 public class PluginProcessor extends AbstractProcessor {
 
@@ -98,7 +96,9 @@ public class PluginProcessor extends AbstractProcessor {
             "META-INF/services/org.apache.logging.log4j.plugins.model.PluginService";
 
     private boolean enableBndAnnotations;
-    private String packageName = "";
+    private CharSequence packageName = "";
+    private final PluginIndex pluginIndex = new PluginIndex();
+    private final Set processedElements = new HashSet<>();
 
     public PluginProcessor() {}
 
@@ -112,89 +112,124 @@ public SourceVersion getSupportedSourceVersion() {
         return SourceVersion.latest();
     }
 
+    @Override
+    public synchronized void init(ProcessingEnvironment processingEnv) {
+        super.init(processingEnv);
+        handleOptions();
+    }
+
     @Override
     public boolean process(final Set extends TypeElement> annotations, final RoundEnvironment roundEnv) {
-        handleOptions(processingEnv.getOptions());
-        final Messager messager = processingEnv.getMessager();
-        messager.printMessage(Kind.NOTE, "Processing Log4j annotations");
-        try {
-            final Set extends Element> elements = roundEnv.getElementsAnnotatedWith(Plugin.class);
-            if (elements.isEmpty()) {
-                messager.printMessage(Kind.NOTE, "No elements to process");
-                return true;
+        // Process the elements for this round
+        if (!annotations.isEmpty()) {
+            processPluginAnnotatedClasses(ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Plugin.class)));
+        }
+        // Write the generated code
+        if (roundEnv.processingOver() && !pluginIndex.isEmpty()) {
+            try {
+                final Messager messager = processingEnv.getMessager();
+                messager.printMessage(Kind.NOTE, "Writing Log4j plugin metadata using base package " + packageName);
+                writeClassFile();
+                writeServiceFile();
+                messager.printMessage(Kind.NOTE, "Log4j annotations processed");
+            } catch (final Exception e) {
+                handleUnexpectedError(e);
             }
-            messager.printMessage(Kind.NOTE, "Retrieved " + elements.size() + " Plugin elements");
-            final List list = new ArrayList<>();
-            packageName = collectPlugins(packageName, elements, list);
-            messager.printMessage(Kind.NOTE, "Writing plugin metadata using base package " + packageName);
-            Collections.sort(list);
-            writeClassFile(packageName, list);
-            writeServiceFile(packageName);
-            messager.printMessage(Kind.NOTE, "Annotations processed");
-        } catch (final Exception ex) {
-            var writer = new StringWriter();
-            ex.printStackTrace(new PrintWriter(writer));
-            error(writer.toString());
         }
+        // Do not claim the annotations to allow other annotation processors to run
         return false;
     }
 
-    private void error(final CharSequence message) {
-        processingEnv.getMessager().printMessage(Kind.ERROR, message);
+    private void processPluginAnnotatedClasses(Set pluginClasses) {
+        final boolean calculatePackageName = packageName.isEmpty();
+        final Elements elements = processingEnv.getElementUtils();
+        final Messager messager = processingEnv.getMessager();
+        for (var pluginClass : pluginClasses) {
+            final String name = getPluginName(pluginClass);
+            final String namespace = getNamespace(pluginClass);
+            final String className = elements.getBinaryName(pluginClass).toString();
+            var builder =
+                    PluginEntry.builder().setName(name).setNamespace(namespace).setClassName(className);
+            processConfigurableAnnotation(pluginClass, builder);
+            var entry = builder.get();
+            messager.printMessage(Kind.NOTE, "Parsed Log4j plugin " + entry, pluginClass);
+            if (!pluginIndex.add(entry)) {
+                messager.printMessage(Kind.WARNING, "Duplicate Log4j plugin parsed " + entry, pluginClass);
+            }
+            pluginIndex.addAll(createPluginAliases(pluginClass, builder));
+            if (calculatePackageName) {
+                packageName = calculatePackageName(elements, pluginClass, packageName);
+            }
+            processedElements.add(pluginClass);
+        }
     }
 
-    private String collectPlugins(
-            String packageName, final Iterable extends Element> elements, final List list) {
-        final boolean calculatePackage = packageName.isEmpty();
-        final var pluginVisitor = new PluginElementVisitor();
-        final var pluginAliasesVisitor = new PluginAliasesElementVisitor();
-        for (final Element element : elements) {
-            // The elements must be annotated with `Plugin`
-            Plugin plugin = element.getAnnotation(Plugin.class);
-            final var entry = element.accept(pluginVisitor, plugin);
-            list.add(entry);
-            if (calculatePackage) {
-                packageName = calculatePackage(element, packageName);
-            }
-            list.addAll(element.accept(pluginAliasesVisitor, plugin));
+    private static void processConfigurableAnnotation(TypeElement pluginClass, PluginEntry.Builder builder) {
+        var configurable = pluginClass.getAnnotation(Configurable.class);
+        if (configurable != null) {
+            var elementType = configurable.elementType();
+            builder.setElementType(elementType.isEmpty() ? builder.getName() : elementType)
+                    .setDeferChildren(configurable.deferChildren())
+                    .setPrintable(configurable.printObject());
         }
-        return packageName;
     }
 
-    private String calculatePackage(Element element, String packageName) {
-        final Name name = processingEnv.getElementUtils().getPackageOf(element).getQualifiedName();
-        if (name.isEmpty()) {
-            return "";
+    private static List createPluginAliases(TypeElement pluginClass, PluginEntry.Builder builder) {
+        return Optional.ofNullable(pluginClass.getAnnotation(PluginAliases.class)).map(PluginAliases::value).stream()
+                .flatMap(Arrays::stream)
+                .map(alias -> alias.toLowerCase(Locale.ROOT))
+                .map(key -> builder.setKey(key).get())
+                .toList();
+    }
+
+    private void handleUnexpectedError(final Exception e) {
+        var writer = new StringWriter();
+        e.printStackTrace(new PrintWriter(writer));
+        processingEnv
+                .getMessager()
+                .printMessage(Kind.ERROR, "Unexpected error processing Log4j annotations: " + writer);
+    }
+
+    private static CharSequence calculatePackageName(
+            Elements elements, TypeElement typeElement, CharSequence packageName) {
+        var qualifiedName = elements.getPackageOf(typeElement).getQualifiedName();
+        if (qualifiedName.isEmpty()) {
+            return packageName;
         }
-        final String pkgName = name.toString();
         if (packageName.isEmpty()) {
-            return pkgName;
+            return qualifiedName;
         }
-        if (pkgName.length() == packageName.length()) {
+        int packageLength = packageName.length();
+        int qualifiedLength = qualifiedName.length();
+        if (packageLength == qualifiedLength) {
             return packageName;
         }
-        if (pkgName.length() < packageName.length() && packageName.startsWith(pkgName)) {
-            return pkgName;
+        if (qualifiedLength < packageLength
+                && qualifiedName.contentEquals(packageName.subSequence(0, qualifiedLength))) {
+            return qualifiedName;
         }
-
-        return commonPrefix(pkgName, packageName);
+        return commonPrefix(qualifiedName, packageName);
     }
 
-    private void writeServiceFile(final String pkgName) throws IOException {
+    private void writeServiceFile() throws IOException {
         final FileObject fileObject = processingEnv
                 .getFiler()
-                .createResource(StandardLocation.CLASS_OUTPUT, Strings.EMPTY, SERVICE_FILE_NAME);
+                .createResource(
+                        StandardLocation.CLASS_OUTPUT,
+                        Strings.EMPTY,
+                        SERVICE_FILE_NAME,
+                        processedElements.toArray(TypeElement[]::new));
         try (final PrintWriter writer =
                 new PrintWriter(new BufferedWriter(new OutputStreamWriter(fileObject.openOutputStream(), UTF_8)))) {
             writer.println("# Generated by " + PluginProcessor.class.getName());
-            writer.println(createFqcn(pkgName));
+            writer.println(createFqcn(packageName));
         }
     }
 
-    private void writeClassFile(final String pkg, final List list) {
-        final String fqcn = createFqcn(pkg);
+    private void writeClassFile() {
+        final String fqcn = createFqcn(packageName);
         try (final PrintWriter writer = createSourceFile(fqcn)) {
-            writer.println("package " + pkg + ".plugins;");
+            writer.println("package " + packageName + ".plugins;");
             writer.println("");
             if (enableBndAnnotations) {
                 writer.println("import aQute.bnd.annotation.Resolution;");
@@ -209,9 +244,9 @@ private void writeClassFile(final String pkg, final List list) {
             writer.println("public class Log4jPlugins extends PluginService {");
             writer.println("");
             writer.println("  private static final PluginEntry[] ENTRIES = new PluginEntry[] {");
-            final int max = list.size() - 1;
-            for (int i = 0; i < list.size(); ++i) {
-                final PluginEntry entry = list.get(i);
+            final int max = pluginIndex.size() - 1;
+            int current = 0;
+            for (final PluginEntry entry : pluginIndex) {
                 writer.println("    PluginEntry.builder()");
                 writer.println(String.format("      .setKey(\"%s\")", entry.key()));
                 writer.println(String.format("      .setClassName(\"%s\")", entry.className()));
@@ -227,7 +262,8 @@ private void writeClassFile(final String pkg, final List list) {
                 if (entry.deferChildren()) {
                     writer.println("      .setDeferChildren(true)");
                 }
-                writer.println("      .get()" + (i < max ? "," : Strings.EMPTY));
+                writer.println("      .get()" + (current < max ? "," : Strings.EMPTY));
+                current++;
             }
             writer.println("    };");
             writer.println("    @Override");
@@ -238,17 +274,25 @@ private void writeClassFile(final String pkg, final List list) {
 
     private PrintWriter createSourceFile(final String fqcn) {
         try {
-            final JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(fqcn);
+            final JavaFileObject sourceFile =
+                    processingEnv.getFiler().createSourceFile(fqcn, processedElements.toArray(TypeElement[]::new));
             return new PrintWriter(sourceFile.openWriter());
         } catch (IOException e) {
             throw new LoggingException("Unable to create Plugin Service Class " + fqcn, e);
         }
     }
 
-    private String createFqcn(String packageName) {
+    private String createFqcn(CharSequence packageName) {
         return packageName + ".plugins.Log4jPlugins";
     }
 
+    private static String getPluginName(TypeElement pluginClass) {
+        return Optional.ofNullable(pluginClass.getAnnotation(Plugin.class))
+                .map(Plugin::value)
+                .filter(s -> !s.isEmpty())
+                .orElseGet(() -> pluginClass.getSimpleName().toString());
+    }
+
     private static String getNamespace(final TypeElement e) {
         return Optional.ofNullable(e.getAnnotation(Namespace.class))
                 .map(Namespace::value)
@@ -267,52 +311,18 @@ private static String getNamespace(final TypeElement e) {
                         .orElse(Plugin.EMPTY));
     }
 
-    private static PluginEntry configureNamespace(final TypeElement e, final PluginEntry.Builder builder) {
-        final Configurable configurable = e.getAnnotation(Configurable.class);
-        if (configurable != null) {
-            builder.setNamespace(Node.CORE_NAMESPACE)
-                    .setElementType(
-                            configurable.elementType().isEmpty() ? builder.getName() : configurable.elementType())
-                    .setDeferChildren(configurable.deferChildren())
-                    .setPrintable(configurable.printObject());
-        } else {
-            builder.setNamespace(getNamespace(e));
-        }
-        return builder.get();
-    }
-
-    /**
-     * ElementVisitor to scan the Plugin annotation.
-     */
-    private final class PluginElementVisitor extends SimpleElementVisitor8 {
-        @Override
-        public PluginEntry visitType(final TypeElement e, final Plugin plugin) {
-            Objects.requireNonNull(plugin, "Plugin annotation is null.");
-            String name = plugin.value();
-            if (name.isEmpty()) {
-                name = e.getSimpleName().toString();
-            }
-            final PluginEntry.Builder builder = PluginEntry.builder()
-                    .setKey(name.toLowerCase(Locale.ROOT))
-                    .setName(name)
-                    .setClassName(
-                            processingEnv.getElementUtils().getBinaryName(e).toString());
-            return configureNamespace(e, builder);
-        }
-    }
-
-    private String commonPrefix(final String str1, final String str2) {
+    private static CharSequence commonPrefix(final CharSequence str1, final CharSequence str2) {
         final int minLength = Math.min(str1.length(), str2.length());
         for (int i = 0; i < minLength; i++) {
             if (str1.charAt(i) != str2.charAt(i)) {
                 if (i > 1 && str1.charAt(i - 1) == '.') {
-                    return str1.substring(0, i - 1);
+                    return str1.subSequence(0, i - 1);
                 } else {
-                    return str1.substring(0, i);
+                    return str1.subSequence(0, i);
                 }
             }
         }
-        return str1.substring(0, minLength);
+        return str1.subSequence(0, minLength);
     }
 
     private boolean isServiceConsumerClassPresent() {
@@ -320,7 +330,8 @@ private boolean isServiceConsumerClassPresent() {
         return processingEnv.getElementUtils().getTypeElement("aQute.bnd.annotation.spi.ServiceConsumer") != null;
     }
 
-    private void handleOptions(Map options) {
+    private void handleOptions() {
+        var options = processingEnv.getOptions();
         packageName = options.getOrDefault(PLUGIN_PACKAGE, "");
         String enableBndAnnotationsOption = options.get(ENABLE_BND_ANNOTATIONS);
         if (enableBndAnnotationsOption != null) {
@@ -329,38 +340,4 @@ private void handleOptions(Map options) {
             this.enableBndAnnotations = isServiceConsumerClassPresent();
         }
     }
-
-    /**
-     * ElementVisitor to scan the PluginAliases annotation.
-     */
-    private final class PluginAliasesElementVisitor extends SimpleElementVisitor8, Plugin> {
-
-        private PluginAliasesElementVisitor() {
-            super(List.of());
-        }
-
-        @Override
-        public Collection visitType(final TypeElement e, final Plugin plugin) {
-            final PluginAliases aliases = e.getAnnotation(PluginAliases.class);
-            if (aliases == null) {
-                return DEFAULT_VALUE;
-            }
-            String name = plugin.value();
-            if (name.isEmpty()) {
-                name = e.getSimpleName().toString();
-            }
-            final PluginEntry.Builder builder = PluginEntry.builder()
-                    .setName(name)
-                    .setClassName(
-                            processingEnv.getElementUtils().getBinaryName(e).toString());
-            configureNamespace(e, builder);
-            final Collection entries = new ArrayList<>(aliases.value().length);
-            for (final String alias : aliases.value()) {
-                final PluginEntry entry =
-                        builder.setKey(alias.toLowerCase(Locale.ROOT)).get();
-                entries.add(entry);
-            }
-            return entries;
-        }
-    }
 }
diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java
new file mode 100644
index 00000000000..50e8a651055
--- /dev/null
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java
@@ -0,0 +1,149 @@
+/*
+ * 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.plugin.processor.internal;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.util.Elements;
+import org.apache.logging.log4j.plugin.processor.GraalVmProcessor;
+
+public final class Annotations {
+
+    private static final Collection FACTORY_TYPE_NAMES = List.of(
+            "org.apache.logging.log4j.plugins.Factory",
+            "org.apache.logging.log4j.plugins.PluginFactory",
+            "org.apache.logging.log4j.plugins.SingletonFactory",
+            "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory",
+            "org.apache.logging.log4j.core.config.plugins.PluginFactory");
+
+    private static final Collection INJECT_NAMES = List.of("org.apache.logging.log4j.plugins.Inject");
+
+    private static final Collection QUALIFIER_TYPE_NAMES = List.of(
+            "org.apache.logging.log4j.plugins.Named",
+            "org.apache.logging.log4j.plugins.PluginAttribute",
+            "org.apache.logging.log4j.plugins.PluginBuilderAttribute",
+            "org.apache.logging.log4j.plugins.PluginElement",
+            "org.apache.logging.log4j.plugins.PluginNode",
+            "org.apache.logging.log4j.plugins.PluginValue",
+            "org.apache.logging.log4j.core.config.plugins.PluginAttribute",
+            "org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute",
+            "org.apache.logging.log4j.core.config.plugins.PluginConfiguration",
+            "org.apache.logging.log4j.core.config.plugins.PluginElement",
+            "org.apache.logging.log4j.core.config.plugins.PluginLoggerContext",
+            "org.apache.logging.log4j.core.config.plugins.PluginNode",
+            "org.apache.logging.log4j.core.config.plugins.PluginValue");
+
+    /**
+     * These must be public types with either:
+     * 
+     *     - A factory method.
 
+     *     - A static method called {@code newInstance}.
 
+     *     - A public no-argument constructor.
 
+     * 
+     * 
+     *     Note: The annotations listed here must also be declared in
+     *     {@link GraalVmProcessor}.
+     * 
+     */
+    private static final Collection PLUGIN_ANNOTATION_NAMES =
+            List.of("org.apache.logging.log4j.plugins.Plugin", "org.apache.logging.log4j.core.config.plugins.Plugin");
+
+    /**
+     * Reflection is also used to create meta annotation strategies.
+     * .
+     * 
+     *     Note: The annotations listed here must also be declared in
+     *     {@link GraalVmProcessor}.
+     * 
+     */
+    private static final Collection META_ANNOTATION_STRATEGY_NAMES = List.of(
+            "org.apache.logging.log4j.plugins.condition.Conditional",
+            "org.apache.logging.log4j.plugins.validation.Constraint");
+
+    public enum Type {
+        INJECT,
+        /**
+         * Annotation used to mark a configuration attribute, element or other injected parameters.
+         */
+        QUALIFIER,
+        /**
+         * Annotation used to mark a Log4j Plugin factory method.
+         */
+        FACTORY,
+        /**
+         * Annotation used to mark a Log4j Plugin class.
+         */
+        PLUGIN,
+        /**
+         * Annotation containing the name of a
+         * {@link org.apache.logging.log4j.plugins.validation.ConstraintValidator}
+         * or
+         * {@link org.apache.logging.log4j.plugins.condition.Condition}.
+         */
+        META_ANNOTATION_STRATEGY,
+        /**
+         * Unknown
+         */
+        UNKNOWN
+    }
+
+    private final Map typeElementToTypeMap = new HashMap<>();
+
+    public Annotations(final Elements elements) {
+        FACTORY_TYPE_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.FACTORY));
+        INJECT_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.INJECT));
+        QUALIFIER_TYPE_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.QUALIFIER));
+        PLUGIN_ANNOTATION_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.PLUGIN));
+        META_ANNOTATION_STRATEGY_NAMES.forEach(
+                className -> addTypeElementIfExists(elements, className, Type.META_ANNOTATION_STRATEGY));
+    }
+
+    private void addTypeElementIfExists(Elements elements, CharSequence className, Type type) {
+        final TypeElement element = elements.getTypeElement(className);
+        if (element != null) {
+            typeElementToTypeMap.put(element, type);
+        }
+    }
+
+    public Annotations.Type classifyAnnotation(TypeElement element) {
+        return typeElementToTypeMap.getOrDefault(element, Type.UNKNOWN);
+    }
+
+    public Element getAnnotationClassValue(Element element, TypeElement annotation) {
+        // This prevents getting an "Attempt to access Class object for TypeMirror" exception
+        AnnotationMirror annotationMirror = element.getAnnotationMirrors().stream()
+                .filter(am -> am.getAnnotationType().asElement().equals(annotation))
+                .findFirst()
+                .orElseThrow(
+                        () -> new IllegalStateException("No `@" + annotation + "` annotation found on " + element));
+        AnnotationValue annotationValue = annotationMirror.getElementValues().entrySet().stream()
+                .filter(e -> "value".equals(e.getKey().getSimpleName().toString()))
+                .map(Map.Entry::getValue)
+                .findFirst()
+                .orElseThrow(() ->
+                        new IllegalStateException("No `value` found `@" + annotation + "` annotation on " + element));
+        DeclaredType value = (DeclaredType) annotationValue.getValue();
+        return value.asElement();
+    }
+}
diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java
new file mode 100644
index 00000000000..c65fcff7219
--- /dev/null
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java
@@ -0,0 +1,296 @@
+/*
+ * 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.plugin.processor.internal;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.stream.IntStream;
+import org.apache.logging.log4j.util.StringBuilders;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Provides support for the
+ * {@code reachability-metadata.json}
+ * file format.
+ */
+@NullMarked
+public final class ReachabilityMetadata {
+
+    /**
+     * Key used to specify the name of a field or method
+     */
+    public static final String FIELD_OR_METHOD_NAME = "name";
+    /**
+     * Key used to list the method parameter types.
+     */
+    public static final String PARAMETER_TYPES = "parameterTypes";
+    /**
+     * Key used to specify the name of a type.
+     * 
+     * Since GraalVM for JDK 23 it will be called "type".
+     * 
+     */
+    public static final String TYPE_NAME = "name";
+    /**
+     * Key used to specify the list of fields available for reflection.
+     */
+    public static final String FIELDS = "fields";
+    /**
+     * Key used to specify the list of methods available for reflection.
+     */
+    public static final String METHODS = "methods";
+
+    private static class MinimalJsonWriter {
+        private final Appendable output;
+
+        public MinimalJsonWriter(Appendable output) {
+            this.output = output;
+        }
+
+        public void writeString(CharSequence input) throws IOException {
+            output.append('"');
+            StringBuilder sb = new StringBuilder(input);
+            StringBuilders.escapeJson(sb, 0);
+            output.append(sb);
+            output.append('"');
+        }
+
+        public void writeObjectStart() throws IOException {
+            output.append('{');
+        }
+
+        public void writeObjectEnd() throws IOException {
+            output.append('}');
+        }
+
+        public void writeObjectKey(CharSequence key) throws IOException {
+            writeString(key);
+            output.append(':').append(' ');
+        }
+
+        public void writeArrayStart() throws IOException {
+            output.append('[');
+        }
+
+        public void writeSeparator() throws IOException {
+            output.append(',').append(' ');
+        }
+
+        public void writeArrayEnd() throws IOException {
+            output.append(']');
+        }
+
+        public void writeLineSeparator() throws IOException {
+            output.append('\n');
+        }
+    }
+
+    /**
+     * Specifies a field that needs to be accessed through reflection.
+     */
+    public static final class Field implements Comparable {
+
+        private final String name;
+
+        public Field(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        void toJson(MinimalJsonWriter jsonWriter) throws IOException {
+            jsonWriter.writeObjectStart();
+            jsonWriter.writeObjectKey(FIELD_OR_METHOD_NAME);
+            jsonWriter.writeString(name);
+            jsonWriter.writeObjectEnd();
+        }
+
+        @Override
+        public int compareTo(Field other) {
+            return name.compareTo(other.name);
+        }
+    }
+
+    /**
+     * Specifies a method that needs to be accessed through reflection.
+     */
+    public static final class Method implements Comparable {
+
+        private final String name;
+        private final List parameterTypes = new ArrayList<>();
+
+        public Method(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void addParameterType(final String parameterType) {
+            parameterTypes.add(parameterType);
+        }
+
+        void toJson(MinimalJsonWriter jsonWriter) throws IOException {
+            jsonWriter.writeObjectStart();
+            jsonWriter.writeObjectKey(FIELD_OR_METHOD_NAME);
+            jsonWriter.writeString(name);
+            jsonWriter.writeSeparator();
+            jsonWriter.writeObjectKey(PARAMETER_TYPES);
+            jsonWriter.writeArrayStart();
+            boolean first = true;
+            for (String parameterType : parameterTypes) {
+                if (!first) {
+                    jsonWriter.writeSeparator();
+                }
+                first = false;
+                jsonWriter.writeString(parameterType);
+            }
+            jsonWriter.writeArrayEnd();
+            jsonWriter.writeObjectEnd();
+        }
+
+        @Override
+        public int compareTo(Method other) {
+            int result = name.compareTo(other.name);
+            if (result == 0) {
+                result = parameterTypes.size() - other.parameterTypes.size();
+            }
+            if (result == 0) {
+                result = IntStream.range(0, parameterTypes.size())
+                        .map(idx -> parameterTypes.get(idx).compareTo(other.parameterTypes.get(idx)))
+                        .filter(r -> r != 0)
+                        .findFirst()
+                        .orElse(0);
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Specifies a Java type that needs to be accessed through reflection.
+     */
+    public static final class Type {
+
+        private final String type;
+        private final Collection methods = new TreeSet<>();
+        private final Collection fields = new TreeSet<>();
+
+        public Type(String type) {
+            this.type = type;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public void addMethod(Method method) {
+            methods.add(method);
+        }
+
+        public void addField(Field field) {
+            fields.add(field);
+        }
+
+        void toJson(MinimalJsonWriter jsonWriter) throws IOException {
+            jsonWriter.writeObjectStart();
+            jsonWriter.writeObjectKey(TYPE_NAME);
+            jsonWriter.writeString(type);
+            jsonWriter.writeSeparator();
+
+            boolean first = true;
+            jsonWriter.writeObjectKey(METHODS);
+            jsonWriter.writeArrayStart();
+            for (Method method : methods) {
+                if (!first) {
+                    jsonWriter.writeSeparator();
+                }
+                first = false;
+                method.toJson(jsonWriter);
+            }
+            jsonWriter.writeArrayEnd();
+            jsonWriter.writeSeparator();
+
+            first = true;
+            jsonWriter.writeObjectKey(FIELDS);
+            jsonWriter.writeArrayStart();
+            for (Field field : fields) {
+                if (!first) {
+                    jsonWriter.writeSeparator();
+                }
+                first = false;
+                field.toJson(jsonWriter);
+            }
+            jsonWriter.writeArrayEnd();
+            jsonWriter.writeObjectEnd();
+        }
+    }
+
+    /**
+     * Collection of reflection metadata.
+     */
+    public static final class Reflection {
+
+        private final Collection types = new TreeSet<>(Comparator.comparing(Type::getType));
+
+        public Reflection(Collection types) {
+            this.types.addAll(types);
+        }
+
+        void toJson(MinimalJsonWriter jsonWriter) throws IOException {
+            boolean first = true;
+            jsonWriter.writeArrayStart();
+            for (Type type : types) {
+                if (!first) {
+                    jsonWriter.writeSeparator();
+                }
+                first = false;
+                jsonWriter.writeLineSeparator();
+                type.toJson(jsonWriter);
+            }
+            jsonWriter.writeLineSeparator();
+            jsonWriter.writeArrayEnd();
+        }
+    }
+
+    /**
+     * Writes the contents of a {@code reflect-config.json} file.
+     *
+     * @param types  The reflection metadata for types.
+     * @param output The object to use as output.
+     */
+    public static void writeReflectConfig(Collection types, OutputStream output) throws IOException {
+        try (Writer writer = new OutputStreamWriter(output, StandardCharsets.UTF_8)) {
+            Reflection reflection = new Reflection(types);
+            MinimalJsonWriter jsonWriter = new MinimalJsonWriter(writer);
+            reflection.toJson(jsonWriter);
+            jsonWriter.writeLineSeparator();
+        }
+    }
+
+    private ReachabilityMetadata() {}
+}
diff --git a/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java
new file mode 100644
index 00000000000..9e11f0be933
--- /dev/null
+++ b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.plugin.processor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class GraalVmProcessorTest {
+    static final String GROUP_ID = "org.apache.logging.log4j";
+    static final String ARTIFACT_ID = "log4j-plugin-processor-test";
+    static final Path REFLECT_CONFIG_PATH =
+            Path.of("META-INF", "native-image", "log4j-generated", GROUP_ID, ARTIFACT_ID, "reflect-config.json");
+
+    static String readExpectedReflectConfig() throws IOException {
+        var url = Objects.requireNonNull(GraalVmProcessorTest.class.getResource("/expected-reflect-config.json"));
+        try (var inputStream = url.openStream()) {
+            return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+        }
+    }
+
+    static String readActualReflectConfig(Path baseDirectory) throws IOException {
+        return Files.readString(baseDirectory.resolve(REFLECT_CONFIG_PATH));
+    }
+
+    static List findInputSourceFiles() throws IOException {
+        try (var stream = Files.list(Path.of("src", "test", "resources", "example"))) {
+            return stream.filter(Files::isRegularFile).toList();
+        }
+    }
+
+    @Test
+    void verifyAnnotationProcessorGeneratesExpectedReachability(@TempDir Path outputDir) throws Exception {
+        var compiler = ToolProvider.getSystemJavaCompiler();
+        var fileManager = compiler.getStandardFileManager(null, null, null);
+        fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, List.of(outputDir));
+        fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, List.of(outputDir));
+        var sourceFiles = fileManager.getJavaFileObjectsFromPaths(findInputSourceFiles());
+        var options = List.of("-Alog4j.graalvm.groupId=" + GROUP_ID, "-Alog4j.graalvm.artifactId=" + ARTIFACT_ID);
+        var task = compiler.getTask(null, fileManager, null, options, null, sourceFiles);
+        task.setProcessors(List.of(new GraalVmProcessor()));
+        assertEquals(true, task.call());
+        String expected = readExpectedReflectConfig();
+        String actual = readActualReflectConfig(outputDir);
+        assertEquals(expected, actual);
+    }
+}
diff --git a/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java
index b6c2e7033ed..d3d1f88ecef 100644
--- a/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java
+++ b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java
@@ -16,18 +16,18 @@
  */
 package org.apache.logging.log4j.plugin.processor;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -39,7 +39,7 @@
 import javax.tools.StandardJavaFileManager;
 import javax.tools.StandardLocation;
 import javax.tools.ToolProvider;
-import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.file.PathUtils;
 import org.apache.logging.log4j.plugins.model.PluginEntry;
 import org.apache.logging.log4j.plugins.model.PluginNamespace;
 import org.apache.logging.log4j.plugins.model.PluginService;
@@ -86,7 +86,8 @@ private static PluginService generatePluginService(String expectedPluginPackage,
         try {
             // Instantiate the tooling
             JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
-            StandardJavaFileManager fileManager = compiler.getStandardFileManager(collector, Locale.ROOT, UTF_8);
+            StandardJavaFileManager fileManager =
+                    compiler.getStandardFileManager(collector, Locale.ROOT, StandardCharsets.UTF_8);
 
             // Populate sources
             Iterable extends JavaFileObject> sources = fileManager.getJavaFileObjects(fakePluginPath);
@@ -97,21 +98,24 @@ private static PluginService generatePluginService(String expectedPluginPackage,
 
             // Compile the sources
             final JavaCompiler.CompilationTask task =
-                    compiler.getTask(null, fileManager, collector, Arrays.asList(options), null, sources);
+                    compiler.getTask(null, fileManager, collector, List.of(options), null, sources);
             task.setProcessors(List.of(new PluginProcessor()));
-            task.call();
+            Boolean result = task.call();
 
             // Verify successful compilation
-            List> diagnostics = collector.getDiagnostics();
-            assertThat(diagnostics).isEmpty();
+            assertEquals(true, result);
+            assertThat(collector.getDiagnostics()).isEmpty();
 
             // Find the PluginService class
             Path pluginServicePath = outputDir.resolve(fqcn.replaceAll("\\.", "/") + ".class");
             assertThat(pluginServicePath).exists();
             Class> pluginServiceClass = classLoader.defineClass(fqcn, pluginServicePath);
-            return (PluginService) pluginServiceClass.getConstructor().newInstance();
+            return pluginServiceClass
+                    .asSubclass(PluginService.class)
+                    .getConstructor()
+                    .newInstance();
         } finally {
-            FileUtils.deleteDirectory(outputDir.toFile());
+            PathUtils.deleteDirectory(outputDir);
         }
     }
 
@@ -227,7 +231,6 @@ public List> getDiagnostics() {
         public void report(Diagnostic extends JavaFileObject> diagnostic) {
             switch (diagnostic.getKind()) {
                 case ERROR:
-                case WARNING:
                 case MANDATORY_WARNING:
                     diagnostics.add(diagnostic);
                     break;
diff --git a/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java b/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java
new file mode 100644
index 00000000000..8350441b241
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java
@@ -0,0 +1,58 @@
+/*
+ * 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 example;
+
+import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+/**
+ *
+ */
+public class AbstractPluginWithGenericBuilder {
+
+    public abstract static class Builder> {
+
+        @PluginBuilderAttribute
+        @Required(message = "The thing given by the builder is null")
+        private String thing;
+
+        @SuppressWarnings("unchecked")
+        public B asBuilder() {
+            return (B) this;
+        }
+
+        public String getThing() {
+            return thing;
+        }
+
+        public B setThing(final String name) {
+            this.thing = name;
+            return asBuilder();
+        }
+    }
+
+    private final String thing;
+
+    public AbstractPluginWithGenericBuilder(final String thing) {
+        super();
+        this.thing = thing;
+    }
+
+    public String getThing() {
+        return thing;
+    }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java b/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java
new file mode 100644
index 00000000000..512a5eb4f86
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java
@@ -0,0 +1,63 @@
+/*
+ * 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 example;
+
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Inject;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginElement;
+
+@Configurable
+@Plugin("configurable")
+public class ConfigurablePlugin {
+    private final ValidatingPlugin alpha;
+    private final ValidatingPluginWithGenericBuilder beta;
+    private final ValidatingPluginWithTypedBuilder gamma;
+    private final PluginWithGenericSubclassFoo1Builder delta;
+
+    @Inject
+    public ConfigurablePlugin(
+            @PluginElement final ValidatingPlugin alpha,
+            @PluginElement final ValidatingPluginWithGenericBuilder beta,
+            @PluginElement final ValidatingPluginWithTypedBuilder gamma,
+            @PluginElement final PluginWithGenericSubclassFoo1Builder delta) {
+        this.alpha = alpha;
+        this.beta = beta;
+        this.gamma = gamma;
+        this.delta = delta;
+    }
+
+    public String getAlphaName() {
+        return alpha.getName();
+    }
+
+    public String getBetaName() {
+        return beta.getName();
+    }
+
+    public String getGammaName() {
+        return gamma.getName();
+    }
+
+    public String getDeltaThing() {
+        return delta.getThing();
+    }
+
+    public String getDeltaName() {
+        return delta.getFoo1();
+    }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java b/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java
new file mode 100644
index 00000000000..ce69006fdfe
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java
@@ -0,0 +1,33 @@
+/*
+ * 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 example;
+
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Inject;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginElement;
+
+@Configurable
+@Plugin
+public record ConfigurableRecord(
+        @PluginElement ValidatingPlugin alpha,
+        @PluginElement ValidatingPluginWithGenericBuilder beta,
+        @PluginElement ValidatingPluginWithTypedBuilder gamma,
+        @PluginElement PluginWithGenericSubclassFoo1Builder delta) {
+    @Inject
+    public ConfigurableRecord {}
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java b/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java
new file mode 100644
index 00000000000..64a28433f0b
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java
@@ -0,0 +1,66 @@
+/*
+ * 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 example;
+
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginAttribute;
+import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+@Configurable
+@Plugin("PluginWithGenericSubclassFoo1Builder")
+public class PluginWithGenericSubclassFoo1Builder extends AbstractPluginWithGenericBuilder {
+
+    public static class Builder> extends AbstractPluginWithGenericBuilder.Builder
+            implements org.apache.logging.log4j.plugins.util.Builder {
+
+        @PluginAttribute
+        @Required(message = "The foo1 given by the builder is null")
+        private String foo1;
+
+        @Override
+        public PluginWithGenericSubclassFoo1Builder build() {
+            return new PluginWithGenericSubclassFoo1Builder(getThing(), getFoo1());
+        }
+
+        public String getFoo1() {
+            return foo1;
+        }
+
+        public B setFoo1(final String foo1) {
+            this.foo1 = foo1;
+            return asBuilder();
+        }
+    }
+
+    @PluginFactory
+    public static > B newBuilder() {
+        return new Builder().asBuilder();
+    }
+
+    private final String foo1;
+
+    public PluginWithGenericSubclassFoo1Builder(final String thing, final String foo1) {
+        super(thing);
+        this.foo1 = foo1;
+    }
+
+    public String getFoo1() {
+        return foo1;
+    }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java
new file mode 100644
index 00000000000..3b48000c938
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java
@@ -0,0 +1,64 @@
+/*
+ * 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 example;
+
+import java.util.Objects;
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+/**
+ *
+ */
+@Configurable
+@Plugin("Validator")
+public class ValidatingPlugin {
+
+    private final String name;
+
+    public ValidatingPlugin(final String name) {
+        this.name = Objects.requireNonNull(name, "name");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    @PluginFactory
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static class Builder implements org.apache.logging.log4j.plugins.util.Builder {
+
+        @PluginBuilderAttribute
+        @Required(message = "The name given by the builder is null")
+        private String name;
+
+        public Builder setName(final String name) {
+            this.name = name;
+            return this;
+        }
+
+        @Override
+        public ValidatingPlugin build() {
+            return new ValidatingPlugin(name);
+        }
+    }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java
new file mode 100644
index 00000000000..211a003fe30
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java
@@ -0,0 +1,70 @@
+/*
+ * 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 example;
+
+import java.util.Objects;
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginAttribute;
+import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+/**
+ *
+ */
+@Configurable
+@Plugin("ValidatingPluginWithGenericBuilder")
+public class ValidatingPluginWithGenericBuilder {
+
+    private final String name;
+
+    public ValidatingPluginWithGenericBuilder(final String name) {
+        this.name = Objects.requireNonNull(name, "name");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    @PluginFactory
+    public static > B newBuilder() {
+        return new Builder().asBuilder();
+    }
+
+    public static class Builder>
+            implements org.apache.logging.log4j.plugins.util.Builder {
+
+        @PluginAttribute
+        @Required(message = "The name given by the builder is null")
+        private String name;
+
+        public B setName(final String name) {
+            this.name = name;
+            return asBuilder();
+        }
+
+        @SuppressWarnings("unchecked")
+        public B asBuilder() {
+            return (B) this;
+        }
+
+        @Override
+        public ValidatingPluginWithGenericBuilder build() {
+            return new ValidatingPluginWithGenericBuilder(name);
+        }
+    }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java
new file mode 100644
index 00000000000..2977b1041a1
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java
@@ -0,0 +1,65 @@
+/*
+ * 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 example;
+
+import java.util.Objects;
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+/**
+ *
+ */
+@Configurable
+@Plugin("ValidatingPluginWithTypedBuilder")
+public class ValidatingPluginWithTypedBuilder {
+
+    private final String name;
+
+    public ValidatingPluginWithTypedBuilder(final String name) {
+        this.name = Objects.requireNonNull(name, "name");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    @PluginFactory
+    public static Builder newBuilder() {
+        return new Builder<>();
+    }
+
+    public static class Builder
+            implements org.apache.logging.log4j.plugins.util.Builder {
+
+        @PluginBuilderAttribute
+        @Required(message = "The name given by the builder is null")
+        private String name;
+
+        public Builder setName(final String name) {
+            this.name = name;
+            return this;
+        }
+
+        @Override
+        public ValidatingPluginWithTypedBuilder build() {
+            return new ValidatingPluginWithTypedBuilder(name);
+        }
+    }
+}
diff --git a/log4j-plugin-processor/src/test/resources/expected-reflect-config.json b/log4j-plugin-processor/src/test/resources/expected-reflect-config.json
new file mode 100644
index 00000000000..f5af8c6511d
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/expected-reflect-config.json
@@ -0,0 +1,15 @@
+[
+{"name": "example.AbstractPluginWithGenericBuilder$Builder", "methods": [], "fields": [{"name": "thing"}]}, 
+{"name": "example.ConfigurablePlugin", "methods": [{"name": "", "parameterTypes": ["example.ValidatingPlugin", "example.ValidatingPluginWithGenericBuilder", "example.ValidatingPluginWithTypedBuilder", "example.PluginWithGenericSubclassFoo1Builder"]}], "fields": []}, 
+{"name": "example.ConfigurableRecord", "methods": [{"name": "", "parameterTypes": ["example.ValidatingPlugin", "example.ValidatingPluginWithGenericBuilder", "example.ValidatingPluginWithTypedBuilder", "example.PluginWithGenericSubclassFoo1Builder"]}], "fields": [{"name": "alpha"}, {"name": "beta"}, {"name": "delta"}, {"name": "gamma"}]}, 
+{"name": "example.FakePlugin", "methods": [{"name": "", "parameterTypes": []}], "fields": []}, 
+{"name": "example.FakePlugin$Nested", "methods": [{"name": "", "parameterTypes": []}], "fields": []}, 
+{"name": "example.PluginWithGenericSubclassFoo1Builder", "methods": [{"name": "", "parameterTypes": ["java.lang.String", "java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []}, 
+{"name": "example.PluginWithGenericSubclassFoo1Builder$Builder", "methods": [], "fields": [{"name": "foo1"}]}, 
+{"name": "example.ValidatingPlugin", "methods": [{"name": "", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []}, 
+{"name": "example.ValidatingPlugin$Builder", "methods": [], "fields": [{"name": "name"}]}, 
+{"name": "example.ValidatingPluginWithGenericBuilder", "methods": [{"name": "", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []}, 
+{"name": "example.ValidatingPluginWithGenericBuilder$Builder", "methods": [], "fields": [{"name": "name"}]}, 
+{"name": "example.ValidatingPluginWithTypedBuilder", "methods": [{"name": "", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []}, 
+{"name": "example.ValidatingPluginWithTypedBuilder$Builder", "methods": [], "fields": [{"name": "name"}]}
+]
diff --git a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginCacheTest.java b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginIndexTest.java
similarity index 50%
rename from log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginCacheTest.java
rename to log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginIndexTest.java
index 6fc33cab54d..a338e40d210 100644
--- a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginCacheTest.java
+++ b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginIndexTest.java
@@ -17,47 +17,39 @@
 package org.apache.logging.log4j.plugins.processor;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
 
-import java.io.IOException;
-import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import org.apache.logging.log4j.plugins.model.PluginCache;
 import org.apache.logging.log4j.plugins.model.PluginEntry;
+import org.apache.logging.log4j.plugins.model.PluginIndex;
 import org.junit.jupiter.api.Test;
 import org.junitpioneer.jupiter.Issue;
 
-public class PluginCacheTest {
+public class PluginIndexTest {
 
     @Test
     @Issue("https://issues.apache.org/jira/browse/LOG4J2-2735")
-    public void testOutputIsReproducibleWhenInputOrderingChanges() throws IOException {
-        final PluginCache cacheA = new PluginCache();
-        createCategory(cacheA, "one", Arrays.asList("bravo", "alpha", "charlie"));
-        createCategory(cacheA, "two", Arrays.asList("alpha", "charlie", "bravo"));
-        assertEquals(cacheA.getAllNamespaces().size(), 2);
-        assertEquals(cacheA.getAllNamespaces().get("one").size(), 3);
-        assertEquals(cacheA.getAllNamespaces().get("two").size(), 3);
-        final PluginCache cacheB = new PluginCache();
-        createCategory(cacheB, "two", Arrays.asList("bravo", "alpha", "charlie"));
-        createCategory(cacheB, "one", Arrays.asList("alpha", "charlie", "bravo"));
-        assertEquals(cacheB.getAllNamespaces().size(), 2);
-        assertEquals(cacheB.getAllNamespaces().get("one").size(), 3);
-        assertEquals(cacheB.getAllNamespaces().get("two").size(), 3);
-        assertEquals(Objects.toString(cacheA.getAllNamespaces()), Objects.toString(cacheB.getAllNamespaces()));
+    public void testOutputIsReproducibleWhenInputOrderingChanges() {
+        final PluginIndex indexA = new PluginIndex();
+        createNamespace(indexA, "one", List.of("bravo", "alpha", "charlie"));
+        createNamespace(indexA, "two", List.of("alpha", "charlie", "bravo"));
+        assertEquals(6, indexA.size());
+        final PluginIndex indexB = new PluginIndex();
+        createNamespace(indexB, "two", List.of("bravo", "alpha", "charlie"));
+        createNamespace(indexB, "one", List.of("alpha", "charlie", "bravo"));
+        assertEquals(6, indexB.size());
+        assertIterableEquals(indexA, indexB);
     }
 
-    private void createCategory(final PluginCache cache, final String categoryName, final List entryNames) {
-        final Map category = cache.getNamespace(categoryName);
+    private void createNamespace(final PluginIndex index, final String namespace, final List entryNames) {
         for (String entryName : entryNames) {
             final PluginEntry entry = PluginEntry.builder()
                     .setKey(entryName)
                     .setName(entryName)
                     .setClassName("com.example.Plugin")
-                    .setNamespace(categoryName)
+                    .setNamespace(namespace)
                     .get();
-            category.put(entryName, entry);
+            index.add(entry);
         }
     }
 }
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginCache.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginCache.java
deleted file mode 100644
index 024e931d3a7..00000000000
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginCache.java
+++ /dev/null
@@ -1,91 +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.model;
-
-import java.io.BufferedInputStream;
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.net.URL;
-import java.util.Enumeration;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class PluginCache {
-    private final Map> namespaces = new TreeMap<>();
-
-    /**
-     * Returns all namespaces of plugins in this cache.
-     *
-     * @return all namespaces of plugins in this cache.
-     * @since 2.1
-     */
-    public Map> getAllNamespaces() {
-        return namespaces;
-    }
-
-    /**
-     * Gets or creates a namespace of plugins.
-     *
-     * @param namespace namespace to look up.
-     * @return plugin mapping of names to plugin entries.
-     */
-    public Map getNamespace(final String namespace) {
-        final String key = namespace.toLowerCase(Locale.ROOT);
-        return namespaces.computeIfAbsent(key, ignored -> new TreeMap<>());
-    }
-
-    /**
-     * Loads and merges all the Log4j plugin cache files specified. Usually, this is obtained via a ClassLoader.
-     *
-     * @param resources URLs to all the desired plugin cache files to load.
-     * @throws IOException if an I/O exception occurs.
-     */
-    public void loadCacheFiles(final Enumeration resources) throws IOException {
-        namespaces.clear();
-        while (resources.hasMoreElements()) {
-            final URL url = resources.nextElement();
-            try (final DataInputStream in = new DataInputStream(new BufferedInputStream(url.openStream()))) {
-                final int count = in.readInt();
-                for (int i = 0; i < count; i++) {
-                    final var builder = PluginEntry.builder().setNamespace(in.readUTF());
-                    final Map m = getNamespace(builder.getNamespace());
-                    final int entries = in.readInt();
-                    for (int j = 0; j < entries; j++) {
-                        // Must always read all parts of the entry, even if not adding, so that the stream progresses
-                        final var entry = builder.setKey(in.readUTF())
-                                .setClassName(in.readUTF())
-                                .setName(in.readUTF())
-                                .setPrintable(in.readBoolean())
-                                .setDeferChildren(in.readBoolean())
-                                .get();
-                        m.putIfAbsent(entry.key(), entry);
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Gets the number of plugin namespaces registered.
-     *
-     * @return number of plugin namespaces in cache.
-     */
-    public int size() {
-        return namespaces.size();
-    }
-}
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginIndex.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginIndex.java
new file mode 100644
index 00000000000..f9a6b38aca7
--- /dev/null
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginIndex.java
@@ -0,0 +1,79 @@
+/*
+ * 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.model;
+
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public class PluginIndex extends AbstractCollection {
+    private final Map> index = new TreeMap<>();
+
+    @Override
+    public void forEach(Consumer super PluginEntry> action) {
+        for (var namespace : index.values()) {
+            for (var pluginEntry : namespace.values()) {
+                action.accept(pluginEntry);
+            }
+        }
+    }
+
+    @Override
+    public Iterator iterator() {
+        return index.values().stream()
+                .map(Map::values)
+                .flatMap(Collection::stream)
+                .iterator();
+    }
+
+    @Override
+    public int size() {
+        return index.values().stream().mapToInt(Map::size).sum();
+    }
+
+    @Override
+    public boolean add(PluginEntry entry) {
+        return getOrCreateNamespace(entry.namespace()).putIfAbsent(entry.key(), entry) == null;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return index.isEmpty();
+    }
+
+    @Override
+    public boolean contains(Object o) {
+        return o instanceof PluginEntry entry
+                && index.containsKey(entry.namespace())
+                && index.get(entry.namespace()).containsKey(entry.key())
+                && index.get(entry.namespace()).get(entry.key()).equals(entry);
+    }
+
+    @Override
+    public void clear() {
+        index.clear();
+    }
+
+    private Map getOrCreateNamespace(String namespace) {
+        return index.computeIfAbsent(namespace, ignored -> new TreeMap<>());
+    }
+}
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginRegistry.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginRegistry.java
index dbd4fdc8b0c..e09b5109880 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginRegistry.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginRegistry.java
@@ -20,6 +20,8 @@
 
 import aQute.bnd.annotation.Cardinality;
 import aQute.bnd.annotation.spi.ServiceConsumer;
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
 import java.io.IOException;
 import java.net.URL;
 import java.text.DecimalFormat;
@@ -101,24 +103,44 @@ private void loadPlugins(final ClassLoader classLoader, final Namespaces namespa
 
     private Namespaces decodeCacheFiles(final ClassLoader classLoader) {
         final long startTime = System.nanoTime();
-        final PluginCache cache = new PluginCache();
+        final PluginIndex index = new PluginIndex();
         try {
             final Enumeration resources = classLoader.getResources(PLUGIN_CACHE_FILE);
             if (resources == null) {
                 LOGGER.info("Plugin preloads not available from class loader {}", classLoader);
             } else {
-                cache.loadCacheFiles(resources);
+                while (resources.hasMoreElements()) {
+                    final URL url = resources.nextElement();
+                    try (final DataInputStream in = new DataInputStream(new BufferedInputStream(url.openStream()))) {
+                        final int count = in.readInt();
+                        for (int i = 0; i < count; i++) {
+                            final var builder = PluginEntry.builder().setNamespace(in.readUTF());
+                            final int entries = in.readInt();
+                            for (int j = 0; j < entries; j++) {
+                                // Must always read all parts of the entry, even if not adding, so that the stream
+                                // progresses
+                                final var entry = builder.setKey(in.readUTF())
+                                        .setClassName(in.readUTF())
+                                        .setName(in.readUTF())
+                                        .setPrintable(in.readBoolean())
+                                        .setDeferChildren(in.readBoolean())
+                                        .get();
+                                index.add(entry);
+                            }
+                        }
+                    }
+                }
             }
         } catch (final IOException ioe) {
             LOGGER.warn("Unable to preload plugins", ioe);
         }
         final Namespaces namespaces = new Namespaces();
         final AtomicInteger pluginCount = new AtomicInteger();
-        cache.getAllNamespaces().forEach((key, outer) -> outer.values().forEach(entry -> {
+        index.forEach(entry -> {
             final PluginType> type = new PluginType<>(entry, classLoader);
             namespaces.add(type);
             pluginCount.incrementAndGet();
-        }));
+        });
         reportLoadTime(classLoader, startTime, pluginCount);
         return namespaces;
     }