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..8d12e1c12dcd --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2RuntimeHints.java @@ -0,0 +1,67 @@ +/* + * 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.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(); + } + +}