`PluginBuilder` reads builder fields that `GraalVmProcessor` does not register, so `LevelMatchFilter`, `SystemPropertyArbiter` and others fail in GraalVM native images

Abierto
#4,441 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@typat ya está trabajando en esto.

Desde el 31/10/2026.

  • #4442 de @typat — abierto

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
35/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
java

Línea de trabajo

The runtime side is PluginBuilder.injectFields() and validateFields(), which currently walk every declared field via TypeUtil.getAllDeclaredFields. The metadata side is GraalVmProcessor and its Annotations helper, which need to handle @PluginAttribute. Start by reproducing the native-image failure with the issue's log4j2.xml and demo classes, then check that GraalVmProcessorTest gets a fixture with a @PluginAttribute builder field. Done means the native output matches the JVM output and the new PluginBuilderTest passes.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

bug graalvm plugins priority: P2 waiting-for-maintainer

Description

I first reported this to Spring Boot (spring-projects/spring-boot#52046) because their SpringProfileArbiter broke in our native image. Andy pointed out that the metadata comes from GraalVmProcessor, so the fix belongs here. After digging in, it isn't Spring specific. On current GraalVM CE a plain Log4j 2.26.1 app hits it with Log4j's own filters and arbiters.

There are two gaps between what PluginBuilder does at runtime and what GraalVmProcessor registers.

  1. PluginBuilder.injectFields() and PluginBuilder.validateFields() take every declared field in the builder hierarchy (TypeUtil.getAllDeclaredFields) and call setAccessible on all of them. validateFields() then calls Field.get() on each one, including fields with no plugin annotation. GraalVmProcessor only registers the annotated fields. On GraalVM CE 25.0.2 this works by accident, because getDeclaredFields() hides fields that aren't registered. On GraalVM CE 25.4.4.1.1 (JDK 25.0.4) getDeclaredFields() returns all of them, so Field.get() on a static final constant or a static logger throws MissingReflectionRegistrationError and the plugin isn't created. I get the same failure on 25.0.2 as soon as anything registers the builder type in the newer reachability-metadata.json format, even when it lists only the annotated fields.

  2. GraalVmProcessor doesn't handle @PluginAttribute. PluginBuilder accepts it on builder fields (the visitor lookup is generic) and Routes.Builder and KafkaAppender.Builder use it, but those fields get no metadata. In a native image the attribute is rejected as invalid and the builder keeps its defaults. This one also fails on 25.0.2.

These builders in log4j-core 2.26.1 have fields that the shipped metadata doesn't cover. I found them by comparing declared fields against the reflect-config.json files in the jar, and I ran LevelMatchFilter and SystemPropertyArbiter natively.

  • Every builder that extends AbstractFilter.AbstractFilterBuilder (static ATTR_ON_MATCH / ATTR_ON_MISMATCH), so LevelMatchFilter, BurstFilter, StringMatchFilter, MutableThreadContextMapFilter
  • SystemPropertyArbiter, EnvironmentArbiter, ClassArbiter (static ATTR_* constants) and ScriptArbiter (static LOGGER)
  • PatternMatch.Builder (serialVersionUID)
  • PosixViewAttributeAction.Builder (subst, filePermissions) and JmsAppender.Builder (jmsManager), plain fields with no annotation
  • Routes.Builder (pattern) and KafkaAppender.Builder (all attributes), which use @PluginAttribute on fields, so <Routes pattern="..."> in a RoutingAppender loses its pattern

Outside Log4j, Spring Boot's SpringProfileArbiter.Builder (static statusLogger) and SpringBootTriggeringPolicyBuilder (@PluginAttribute fields) have the same problems.

It fails quietly. The status logger prints errors and the configuration carries on without the plugin, so a filter disappears and lets through events it should deny, or a <Select> gets dropped and the appender loses its layout.

Configuration

Version: 2.26.1 (same code on 2.x at d1e96de)

Operating system: Linux (Docker, toolchain from ghcr.io/graalvm/native-image-community:25)

JDK: GraalVM CE 25.4.4.1.1 (JDK 25.0.4.1.1), default native-image options. GraalVM CE 25.0.2 only shows the @PluginAttribute problem. We first saw this in a Spring Boot app with GraalVM CE and Liberica NIK on JDK 25.0.4.

Logs

Native run on GraalVM CE 25.4.4.1.1 with the reproducer below (trimmed):

main ERROR Could not create plugin of type class org.apache.logging.log4j.core.config.arbiters.SystemPropertyArbiter for element SystemPropertyArbiter: org.graalvm.nativeimage.MissingReflectionRegistrationError: Cannot reflectively read or write field 'public static final java.lang.String org.apache.logging.log4j.core.config.arbiters.SystemPropertyArbiter$Builder.ATTR_PROPERTY_NAME'. [...]
	at [email protected]/java.lang.reflect.Field.get(Field.java:440)
	at org.apache.logging.log4j.core.config.plugins.util.PluginBuilder.validateFields(PluginBuilder.java:220)
	at org.apache.logging.log4j.core.config.plugins.util.PluginBuilder.injectFields(PluginBuilder.java:203)
	at org.apache.logging.log4j.core.config.plugins.util.PluginBuilder.build(PluginBuilder.java:123)
	[...]
main ERROR Unable to invoke factory method in class org.apache.logging.log4j.core.config.arbiters.SystemPropertyArbiter for element SystemPropertyArbiter: java.lang.IllegalStateException: No factory method found for class org.apache.logging.log4j.core.config.arbiters.SystemPropertyArbiter
main ERROR Error capturing node data for node Console
main ERROR AttributeArbiter contains an invalid element or attribute "active"
main ERROR appender Console has no parameter that matches element Select
main ERROR Could not create plugin of type class org.apache.logging.log4j.core.filter.LevelMatchFilter for element LevelMatchFilter: org.graalvm.nativeimage.MissingReflectionRegistrationError: Cannot reflectively read or write field 'public static final java.lang.String org.apache.logging.log4j.core.filter.AbstractFilter$AbstractFilterBuilder.ATTR_ON_MISMATCH'. [...]
main ERROR Unable to invoke factory method in class org.apache.logging.log4j.core.filter.LevelMatchFilter for element LevelMatchFilter: java.lang.IllegalStateException: No factory method found for class org.apache.logging.log4j.core.filter.LevelMatchFilter

The No factory method found errors are just PluginBuilder falling back to a factory method after the builder fails. It's not #3871 again; the type-name fix from 2.25.3 is in place here.

Output on the JVM:

ARBITER  INFO  info message
CUSTOM   INFO  info message
ARBITER  WARN  warn message
FILTER   WARN  warn message
CUSTOM   WARN  warn message

Output in the native image (layout lost, filter gone, wrong <Select> branch):

info message
FILTER   INFO  info message
WRONG    INFO  info message
warn message
FILTER   WARN  warn message
WRONG    WARN  warn message

Reproduction

log4j2.xml:

<Configuration status="WARN">
  <Appenders>
    <Console name="Arbiter">
      <Select>
        <SystemPropertyArbiter propertyName="java.vm.name" propertyValue="does-not-match">
          <PatternLayout pattern="WRONG    %-5p %m%n"/>
        </SystemPropertyArbiter>
        <DefaultArbiter>
          <PatternLayout pattern="ARBITER  %-5p %m%n"/>
        </DefaultArbiter>
      </Select>
    </Console>
    <Console name="Filter">
      <PatternLayout pattern="FILTER   %-5p %m%n"/>
      <LevelMatchFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
    </Console>
    <Console name="Custom">
      <Select>
        <AttributeArbiter active="true">
          <PatternLayout pattern="CUSTOM   %-5p %m%n"/>
        </AttributeArbiter>
        <DefaultArbiter>
          <PatternLayout pattern="WRONG    %-5p %m%n"/>
        </DefaultArbiter>
      </Select>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="INFO">
      <AppenderRef ref="Arbiter"/>
      <AppenderRef ref="Filter"/>
      <AppenderRef ref="Custom"/>
    </Root>
  </Loggers>
</Configuration>

demo/Main.java:

package demo;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Main {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(Main.class);
        logger.info("info message");
        logger.warn("warn message");
    }
}

