Tolerate multiple identical configurations in Logback AOT contribution

Fixes gh-36997
This commit is contained in:
Andy Wilkinson 2024-07-12 11:51:38 +01:00
parent b605b86b32
commit acdaa6dc35
2 changed files with 95 additions and 17 deletions

View File

@ -16,7 +16,6 @@
package org.springframework.boot.logging.logback;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -25,6 +24,7 @@ import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@ -51,6 +51,8 @@ import ch.qos.logback.core.spi.ContextAware;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.util.AggregationType;
import org.springframework.aot.generate.GeneratedFiles.FileHandler;
import org.springframework.aot.generate.GeneratedFiles.Kind;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.SerializationHints;
@ -62,11 +64,11 @@ import org.springframework.core.CollectionFactory;
import org.springframework.core.NativeDetector;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.function.SingletonSupplier;
import org.springframework.util.function.ThrowingConsumer;
/**
* Extended version of the Logback {@link JoranConfigurator} that adds additional Spring
@ -176,15 +178,10 @@ class SpringBootJoranConfigurator extends JoranConfigurator {
}
private void writeTo(GenerationContext generationContext) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
output.writeObject(this.model);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
Resource modelResource = new ByteArrayResource(bytes.toByteArray());
generationContext.getGeneratedFiles().addResourceFile(MODEL_RESOURCE_LOCATION, modelResource);
byte[] serializedModel = serializeModel();
generationContext.getGeneratedFiles()
.handleFile(Kind.RESOURCE, MODEL_RESOURCE_LOCATION,
new RequireNewOrMatchingContentFileHandler(serializedModel));
generationContext.getRuntimeHints().resources().registerPattern(MODEL_RESOURCE_LOCATION);
SerializationHints serializationHints = generationContext.getRuntimeHints().serialization();
serializationTypes(this.model).forEach(serializationHints::registerType);
@ -194,6 +191,17 @@ class SpringBootJoranConfigurator extends JoranConfigurator {
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
}
private byte[] serializeModel() {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
output.writeObject(this.model);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
return bytes.toByteArray();
}
@SuppressWarnings("unchecked")
private Set<Class<? extends Serializable>> serializationTypes(Model model) {
Set<Class<? extends Serializable>> modelClasses = new HashSet<>();
@ -389,7 +397,9 @@ class SpringBootJoranConfigurator extends JoranConfigurator {
private void save(GenerationContext generationContext) {
Map<String, String> registryMap = getRegistryMap();
generationContext.getGeneratedFiles().addResourceFile(RESOURCE_LOCATION, () -> asInputStream(registryMap));
byte[] rules = asBytes(registryMap);
generationContext.getGeneratedFiles()
.handleFile(Kind.RESOURCE, RESOURCE_LOCATION, new RequireNewOrMatchingContentFileHandler(rules));
generationContext.getRuntimeHints().resources().registerPattern(RESOURCE_LOCATION);
for (String ruleClassName : registryMap.values()) {
generationContext.getRuntimeHints()
@ -398,7 +408,7 @@ class SpringBootJoranConfigurator extends JoranConfigurator {
}
}
private InputStream asInputStream(Map<String, String> patternRuleRegistry) {
private byte[] asBytes(Map<String, String> patternRuleRegistry) {
Properties properties = CollectionFactory.createSortedProperties(true);
patternRuleRegistry.forEach(properties::setProperty);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
@ -408,7 +418,32 @@ class SpringBootJoranConfigurator extends JoranConfigurator {
catch (IOException ex) {
throw new RuntimeException(ex);
}
return new ByteArrayInputStream(bytes.toByteArray());
return bytes.toByteArray();
}
}
private static final class RequireNewOrMatchingContentFileHandler implements ThrowingConsumer<FileHandler> {
private final byte[] newContent;
private RequireNewOrMatchingContentFileHandler(byte[] newContent) {
this.newContent = newContent;
}
@Override
public void acceptWithException(FileHandler file) throws Exception {
if (file.exists()) {
byte[] existingContent = file.getContent().getInputStream().readAllBytes();
if (!Arrays.equals(this.newContent, existingContent)) {
throw new IllegalStateException(
"Logging configuration differs from the configuration that has already been written. "
+ "Update your logging configuration so that it is the same for each context");
}
}
else {
file.create(new ByteArrayResource(this.newContent));
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -30,6 +30,7 @@ import java.util.stream.Stream;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.model.RootLoggerModel;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.Layout;
@ -61,6 +62,7 @@ import org.springframework.boot.logging.logback.SpringBootJoranConfigurator.Logb
import org.springframework.core.io.InputStreamSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link LogbackConfigurationAotContribution}.
@ -93,6 +95,37 @@ class LogbackConfigurationAotContributionTests {
assertThat(patternRules).isEmpty();
}
@Test
void contributionOfBasicModelThatMatchesExistingModel() {
TestGenerationContext generationContext = new TestGenerationContext();
Model model = new Model();
applyContribution(model, generationContext);
applyContribution(model, generationContext);
InMemoryGeneratedFiles generatedFiles = generationContext.getGeneratedFiles();
assertThat(generatedFiles).has(resource("META-INF/spring/logback-model"));
assertThat(generatedFiles).has(resource("META-INF/spring/logback-pattern-rules"));
SerializationHints serializationHints = generationContext.getRuntimeHints().serialization();
assertThat(serializationHints.javaSerializationHints()
.map(JavaSerializationHint::getType)
.map(TypeReference::getName))
.containsExactlyInAnyOrder(namesOf(Model.class, ArrayList.class, Boolean.class, Integer.class));
assertThat(generationContext.getRuntimeHints().reflection().typeHints()).isEmpty();
Properties patternRules = load(
generatedFiles.getGeneratedFile(Kind.RESOURCE, "META-INF/spring/logback-pattern-rules"));
assertThat(patternRules).isEmpty();
}
@Test
void contributionOfBasicModelThatDiffersFromExistingModelThrows() {
TestGenerationContext generationContext = new TestGenerationContext();
applyContribution(new Model(), generationContext);
Model model = new Model();
model.addSubModel(new RootLoggerModel());
assertThatIllegalStateException().isThrownBy(() -> applyContribution(model, generationContext))
.withMessage("Logging configuration differs from the configuration that has already been written. "
+ "Update your logging configuration so that it is the same for each context");
}
@Test
void patternRulesAreStoredAndRegisteredForReflection() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
@ -238,6 +271,18 @@ class LogbackConfigurationAotContributionTests {
}
private TestGenerationContext applyContribution(Model model, Consumer<LoggerContext> contextCustomizer) {
TestGenerationContext generationContext = new TestGenerationContext();
applyContribution(model, contextCustomizer, generationContext);
return generationContext;
}
private void applyContribution(Model model, TestGenerationContext generationContext) {
applyContribution(model, (context) -> {
}, generationContext);
}
private void applyContribution(Model model, Consumer<LoggerContext> contextCustomizer,
TestGenerationContext generationContext) {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
contextCustomizer.accept(context);
SpringBootJoranConfigurator configurator = new SpringBootJoranConfigurator(null);
@ -245,9 +290,7 @@ class LogbackConfigurationAotContributionTests {
withSystemProperty("spring.aot.processing", "true", () -> configurator.processModel(model));
LogbackConfigurationAotContribution contribution = (LogbackConfigurationAotContribution) context
.getObject(BeanFactoryInitializationAotContribution.class.getName());
TestGenerationContext generationContext = new TestGenerationContext();
contribution.applyTo(generationContext, null);
return generationContext;
}
private String[] namesOf(Class<?>... types) {