From 06fc66cae2eaffabc1389ea79329dbc294a015bb Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sat, 12 Jul 2025 17:16:37 +0200 Subject: [PATCH 1/2] Add AOT runtime hints for Log4j Core 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change introduces AOT runtime hints for Log4j Core 2 to support its integration with Spring Boot native images. Starting with version `2.25.0`, Log4j Core 2 includes built-in GraalVM reachability metadata, allowing native image generation without requiring additional manual configuration. This contribution complements that by adding Spring Boot–specific metadata: * Registers default Spring Boot configuration files. * Registers classes referenced via `ClassUtils.isPresent(...)` checks. Fixes #42273. > [!NOTE] > This change should be reviewed in conjunction with spring-projects/spring-boot#46334, which configures Log4j Core’s `GraalVmProcessor` to generate reachability metadata for Spring Boot’s custom Log4j plugins. Signed-off-by: Piotr P. Karwasz --- core/spring-boot/build.gradle | 3 + .../logging/log4j2/Log4J2LoggingSystem.java | 30 +++++--- .../logging/log4j2/Log4J2RuntimeHints.java | 67 +++++++++++++++++ .../resources/META-INF/spring/aot.factories | 1 + .../log4j2/Log4J2RuntimeHintsTests.java | 73 +++++++++++++++++++ 5 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java create mode 100644 core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHintsTests.java diff --git a/core/spring-boot/build.gradle b/core/spring-boot/build.gradle index 5b6c9fb4ab5d..86799c5f4754 100644 --- a/core/spring-boot/build.gradle +++ b/core/spring-boot/build.gradle @@ -71,6 +71,9 @@ dependencies { testImplementation("org.hibernate.validator:hibernate-validator") testImplementation("org.jboss.logging:jboss-logging") testImplementation("org.springframework.data:spring-data-r2dbc") + + // Used in Log4J2RuntimeHintsTests + testRuntimeOnly("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") } def syncJavaTemplates = tasks.register("syncJavaTemplates", Sync) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index 6e941a3e1675..bea63d57a73c 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -90,44 +90,50 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { private static final String OPTIONAL_PREFIX = "optional:"; - private static final String LOG4J_BRIDGE_HANDLER = "org.apache.logging.log4j.jul.Log4jBridgeHandler"; + /** + * JUL handler that routes messages to the Log4j API (optional dependency). + */ + static final String LOG4J_BRIDGE_HANDLER = "org.apache.logging.log4j.jul.Log4jBridgeHandler"; - private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; + /** + * JUL LogManager that routes messages to the Log4j API as the backend. + */ + static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; /** * JSON tree parser used by Log4j 2 (optional dependency). */ - private static final String JSON_TREE_PARSER_V2 = "com.fasterxml.jackson.databind.ObjectMapper"; + static final String JSON_TREE_PARSER_V2 = "com.fasterxml.jackson.databind.ObjectMapper"; /** * JSON tree parser embedded in Log4j 3. */ - private static final String JSON_TREE_PARSER_V3 = "org.apache.logging.log4j.kit.json.JsonReader"; + static final String JSON_TREE_PARSER_V3 = "org.apache.logging.log4j.kit.json.JsonReader"; /** * Configuration factory for properties files (Log4j 2). */ - private static final String PROPS_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory"; + static final String PROPS_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory"; /** * Configuration factory for properties files (Log4j 3, optional dependency). */ - private static final String PROPS_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory"; + static final String PROPS_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory"; /** * YAML tree parser used by Log4j 2 (optional dependency). */ - private static final String YAML_TREE_PARSER_V2 = "com.fasterxml.jackson.dataformat.yaml.YAMLMapper"; + static final String YAML_TREE_PARSER_V2 = "com.fasterxml.jackson.dataformat.yaml.YAMLMapper"; /** * Configuration factory for YAML files (Log4j 2, embedded). */ - private static final String YAML_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory"; + static final String YAML_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory"; /** * Configuration factory for YAML files (Log4j 3, optional dependency). */ - private static final String YAML_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.yaml.YamlConfigurationFactory"; + static final String YAML_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.yaml.YamlConfigurationFactory"; private static final SpringEnvironmentPropertySource propertySource = new SpringEnvironmentPropertySource(); @@ -616,8 +622,10 @@ protected String getDefaultLogCorrelationPattern() { @Order(0) public static class Factory implements LoggingSystemFactory { - private static final boolean PRESENT = ClassUtils - .isPresent("org.apache.logging.log4j.core.impl.Log4jContextFactory", Factory.class.getClassLoader()); + static final String LOG4J_CORE_CONTEXT_FACTORY = "org.apache.logging.log4j.core.impl.Log4jContextFactory"; + + private static final boolean PRESENT = ClassUtils.isPresent(LOG4J_CORE_CONTEXT_FACTORY, + Factory.class.getClassLoader()); @Override public @Nullable LoggingSystem getLoggingSystem(ClassLoader classLoader) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java new file mode 100644 index 000000000000..ad2739c7fe95 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java @@ -0,0 +1,67 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.logging.log4j2; + +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.util.ClassUtils; + +/** + * {@link RuntimeHintsRegistrar} implementation for {@link Log4J2LoggingSystem}. + * + * @author Piotr P. Karwasz + */ +class Log4J2RuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + if (!ClassUtils.isPresent(Log4J2LoggingSystem.Factory.LOG4J_CORE_CONTEXT_FACTORY, classLoader)) { + return; + } + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.Factory.LOG4J_CORE_CONTEXT_FACTORY); + // Register default Log4j2 configuration files + hints.resources().registerPattern("org/springframework/boot/logging/log4j2/log4j2.xml"); + hints.resources().registerPattern("org/springframework/boot/logging/log4j2/log4j2-file.xml"); + hints.resources().registerPattern("log4j2.springboot"); + // Declares the types that Log4j2LoggingSystem checks for existence reflectively. + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.JSON_TREE_PARSER_V2); + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.JSON_TREE_PARSER_V3); + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.PROPS_CONFIGURATION_FACTORY_V2); + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.PROPS_CONFIGURATION_FACTORY_V3); + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.YAML_TREE_PARSER_V2); + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.YAML_CONFIGURATION_FACTORY_V2); + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.YAML_CONFIGURATION_FACTORY_V3); + // Register JUL to Log4j 2 bridge handler + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.LOG4J_BRIDGE_HANDLER); + registerTypeForReachability(hints, classLoader, Log4J2LoggingSystem.LOG4J_LOG_MANAGER); + // Don't need to register the custom Log4j 2 plugins, + // since they will be registered by the Log4j 2 `GraalvmPluginProcessor`. + } + + /** + * Registers the type to prevent GraalVM from removing it during the native build. + * @param hints the runtime hints to register with + * @param classLoader the class loader to use for type resolution + * @param typeName the name of the type to register + */ + private void registerTypeForReachability(RuntimeHints hints, @Nullable ClassLoader classLoader, String typeName) { + hints.reflection().registerTypeIfPresent(classLoader, typeName); + } + +} diff --git a/core/spring-boot/src/main/resources/META-INF/spring/aot.factories b/core/spring-boot/src/main/resources/META-INF/spring/aot.factories index b661eb51c41d..4a0efd5e6c69 100644 --- a/core/spring-boot/src/main/resources/META-INF/spring/aot.factories +++ b/core/spring-boot/src/main/resources/META-INF/spring/aot.factories @@ -6,6 +6,7 @@ org.springframework.boot.context.config.ConfigDataLocationRuntimeHints,\ org.springframework.boot.context.config.ConfigDataPropertiesRuntimeHints,\ org.springframework.boot.env.PropertySourceRuntimeHints,\ org.springframework.boot.logging.java.JavaLoggingSystemRuntimeHints,\ +org.springframework.boot.logging.log4j2.Log4J2RuntimeHints,\ org.springframework.boot.logging.logback.LogbackRuntimeHints,\ org.springframework.boot.logging.structured.ElasticCommonSchemaProperties$ElasticCommonSchemaPropertiesRuntimeHints,\ org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties$GraylogExtendedLogFormatPropertiesRuntimeHints,\ diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHintsTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHintsTests.java new file mode 100644 index 000000000000..0af647c10df7 --- /dev/null +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHintsTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.logging.log4j2; + +import org.apache.logging.log4j.core.impl.Log4jContextFactory; +import org.apache.logging.log4j.jul.Log4jBridgeHandler; +import org.apache.logging.log4j.jul.LogManager; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Log4J2RuntimeHints}. + * + * @author Piotr P. Karwasz + */ +class Log4J2RuntimeHintsTests { + + @Test + void registersHintsForTypesCheckedByLog4J2LoggingSystem() { + ReflectionHints reflection = registerHints(); + // Once Log4j Core is reachable, GraalVM will automatically + // add reachability metadata embedded in the Log4j Core jar and extensions. + assertThat(reflection.getTypeHint(Log4jContextFactory.class)).isNotNull(); + assertThat(reflection.getTypeHint(Log4jBridgeHandler.class)).isNotNull(); + assertThat(reflection.getTypeHint(LogManager.class)).isNotNull(); + } + + /** + * + */ + @Test + void registersHintsForConfigurationFileParsers() { + ReflectionHints reflection = registerHints(); + // JSON + assertThat(reflection.getTypeHint(TypeReference.of("com.fasterxml.jackson.databind.ObjectMapper"))).isNotNull(); + // YAML + assertThat(reflection.getTypeHint(TypeReference.of("com.fasterxml.jackson.dataformat.yaml.YAMLMapper"))) + .isNotNull(); + } + + @Test + void doesNotRegisterHintsWhenLog4jCoreIsNotAvailable() { + RuntimeHints hints = new RuntimeHints(); + new Log4J2RuntimeHints().registerHints(hints, ClassLoader.getPlatformClassLoader()); + assertThat(hints.reflection().typeHints()).isEmpty(); + } + + private ReflectionHints registerHints() { + RuntimeHints hints = new RuntimeHints(); + new Log4J2RuntimeHints().registerHints(hints, getClass().getClassLoader()); + return hints.reflection(); + } + +} From f3bdd0244362eb3feebef4dee37afcf39fda69a5 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 7 Oct 2025 21:54:02 +0200 Subject: [PATCH 2/2] Fix copyright date Signed-off-by: Piotr P. Karwasz --- .../springframework/boot/logging/log4j2/Log4J2RuntimeHints.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java index ad2739c7fe95..8d12e1c12dcd 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2025-present the original author or authors. + * Copyright 2012-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.