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

mattsicker pushed a commit to branch feature/3.x/graalvm-reachability
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 0d6fbafc30ab5d4ae882dd7a52e2c396e0b07035
Author: Matt Sicker <msic...@apple.com>
AuthorDate: Fri Aug 15 12:35:45 2025 -0500

    Port GraalVM reachability to 3.x
    
    This ports the GraalVmProcessor introduced in 
a14f0ad57ed380b19ddd715df6f3651acea6f8eb (released in 2.25.0) to the plugin 
model of 3.x.
---
 log4j-parent/pom.xml                               |   3 +
 .../log4j/plugin/processor/GraalVmProcessor.java   | 355 +++++++++++++++++++++
 .../plugin/processor/internal/Annotations.java     | 149 +++++++++
 .../processor/internal/ReachabilityMetadata.java   | 296 +++++++++++++++++
 .../plugin/processor/GraalVmProcessorTest.java     |  70 ++++
 .../example/AbstractPluginWithGenericBuilder.java  |  58 ++++
 .../test/resources/example/ConfigurablePlugin.java |  63 ++++
 .../test/resources/example/ConfigurableRecord.java |  33 ++
 .../PluginWithGenericSubclassFoo1Builder.java      |  66 ++++
 .../test/resources/example/ValidatingPlugin.java   |  64 ++++
 .../ValidatingPluginWithGenericBuilder.java        |  70 ++++
 .../example/ValidatingPluginWithTypedBuilder.java  |  65 ++++
 .../test/resources/expected-reflect-config.json    |  15 +
 13 files changed, 1307 insertions(+)

diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index 1f1b6a7575..e2f940675c 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -866,6 +866,9 @@
                 <arg>-Alog4j.docgen.version=${project.version}</arg>
                 <arg>-Alog4j.docgen.description=${project.description}</arg>
                 
