Skip to content

Commit acdaa6d

Browse files
committed
Tolerate multiple identical configurations in Logback AOT contribution
Fixes gh-36997
1 parent b605b86 commit acdaa6d

File tree

2 files changed

+95
-17
lines changed

2 files changed

+95
-17
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.boot.logging.logback;
1818

19-
import java.io.ByteArrayInputStream;
2019
import java.io.ByteArrayOutputStream;
2120
import java.io.IOException;
2221
import java.io.InputStream;
@@ -25,6 +24,7 @@
2524
import java.io.Serializable;
2625
import java.lang.reflect.Method;
2726
import java.lang.reflect.Modifier;
27+
import java.util.Arrays;
2828
import java.util.Collection;
2929
import java.util.HashMap;
3030
import java.util.HashSet;
@@ -51,6 +51,8 @@
5151
import ch.qos.logback.core.spi.ContextAwareBase;
5252
import ch.qos.logback.core.util.AggregationType;
5353

54+
import org.springframework.aot.generate.GeneratedFiles.FileHandler;
55+
import org.springframework.aot.generate.GeneratedFiles.Kind;
5456
import org.springframework.aot.generate.GenerationContext;
5557
import org.springframework.aot.hint.MemberCategory;
5658
import org.springframework.aot.hint.SerializationHints;
@@ -62,11 +64,11 @@
6264
import org.springframework.core.NativeDetector;
6365
import org.springframework.core.io.ByteArrayResource;
6466
import org.springframework.core.io.ClassPathResource;
65-
import org.springframework.core.io.Resource;
6667
import org.springframework.core.io.support.PropertiesLoaderUtils;
6768
import org.springframework.util.ClassUtils;
6869
import org.springframework.util.ReflectionUtils;
6970
import org.springframework.util.function.SingletonSupplier;
71+
import org.springframework.util.function.ThrowingConsumer;
7072