demo/AttributeArbiter.java, a third-party plugin that uses @PluginAttribute on a builder field the way KafkaAppender.Builder does:

package demo;

import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.arbiters.Arbiter;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;

@Plugin(name = "AttributeArbiter", category = Node.CATEGORY, elementType = Arbiter.ELEMENT_TYPE, deferChildren = true, printObject = true)
public final class AttributeArbiter implements Arbiter {

    private final boolean active;

    private AttributeArbiter(boolean active) {
        this.active = active;
    }

    @Override
    public boolean isCondition() {
        return active;
    }

    @PluginBuilderFactory
    public static Builder newBuilder() {
        return new Builder();
    }

    public static final class Builder implements org.apache.logging.log4j.core.util.Builder<AttributeArbiter> {

        @PluginAttribute("active")
        private String active;

        @Override
        public AttributeArbiter build() {
            return new AttributeArbiter(Boolean.parseBoolean(active));
        }
    }
}

Build and run:

CP=log4j-api-2.26.1.jar:log4j-core-2.26.1.jar
javac -proc:full -Alog4j.graalvm.groupId=demo -Alog4j.graalvm.artifactId=demo -cp $CP -d classes demo/*.java
cp log4j2.xml classes/
java -cp classes:$CP demo.Main
native-image --no-fallback -cp classes:$CP -o app demo.Main
./app

The generated metadata for the demo plugin has no entry for AttributeArbiter$Builder:

[{"name":"demo.AttributeArbiter","methods":[{"name":"newBuilder","parameterTypes":[]}],"fields":[]}]

Proposed fix

I have a patch ready and can open a PR against 2.x:

  • PluginBuilder only uses fields that have an annotation meta-annotated with @PluginVisitorStrategy or @Constraint. Those are the only fields it injects or validates anyway, so the runtime matches what the processor registers and doesn't depend on how a GraalVM version treats unregistered fields.
  • GraalVmProcessor (and its Annotations helper) handle @PluginAttribute, so builder fields that use it get registered. Nothing changes for method parameters.

With the patch the reproducer prints the same output natively as on the JVM, on GraalVM CE 25.4.4.1.1. The PR adds a PluginBuilderTest for the field filter and a @PluginAttribute builder field to the GraalVmProcessorTest fixture.

The other way to fix the first part would be for GraalVmProcessor to register all declared fields of builder types. I went with the runtime change because it also covers base classes the processor never sees and keeps the metadata small, but I'm fine either way.

Lenguaje dominante
Java
Estrellas
3.6k
Forks
1.8k
Merge medio
5 d 6 h
PR fusionados (30 d)
24

Preparar el entorno

  • Sin Dockerfile ni archivo de Docker Compose
  • Tiene una plantilla de pull request
  • Sin guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de apache/logging-log4j2

Todos los issues de apache/logging-log4j2

Issues similares

Más issues de Java

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.