Skip to content

Commit 1e30c62

Browse files
committed
Add AOT runtime hints for Log4j Core 2
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 #46334, which configures Log4j Core’s `GraalVmProcessor` to generate reachability metadata for Spring Boot’s custom Log4j plugins. Signed-off-by: Piotr P. Karwasz <[email protected]>
1 parent 1758f50 commit 1e30c62

File tree

5 files changed

+158
-12
lines changed

5 files changed

+158
-12
lines changed

core/spring-boot/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ dependencies {
6969
testImplementation("org.hibernate.validator:hibernate-validator")
7070
testImplementation("org.jboss.logging:jboss-logging")
7171
testImplementation("org.springframework.data:spring-data-r2dbc")
72+
73+
// Used in Log4J2RuntimeHintsTests
74+
testRuntimeOnly("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
7275
}
7376

7477
def syncJavaTemplates = tasks.register("syncJavaTemplates", Sync) {

core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
8989

9090
private static final String OPTIONAL_PREFIX = "optional:";
9191

92-
private static final String LOG4J_BRIDGE_HANDLER = "org.apache.logging.log4j.jul.Log4jBridgeHandler";
93-
94-
private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager";
95-
9692
private static final SpringEnvironmentPropertySource propertySource = new SpringEnvironmentPropertySource();
9793

9894
static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class,
@@ -123,21 +119,25 @@ public Log4J2LoggingSystem(ClassLoader classLoader) {
123119
protected String[] getStandardConfigLocations() {
124120
List<String> locations = new ArrayList<>();
125121
locations.add("log4j2-test.properties");
126-
if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) {
122+
if (isClassAvailable(Log4J2RuntimeHints.YAML_TREE_PARSER_V2)) {
127123
Collections.addAll(locations, "log4j2-test.yaml", "log4j2-test.yml");
128124
}
129-
if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
125+
if (isClassAvailable(Log4J2RuntimeHints.JSON_TREE_PARSER_V2)) {
130126
Collections.addAll(locations, "log4j2-test.json", "log4j2-test.jsn");
131127
}
132-
locations.add("log4j2-test.xml");
128+
if (isClassAvailable(Log4J2RuntimeHints.XML_TREE_PARSER)) {
129+
locations.add("log4j2-test.xml");
130+
}
133131
locations.add("log4j2.properties");
134-
if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) {
132+
if (isClassAvailable(Log4J2RuntimeHints.YAML_TREE_PARSER_V2)) {
135133
Collections.addAll(locations, "log4j2.yaml", "log4j2.yml");
136134
}
137-
if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
135+
if (isClassAvailable(Log4J2RuntimeHints.JSON_TREE_PARSER_V2)) {
138136
Collections.addAll(locations, "log4j2.json", "log4j2.jsn");
139137
}
140-
locations.add("log4j2.xml");
138+
if (isClassAvailable(Log4J2RuntimeHints.XML_TREE_PARSER)) {
139+
locations.add("log4j2.xml");
140+
}
141141
String propertyDefinedLocation = new PropertiesUtil(new Properties())
142142
.getStringProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
143143
if (propertyDefinedLocation != null) {
@@ -185,11 +185,11 @@ private boolean isJulUsingASingleConsoleHandlerAtMost() {
185185

186186
private boolean isLog4jLogManagerInstalled() {
187187
final String logManagerClassName = java.util.logging.LogManager.getLogManager().getClass().getName();
188-
return LOG4J_LOG_MANAGER.equals(logManagerClassName);
188+
return Log4J2RuntimeHints.LOG4J_LOG_MANAGER.equals(logManagerClassName);
189189
}
190190

191191
private boolean isLog4jBridgeHandlerAvailable() {
192-
return ClassUtils.isPresent(LOG4J_BRIDGE_HANDLER, getClassLoader());
192+
return isClassAvailable(Log4J2RuntimeHints.LOG4J_BRIDGE_HANDLER);
193193
}
194194

195195
private void removeLog4jBridgeHandler() {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.logging.log4j2;
18+
19+
import org.springframework.aot.hint.RuntimeHints;
20+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
21+
import org.springframework.util.ClassUtils;
22+
23+
/**
24+
* {@link RuntimeHintsRegistrar} implementation for {@link Log4J2LoggingSystem}.
25+
*
26+
* @author Piotr P. Karwasz
27+
*/
28+
class Log4J2RuntimeHints implements RuntimeHintsRegistrar {
29+
30+
// Log4j2LoggingSystem checks for the presence of these classes reflectively.
31+
static final String PROVIDER = "org.apache.logging.log4j.core.impl.Log4jProvider";
32+
33+
// Tree parsers used by Log4j 2 for configuration files.
34+
static final String JSON_TREE_PARSER_V2 = "com.fasterxml.jackson.databind.ObjectMapper";
35+
static final String XML_TREE_PARSER = "javax.xml.parsers.DocumentBuilderFactory";
36+
static final String YAML_TREE_PARSER_V2 = "com.fasterxml.jackson.dataformat.yaml.YAMLMapper";
37+
38+
// JUL implementations that use Log4j 2 API.
39+
static final String LOG4J_BRIDGE_HANDLER = "org.apache.logging.log4j.jul.Log4jBridgeHandler";
40+
static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager";
41+
42+
@Override
43+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
44+
if (!ClassUtils.isPresent(PROVIDER, classLoader)) {
45+
return;
46+
}
47+
registerTypeForReachability(hints, classLoader, PROVIDER);
48+
// Register default Log4j2 configuration files
49+
hints.resources().registerPattern("org/springframework/boot/logging/log4j2/log4j2.xml");
50+
hints.resources().registerPattern("org/springframework/boot/logging/log4j2/log4j2-file.xml");
51+
hints.resources().registerPattern("log4j2.springboot");
52+
// Declares the types that Log4j2LoggingSystem checks for existence reflectively.
53+
registerTypeForReachability(hints, classLoader, JSON_TREE_PARSER_V2);
54+
registerTypeForReachability(hints, classLoader, XML_TREE_PARSER);
55+
registerTypeForReachability(hints, classLoader, YAML_TREE_PARSER_V2);
56+
registerTypeForReachability(hints, classLoader, LOG4J_BRIDGE_HANDLER);
57+
registerTypeForReachability(hints, classLoader, LOG4J_LOG_MANAGER);
58+
// Don't need to register the custom Log4j 2 plugins,
59+
// since they will be registered by the Log4j 2 `GraalvmPluginProcessor`.
60+
}
61+
62+
/**
63+
* Registers the type to prevent GraalVM from removing it during the native build.
64+
*/
65+
private void registerTypeForReachability(RuntimeHints hints, ClassLoader classLoader, String typeName) {
66+
hints.reflection().registerTypeIfPresent(classLoader, typeName);
67+
}
68+
69+
}

core/spring-boot/src/main/resources/META-INF/spring/aot.factories

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ org.springframework.boot.context.config.ConfigDataPropertiesRuntimeHints,\
77
org.springframework.boot.env.PropertySourceRuntimeHints,\
88
org.springframework.boot.json.JacksonRuntimeHints,\
99
org.springframework.boot.logging.java.JavaLoggingSystemRuntimeHints,\
10+
org.springframework.boot.logging.log4j2.Log4J2RuntimeHints,\
1011
org.springframework.boot.logging.logback.LogbackRuntimeHints,\
1112
org.springframework.boot.logging.structured.ElasticCommonSchemaProperties$ElasticCommonSchemaPropertiesRuntimeHints,\
1213
org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties$GraylogExtendedLogFormatPropertiesRuntimeHints,\
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.logging.log4j2;
18+
19+
import javax.xml.parsers.DocumentBuilderFactory;
20+
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import org.apache.logging.log4j.core.impl.Log4jProvider;
23+
import org.apache.logging.log4j.jul.Log4jBridgeHandler;
24+
import org.apache.logging.log4j.jul.LogManager;
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.aot.hint.ReflectionHints;
28+
import org.springframework.aot.hint.RuntimeHints;
29+
import org.springframework.aot.hint.TypeReference;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
33+
/**
34+
* Tests for {@link Log4J2RuntimeHints}.
35+
*
36+
* @author Piotr P. Karwasz
37+
*/
38+
class Log4J2RuntimeHintsTests {
39+
40+
@Test
41+
void registersHintsForTypesCheckedByLog4J2LoggingSystem() {
42+
ReflectionHints reflection = registerHints();
43+
assertThat(reflection.getTypeHint(Log4jProvider.class)).isNotNull();
44+
assertThat(reflection.getTypeHint(Log4jBridgeHandler.class)).isNotNull();
45+
assertThat(reflection.getTypeHint(LogManager.class)).isNotNull();
46+
}
47+
48+
@Test
49+
void registersHintsForConfigurationFileParsers() {
50+
ReflectionHints reflection = registerHints();
51+
// JSON
52+
assertThat(reflection.getTypeHint(ObjectMapper.class)).isNotNull();
53+
// XML
54+
assertThat(reflection.getTypeHint(DocumentBuilderFactory.class)).isNotNull();
55+
// YAML
56+
assertThat(reflection.getTypeHint(TypeReference.of("com.fasterxml.jackson.dataformat.yaml.YAMLMapper")))
57+
.isNotNull();
58+
}
59+
60+
@Test
61+
void doesNotRegisterHintsWhenLog4jCoreIsNotAvailable() {
62+
RuntimeHints hints = new RuntimeHints();
63+
new Log4J2RuntimeHints().registerHints(hints, ClassLoader.getPlatformClassLoader());
64+
assertThat(hints.reflection().typeHints()).isEmpty();
65+
}
66+
67+
private ReflectionHints registerHints() {
68+
RuntimeHints hints = new RuntimeHints();
69+
new Log4J2RuntimeHints().registerHints(hints, getClass().getClassLoader());
70+
return hints.reflection();
71+
}
72+
73+
}

0 commit comments

Comments
 (0)