7173
/**
7274
* Extended version of the Logback {@link JoranConfigurator} that adds additional Spring
@@ -176,15 +178,10 @@ private ModelWriter(Model model, ModelInterpretationContext modelInterpretationC
176178
}
177179

178180
private void writeTo(GenerationContext generationContext) {
179-
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
180-
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
181-
output.writeObject(this.model);
182-
}
183-
catch (IOException ex) {
184-
throw new RuntimeException(ex);
185-
}
186-
Resource modelResource = new ByteArrayResource(bytes.toByteArray());
187-
generationContext.getGeneratedFiles().addResourceFile(MODEL_RESOURCE_LOCATION, modelResource);
181+
byte[] serializedModel = serializeModel();
182+
generationContext.getGeneratedFiles()
183+
.handleFile(Kind.RESOURCE, MODEL_RESOURCE_LOCATION,
184+
new RequireNewOrMatchingContentFileHandler(serializedModel));
188185
generationContext.getRuntimeHints().resources().registerPattern(MODEL_RESOURCE_LOCATION);
189186
SerializationHints serializationHints = generationContext.getRuntimeHints().serialization();
190187
serializationTypes(this.model).forEach(serializationHints::registerType);
@@ -194,6 +191,17 @@ private void writeTo(GenerationContext generationContext) {
194191
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
195192
}
196193

194+
private byte[] serializeModel() {
195+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
196+
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
197+
output.writeObject(this.model);
198+
}
199+
catch (IOException ex) {
200+
throw new RuntimeException(ex);
201+
}
202+
return bytes.toByteArray();
203+
}
204+
197205
@SuppressWarnings("unchecked")
198206
private Set<Class<? extends Serializable>> serializationTypes(Model model) {
199207
Set<Class<? extends Serializable>> modelClasses = new HashSet<>();
@@ -389,7 +397,9 @@ private Map<String, String> getRegistryMap() {
389397

390398
private void save(GenerationContext generationContext) {
391399
Map<String, String> registryMap = getRegistryMap();
392-
generationContext.getGeneratedFiles().addResourceFile(RESOURCE_LOCATION, () -> asInputStream(registryMap));
400+
byte[] rules = asBytes(registryMap);
401+
generationContext.getGeneratedFiles()
402+
.handleFile(Kind.RESOURCE, RESOURCE_LOCATION, new RequireNewOrMatchingContentFileHandler(rules));
393403
generationContext.getRuntimeHints().resources().registerPattern(RESOURCE_LOCATION);
394404
for (String ruleClassName : registryMap.values()) {
395405
generationContext.getRuntimeHints()
@@ -398,7 +408,7 @@ private void save(GenerationContext generationContext) {
398408
}
399409
}
400410

401-
private InputStream asInputStream(Map<String, String> patternRuleRegistry) {
411+
private byte[] asBytes(Map<String, String> patternRuleRegistry) {
402412
Properties properties = CollectionFactory.createSortedProperties(true);
403413
patternRuleRegistry.forEach(properties::setProperty);
404414
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
@@ -408,7 +418,32 @@ private InputStream asInputStream(Map<String, String> patternRuleRegistry) {
408418
catch (IOException ex) {
409419
throw new RuntimeException(ex);
410420
}
411-
return new ByteArrayInputStream(bytes.toByteArray());
421+
return bytes.toByteArray();
422+
}
423+
424+
}
425+
426+
private static final class RequireNewOrMatchingContentFileHandler implements ThrowingConsumer<FileHandler> {
427+
428+
private final byte[] newContent;
429+
430+
private RequireNewOrMatchingContentFileHandler(byte[] newContent) {
431+
this.newContent = newContent;
432+
}
433+
434+
@Override
435+
public void acceptWithException(FileHandler file) throws Exception {
436+
if (file.exists()) {
437+
byte[] existingContent = file.getContent().getInputStream().readAllBytes();
438+
if (!Arrays.equals(this.newContent, existingContent)) {
439+
throw new IllegalStateException(
440+
"Logging configuration differs from the configuration that has already been written. "
441+
+ "Update your logging configuration so that it is the same for each context");
442+
}
443+
}
444+
else {
445+
file.create(new ByteArrayResource(this.newContent));
446+
}
412447
}
413448

414449
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030

3131
import ch.qos.logback.classic.LoggerContext;
3232
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
33+
import ch.qos.logback.classic.model.RootLoggerModel;
3334
import ch.qos.logback.core.CoreConstants;
3435
import ch.qos.logback.core.FileAppender;
3536
import ch.qos.logback.core.Layout;
@@ -61,6 +62,7 @@
6162
import org.springframework.core.io.InputStreamSource;
6263

6364
import static org.assertj.core.api.Assertions.assertThat;
65+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
6466

6567
/**
6668
* Tests for {@link LogbackConfigurationAotContribution}.
@@ -93,6 +95,37 @@ void contributionOfBasicModel() {
9395
assertThat(patternRules).isEmpty();
9496
}
9597

98+
@Test
99+
void contributionOfBasicModelThatMatchesExistingModel() {
100+
TestGenerationContext generationContext = new TestGenerationContext();
101+
Model model = new Model();
102+
applyContribution(model, generationContext);
103+
applyContribution(model, generationContext);
104+
InMemoryGeneratedFiles generatedFiles = generationContext.getGeneratedFiles();
105+
assertThat(generatedFiles).has(resource("META-INF/spring/logback-model"));
106+
assertThat(generatedFiles).has(resource("META-INF/spring/logback-pattern-rules"));
107+
SerializationHints serializationHints = generationContext.getRuntimeHints().serialization();
108+
assertThat(serializationHints.javaSerializationHints()
109+
.map(JavaSerializationHint::getType)
110+
.map(TypeReference::getName))
111+
.containsExactlyInAnyOrder(namesOf(Model.class, ArrayList.class, Boolean.class, Integer.class));
112+
assertThat(generationContext.getRuntimeHints().reflection().typeHints()).isEmpty();
113+
Properties patternRules = load(
114+
generatedFiles.getGeneratedFile(Kind.RESOURCE, "META-INF/spring/logback-pattern-rules"));
115+
assertThat(patternRules).isEmpty();
116+
}
117+
118+
@Test
119+
void contributionOfBasicModelThatDiffersFromExistingModelThrows() {
120+
TestGenerationContext generationContext = new TestGenerationContext();
121+
applyContribution(new Model(), generationContext);
122+
Model model = new Model();
123+
model.addSubModel(new RootLoggerModel());
124+
assertThatIllegalStateException().isThrownBy(() -> applyContribution(model, generationContext))
125+
.withMessage("Logging configuration differs from the configuration that has already been written. "
126+
+ "Update your logging configuration so that it is the same for each context");
127+
}
128+
96129
@Test
97130
void patternRulesAreStoredAndRegisteredForReflection() {
98131
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
@@ -238,16 +271,26 @@ private TestGenerationContext applyContribution(Model model) {
238271
}
239272

240273
private TestGenerationContext applyContribution(Model model, Consumer<LoggerContext> contextCustomizer) {
274+
TestGenerationContext generationContext = new TestGenerationContext();
275+
applyContribution(model, contextCustomizer, generationContext);
276+
return generationContext;
277+
}
278+
279+
private void applyContribution(Model model, TestGenerationContext generationContext) {
280+
applyContribution(model, (context) -> {
281+
}, generationContext);
282+
}
283+
284+
private void applyContribution(Model model, Consumer<LoggerContext> contextCustomizer,
285+
TestGenerationContext generationContext) {
241286
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
242287
contextCustomizer.accept(context);
243288
SpringBootJoranConfigurator configurator = new SpringBootJoranConfigurator(null);
244289
configurator.setContext(context);
245290
withSystemProperty("spring.aot.processing", "true", () -> configurator.processModel(model));
246291
LogbackConfigurationAotContribution contribution = (LogbackConfigurationAotContribution) context
247292
.getObject(BeanFactoryInitializationAotContribution.class.getName());
248-
TestGenerationContext generationContext = new TestGenerationContext();
249293
contribution.applyTo(generationContext, null);
250-
return generationContext;
251294
}
252295

253296
private String[] namesOf(Class<?>... types) {

0 commit comments

Comments
 (0)