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

オープン
#4,441 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@typat がすでに取り組んでいます。

2026年10月31日 から。

  • #4442 @typat による — オープン

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
35/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
java
領域
observability

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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.

主要言語
Java
スター
3.6k
フォーク
1.8k
平均マージ
5日 6時間
マージ済み PR(30日)
24

環境構築

  • Dockerfile・Docker Compose ファイルなし
  • プルリクエストのテンプレートあり
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

apache/logging-log4j2 のほかの issue

apache/logging-log4j2 の issue をすべて見る

似ている issue

Java の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。