<arg>-Alog4j.docgen.typeFilter.excludePattern=${log4j.docgen.typeFilter.excludePattern}</arg>
+                <!-- Provide arguments for the GraalVM processor -->
+                <arg>-Alog4j.graalvm.groupId=${project.groupId}</arg>
+                <arg>-Alog4j.graalvm.artifactId=${project.artifactId}</arg>
               </compilerArgs>
             </configuration>
 
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 0000000000..0f67a29f89
--- /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.
+ * <p>
+ *     <strong>Note:</strong> The annotations listed here must also be 
classified by the {@link Annotations} helper.
+ * </p>
+ */
+@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<String, ReachabilityMetadata.Type> reachableTypes = new 
HashMap<>();
+    private final List<Element> 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 "<init>":
+                            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<String, String> 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.
+     * <p>
+     *     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.
+     * </p>
+     *
+     * @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=<groupId>%n"
+                            + "  -A%3$s=<artifactId>%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 extends Element> T safeCast(Element element, Class<T> 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<String, @Nullable Void>() {
+                    @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<String, @Nullable Void>() {
+                    @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/internal/Annotations.java
 
b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java
new file mode 100644
index 0000000000..50e8a65105
--- /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<String> 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<String> INJECT_NAMES = 
List.of("org.apache.logging.log4j.plugins.Inject");
+
+    private static final Collection<String> 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:
+     * <ul>
+     *     <li>A factory method.</li>
+     *     <li>A static method called {@code newInstance}.</li>
+     *     <li>A public no-argument constructor.</li>
+     * </ul>
+     * <p>
+     *     <strong>Note:</strong> The annotations listed here must also be 
declared in
+     *     {@link GraalVmProcessor}.
+     * </p>
+     */
+    private static final Collection<String> 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.
+     * .
+     * <p>
+     *     <strong>Note:</strong> The annotations listed here must also be 
declared in
+     *     {@link GraalVmProcessor}.
+     * </p>
+     */
+    private static final Collection<String> 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<TypeElement, Type> 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 0000000000..c65fcff721
--- /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
+ * <a 
href="https://www.graalvm.org/latest/reference-manual/native-image/metadata/#specifying-metadata-with-json";>{@code
 reachability-metadata.json}</a>
+ * 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.
+     * <p>
+     * Since GraalVM for JDK 23 it will be called "type".
+     * </p>
+     */
+    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<Field> {
+
+        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<Method> {
+
+        private final String name;
+        private final List<String> 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<Method> methods = new TreeSet<>();
+        private final Collection<Field> 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<Type> types = new 
TreeSet<>(Comparator.comparing(Type::getType));
+
+        public Reflection(Collection<Type> 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<Type> 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 0000000000..9e11f0be93
--- /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<Path> 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/resources/example/AbstractPluginWithGenericBuilder.java
 
b/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java
new file mode 100644
index 0000000000..8350441b24
--- /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<B extends Builder<B>> {
+
+        @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 0000000000..512a5eb4f8
--- /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 0000000000..ce69006fdf
--- /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 0000000000..64a28433f0
--- /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<B extends Builder<B>> extends 
AbstractPluginWithGenericBuilder.Builder<B>
+            implements 
org.apache.logging.log4j.plugins.util.Builder<PluginWithGenericSubclassFoo1Builder>
 {
+
+        @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 extends Builder<B>> B newBuilder() {
+        return new Builder<B>().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 0000000000..3b48000c93
--- /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<ValidatingPlugin> {
+
+        @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 0000000000..211a003fe3
--- /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 extends Builder<B>> B newBuilder() {
+        return new Builder<B>().asBuilder();
+    }
+
+    public static class Builder<B extends Builder<B>>
+            implements 
org.apache.logging.log4j.plugins.util.Builder<ValidatingPluginWithGenericBuilder>
 {
+
+        @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 0000000000..2977b1041a
--- /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<Integer> newBuilder() {
+        return new Builder<>();
+    }
+
+    public static class Builder<T>
+            implements 
org.apache.logging.log4j.plugins.util.Builder<ValidatingPluginWithTypedBuilder> 
{
+
+        @PluginBuilderAttribute
+        @Required(message = "The name given by the builder is null")
+        private String name;
+
+        public Builder<T> 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 0000000000..f5af8c6511
--- /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": "<init>", 
"parameterTypes": ["example.ValidatingPlugin", 
"example.ValidatingPluginWithGenericBuilder", 
"example.ValidatingPluginWithTypedBuilder", 
"example.PluginWithGenericSubclassFoo1Builder"]}], "fields": []}, 
+{"name": "example.ConfigurableRecord", "methods": [{"name": "<init>", 
"parameterTypes": ["example.ValidatingPlugin", 
"example.ValidatingPluginWithGenericBuilder", 
"example.ValidatingPluginWithTypedBuilder", 
"example.PluginWithGenericSubclassFoo1Builder"]}], "fields": [{"name": 
"alpha"}, {"name": "beta"}, {"name": "delta"}, {"name": "gamma"}]}, 
+{"name": "example.FakePlugin", "methods": [{"name": "<init>", 
"parameterTypes": []}], "fields": []}, 
+{"name": "example.FakePlugin$Nested", "methods": [{"name": "<init>", 
"parameterTypes": []}], "fields": []}, 
+{"name": "example.PluginWithGenericSubclassFoo1Builder", "methods": [{"name": 
"<init>", "parameterTypes": ["java.lang.String", "java.lang.String"]}, {"name": 
"newBuilder", "parameterTypes": []}], "fields": []}, 
+{"name": "example.PluginWithGenericSubclassFoo1Builder$Builder", "methods": 
[], "fields": [{"name": "foo1"}]}, 
+{"name": "example.ValidatingPlugin", "methods": [{"name": "<init>", 
"parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", 
"parameterTypes": []}], "fields": []}, 
+{"name": "example.ValidatingPlugin$Builder", "methods": [], "fields": 
[{"name": "name"}]}, 
+{"name": "example.ValidatingPluginWithGenericBuilder", "methods": [{"name": 
"<init>", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", 
"parameterTypes": []}], "fields": []}, 
+{"name": "example.ValidatingPluginWithGenericBuilder$Builder", "methods": [], 
"fields": [{"name": "name"}]}, 
+{"name": "example.ValidatingPluginWithTypedBuilder", "methods": [{"name": 
"<init>", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", 
"parameterTypes": []}], "fields": []}, 
+{"name": "example.ValidatingPluginWithTypedBuilder$Builder", "methods": [], 
"fields": [{"name": "name"}]}
+]

Reply via email to