diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHintsRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHintsRegistrar.java new file mode 100644 index 0000000000..f36722c136 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHintsRegistrar.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2022 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.logback; + +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.pattern.CallerDataConverter; +import ch.qos.logback.classic.pattern.ClassOfCallerConverter; +import ch.qos.logback.classic.pattern.ContextNameConverter; +import ch.qos.logback.classic.pattern.DateConverter; +import ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter; +import ch.qos.logback.classic.pattern.FileOfCallerConverter; +import ch.qos.logback.classic.pattern.LevelConverter; +import ch.qos.logback.classic.pattern.LineOfCallerConverter; +import ch.qos.logback.classic.pattern.LineSeparatorConverter; +import ch.qos.logback.classic.pattern.LocalSequenceNumberConverter; +import ch.qos.logback.classic.pattern.LoggerConverter; +import ch.qos.logback.classic.pattern.MDCConverter; +import ch.qos.logback.classic.pattern.MarkerConverter; +import ch.qos.logback.classic.pattern.MessageConverter; +import ch.qos.logback.classic.pattern.MethodOfCallerConverter; +import ch.qos.logback.classic.pattern.NopThrowableInformationConverter; +import ch.qos.logback.classic.pattern.PropertyConverter; +import ch.qos.logback.classic.pattern.RelativeTimeConverter; +import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter; +import ch.qos.logback.classic.pattern.SyslogStartConverter; +import ch.qos.logback.classic.pattern.ThreadConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.core.rolling.helper.DateTokenConverter; +import ch.qos.logback.core.rolling.helper.IntegerTokenConverter; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeHint.Builder; +import org.springframework.aot.hint.TypeReference; +import org.springframework.util.ClassUtils; + +/** + * {@link RuntimeHintsRegistrar} for Logback. + * + * @author Andy Wilkinson + */ +class LogbackRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + if (!ClassUtils.isPresent("ch.qos.logback.classic.LoggerContext", classLoader)) { + return; + } + ReflectionHints reflection = hints.reflection(); + registerHintsForLogbackLoggingSystemTypeChecks(reflection); + registerHintsForBuiltInLogbackConverters(reflection); + registerHintsForSpringBootConverters(reflection); + } + + private void registerHintsForLogbackLoggingSystemTypeChecks(ReflectionHints reflection) { + Consumer defaultHint = (hint) -> { + }; + reflection.registerType(LoggerContext.class, defaultHint); + reflection.registerType(SLF4JBridgeHandler.class, defaultHint); + } + + private void registerHintsForBuiltInLogbackConverters(ReflectionHints reflection) { + registerForPublicConstructorInvocation(reflection, CallerDataConverter.class, ClassOfCallerConverter.class, + ContextNameConverter.class, DateConverter.class, DateTokenConverter.class, + ExtendedThrowableProxyConverter.class, FileOfCallerConverter.class, IntegerTokenConverter.class, + LevelConverter.class, LineOfCallerConverter.class, LineSeparatorConverter.class, + LocalSequenceNumberConverter.class, LoggerConverter.class, MarkerConverter.class, MDCConverter.class, + MessageConverter.class, MethodOfCallerConverter.class, NopThrowableInformationConverter.class, + PropertyConverter.class, RelativeTimeConverter.class, RootCauseFirstThrowableProxyConverter.class, + SyslogStartConverter.class, ThreadConverter.class, ThrowableProxyConverter.class); + } + + private void registerHintsForSpringBootConverters(ReflectionHints reflection) { + registerForPublicConstructorInvocation(reflection, ColorConverter.class, + ExtendedWhitespaceThrowableProxyConverter.class, WhitespaceThrowableProxyConverter.class); + } + + private void registerForPublicConstructorInvocation(ReflectionHints reflection, Class... classes) { + reflection.registerTypes(typeReferences(classes), + (hint) -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + } + + private Iterable typeReferences(Class... classes) { + return Stream.of(classes).map(TypeReference::of).collect(Collectors.toList()); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories index 328698ffd4..eb0fe29494 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories @@ -1,2 +1,4 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ +org.springframework.boot.logging.logback.LogbackRuntimeHintsRegistrar org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackRuntimeHintsRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackRuntimeHintsRegistrarTests.java new file mode 100644 index 0000000000..47645566eb --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackRuntimeHintsRegistrarTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2022 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.logback; + +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.pattern.ClassicConverter; +import org.junit.jupiter.api.Test; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeHint; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LogbackRuntimeHintsRegistrar}. + * + * @author Andy Wilkinson + */ +class LogbackRuntimeHintsRegistrarTests { + + @Test + void registersHintsForTypesCheckedByLogbackLoggingSystem() throws Exception { + ReflectionHints reflection = registerHints(); + assertThat(reflection.getTypeHint(LoggerContext.class)).isNotNull(); + assertThat(reflection.getTypeHint(SLF4JBridgeHandler.class)).isNotNull(); + } + + @Test + void registersHintsForBuiltInLogbackConverters() throws Exception { + RuntimeHints hints = new RuntimeHints(); + new LogbackRuntimeHintsRegistrar().registerHints(hints, getClass().getClassLoader()); + ReflectionHints reflection = registerHints(); + assertThat(logbackConverters()).allSatisfy(registeredForPublicConstructorInvocation(reflection)); + } + + @Test + void registersHintsForSpringBootConverters() throws IOException, ClassNotFoundException, LinkageError { + ReflectionHints reflection = registerHints(); + assertThat(List.of(ColorConverter.class, ExtendedWhitespaceThrowableProxyConverter.class, + WhitespaceThrowableProxyConverter.class)) + .allSatisfy(registeredForPublicConstructorInvocation(reflection)); + } + + @Test + void doesNotRegisterHintsWhenLoggerContextIsNotAvailable() { + RuntimeHints hints = new RuntimeHints(); + new LogbackRuntimeHintsRegistrar().registerHints(hints, ClassLoader.getPlatformClassLoader()); + assertThat(hints.reflection().typeHints()).isEmpty(); + } + + private ReflectionHints registerHints() { + RuntimeHints hints = new RuntimeHints(); + new LogbackRuntimeHintsRegistrar().registerHints(hints, getClass().getClassLoader()); + ReflectionHints reflection = hints.reflection(); + return reflection; + } + + private Consumer> registeredForPublicConstructorInvocation(ReflectionHints reflection) { + return (converter) -> { + TypeHint typeHint = reflection.getTypeHint(converter); + assertThat(typeHint).isNotNull(); + assertThat(typeHint.getMemberCategories()).containsExactly(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + }; + } + + private List> logbackConverters() throws IOException { + ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] converterResources = resolver + .getResources("classpath:ch/qos/logback/classic/pattern/*Converter.class"); + return Stream.of(converterResources).map(this::className).map(this::load).filter(this::isConcreteConverter) + .collect(Collectors.toList()); + } + + private String className(Resource resource) { + String filename = resource.getFilename(); + filename = filename.substring(0, filename.length() - ".class".length()); + return "ch.qos.logback.classic.pattern." + filename; + } + + private Class load(String className) { + try { + return ClassUtils.forName(className, getClass().getClassLoader()); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private boolean isConcreteConverter(Class candidate) { + return ClassicConverter.class.isAssignableFrom(candidate) && !Modifier.isAbstract(candidate.getModifiers()); + } + +}