diff --git a/core/spring-boot/build.gradle b/core/spring-boot/build.gradle index 44cb7ba5066c..66e40b7bafb6 100644 --- a/core/spring-boot/build.gradle +++ b/core/spring-boot/build.gradle @@ -69,6 +69,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 e697f50b4ae0..490fa20324d8 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 @@ -89,10 +89,6 @@ 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"; - - private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; - private static final SpringEnvironmentPropertySource propertySource = new SpringEnvironmentPropertySource(); static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class, @@ -123,18 +119,18 @@ public Log4J2LoggingSystem(ClassLoader classLoader) { protected String[] getStandardConfigLocations() { List locations = new ArrayList<>(); locations.add("log4j2-test.properties"); - if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) { + if (isClassAvailable(Log4J2RuntimeHints.YAML_TREE_PARSER_V2)) { Collections.addAll(locations, "log4j2-test.yaml", "log4j2-test.yml"); } - if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) { + if (isClassAvailable(Log4J2RuntimeHints.JSON_TREE_PARSER_V2)) { Collections.addAll(locations, "log4j2-test.json", "log4j2-test.jsn"); } locations.add("log4j2-test.xml"); locations.add("log4j2.properties"); - if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) { + if (isClassAvailable(Log4J2RuntimeHints.YAML_TREE_PARSER_V2)) { Collections.addAll(locations, "log4j2.yaml", "log4j2.yml"); } - if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) { + if (isClassAvailable(Log4J2RuntimeHints.JSON_TREE_PARSER_V2)) { Collections.addAll(locations, "log4j2.json", "log4j2.jsn"); } locations.add("log4j2.xml"); @@ -185,11 +181,11 @@ private boolean isJulUsingASingleConsoleHandlerAtMost() { private boolean isLog4jLogManagerInstalled() { final String logManagerClassName = java.util.logging.LogManager.getLogManager().getClass().getName(); - return LOG4J_LOG_MANAGER.equals(logManagerClassName); + return Log4J2RuntimeHints.LOG4J_LOG_MANAGER.equals(logManagerClassName); } private boolean isLog4jBridgeHandlerAvailable() { - return ClassUtils.isPresent(LOG4J_BRIDGE_HANDLER, getClassLoader()); + return ClassUtils.isPresent(Log4J2RuntimeHints.LOG4J_BRIDGE_HANDLER, getClassLoader()); } private void removeLog4jBridgeHandler() { 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..040dc24c7aa6 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java @@ -0,0 +1,70 @@ +/* + * 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.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 { + + // Log4j2LoggingSystem checks for the presence of these classes reflectively. + static final String PROVIDER = "org.apache.logging.log4j.core.impl.Log4jProvider"; + + // Tree parsers used by Log4j 2 for configuration files. + static final String JSON_TREE_PARSER_V2 = "com.fasterxml.jackson.databind.ObjectMapper"; + static final String YAML_TREE_PARSER_V2 = "com.fasterxml.jackson.dataformat.yaml.YAMLMapper"; + + // JUL implementations that use Log4j 2 API. + static final String LOG4J_BRIDGE_HANDLER = "org.apache.logging.log4j.jul.Log4jBridgeHandler"; + static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + if (!ClassUtils.isPresent(PROVIDER, classLoader)) { + return; + } + registerTypeForReachability(hints, classLoader, PROVIDER); + // 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, JSON_TREE_PARSER_V2); + registerTypeForReachability(hints, classLoader, YAML_TREE_PARSER_V2); + registerTypeForReachability(hints, classLoader, LOG4J_BRIDGE_HANDLER); + registerTypeForReachability(hints, classLoader, 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, 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 61390a75beb4..77c395bacfff 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 @@ -7,6 +7,7 @@ org.springframework.boot.context.config.ConfigDataPropertiesRuntimeHints,\ org.springframework.boot.env.PropertySourceRuntimeHints,\ org.springframework.boot.json.JacksonRuntimeHints,\ 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/Log4J2LoggingSystemTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index 996db9c223a6..59fc9973fbb5 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -309,7 +309,7 @@ void configLocationsWithJacksonDatabind() { @Test void configLocationsWithJacksonDataformatYaml() { - this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLParser"); + this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLMapper"); assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties", "log4j2-test.yaml", "log4j2-test.yml", "log4j2-test.xml", "log4j2.properties", "log4j2.yaml", "log4j2.yml", "log4j2.xml"); @@ -317,7 +317,7 @@ void configLocationsWithJacksonDataformatYaml() { @Test void configLocationsWithJacksonDatabindAndDataformatYaml() { - this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLParser", + this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLMapper", ObjectMapper.class.getName()); assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties", "log4j2-test.yaml", "log4j2-test.yml", "log4j2-test.json", "log4j2-test.jsn", "log4j2-test.xml", 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..7e30c52f254e --- /dev/null +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHintsTests.java @@ -0,0 +1,69 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.core.impl.Log4jProvider; +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(); + assertThat(reflection.getTypeHint(Log4jProvider.class)).isNotNull(); + assertThat(reflection.getTypeHint(Log4jBridgeHandler.class)).isNotNull(); + assertThat(reflection.getTypeHint(LogManager.class)).isNotNull(); + } + + @Test + void registersHintsForConfigurationFileParsers() { + ReflectionHints reflection = registerHints(); + // JSON + assertThat(reflection.getTypeHint(ObjectMapper.class)).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(); + } + +}