diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java index 2f726c6f36..d205ebd820 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java @@ -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> serializationTypes(Model model) { Set> modelClasses = new HashSet<>(); @@ -389,7 +397,9 @@ class SpringBootJoranConfigurator extends JoranConfigurator { private void save(GenerationContext generationContext) { Map 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 patternRuleRegistry) { + private byte[] asBytes(Map 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 { + + 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)); + } } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java index d215c35d23..a2cd1ded8f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackConfigurationAotContributionTests.java @@ -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 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 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) {