From 4a1abb9617c11402cf18f9a7337ca7d63e209d05 Mon Sep 17 00:00:00 2001 From: dlwldn30 Date: Sun, 16 Aug 2026 23:44:13 +0900 Subject: [PATCH 1/2] Release file handle when comparing AOT file content RequireNewOrMatchingContentFileHandler reads the already generated file through content.getInputStream().readAllBytes(). That method does not close the stream, and the stream is never assigned, so it cannot be closed at all. During AOT processing the content is a FileSystemResource, so each comparison leaks a file handle. FileSystemGeneratedFiles already uses try-with-resources when it consumes an InputStreamSource. Read the existing content inside a try-with-resources block. See gh-51398 Signed-off-by: dlwldn30 --- .../logback/SpringBootJoranConfigurator.java | 5 +- .../SpringBootJoranConfiguratorTests.java | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java index ac1d5cc0bd3..570c09db4be 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java @@ -442,7 +442,10 @@ class SpringBootJoranConfigurator extends JoranConfigurator { if (file.exists()) { InputStreamSource content = file.getContent(); Assert.state(content != null, "Unable to get file content"); - byte[] existingContent = content.getInputStream().readAllBytes(); + byte[] existingContent; + try (InputStream inputStream = content.getInputStream()) { + existingContent = inputStream.readAllBytes(); + } if (!Arrays.equals(this.newContent, existingContent)) { throw new IllegalStateException( "Logging configuration differs from the configuration that has already been written. " diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java index 0b08bbe8ac2..afb3f7d951f 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java @@ -16,10 +16,17 @@ package org.springframework.boot.logging.logback; +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import ch.qos.logback.classic.BasicConfigurator; import ch.qos.logback.classic.LoggerContext; @@ -32,7 +39,12 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.generate.GeneratedFiles.FileHandler; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; @@ -40,10 +52,14 @@ import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.context.aot.AbstractAotProcessor; +import org.springframework.core.io.InputStreamSource; +import org.springframework.javapoet.ClassName; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; /** * Tests for {@link SpringBootJoranConfigurator}. @@ -283,6 +299,29 @@ class SpringBootJoranConfiguratorTests { }); } + @Test + @WithPropertyXmlResource + void aotContributionClosesExistingFileContent() throws Exception { + withSystemProperty(AbstractAotProcessor.AOT_PROCESSING, "true", () -> { + initialize("property.xml"); + BeanFactoryInitializationAotContribution contribution = (BeanFactoryInitializationAotContribution) this.context + .getObject(BeanFactoryInitializationAotContribution.class.getName()); + assertThat(contribution).isNotNull(); + List closed = new ArrayList<>(); + GeneratedFiles generatedFiles = (kind, path, handler) -> { + AtomicBoolean fileClosed = new AtomicBoolean(); + closed.add(fileClosed); + handler.accept(new ClosableContentFileHandler(fileClosed)); + }; + DefaultGenerationContext generationContext = new DefaultGenerationContext( + new ClassNameGenerator(ClassName.get(Object.class)), generatedFiles); + assertThatIllegalStateException() + .isThrownBy(() -> contribution.applyTo(generationContext, mock(BeanFactoryInitializationCode.class))) + .withMessageContaining("Logging configuration differs"); + assertThat(closed).isNotEmpty().allMatch(AtomicBoolean::get); + }); + } + private void withSystemProperty(String name, String value, Action action) throws Exception { System.setProperty(name, value); try { @@ -318,6 +357,30 @@ class SpringBootJoranConfiguratorTests { } + /** + * {@link FileHandler} whose existing content records when its stream is closed. + */ + private static final class ClosableContentFileHandler extends FileHandler { + + private ClosableContentFileHandler(AtomicBoolean closed) { + super(true, () -> (InputStreamSource) () -> new FilterInputStream( + new ByteArrayInputStream("existing".getBytes(StandardCharsets.UTF_8))) { + + @Override + public void close() throws IOException { + closed.set(true); + super.close(); + } + + }); + } + + @Override + protected void copy(InputStreamSource content, boolean override) { + } + + } + @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @WithResource(name = "property-default-value.xml", content = """ From 5402f5b43f30249aa81e0fbfa53a9ebdc3e36f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 20 Aug 2026 09:29:51 +0200 Subject: [PATCH 2/2] Polish "Release file handle when comparing AOT file content" See gh-51398 --- .../logback/SpringBootJoranConfigurator.java | 11 ++-- .../SpringBootJoranConfiguratorTests.java | 63 ------------------- 2 files changed, 7 insertions(+), 67 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java index 570c09db4be..d344da6e501 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java @@ -442,10 +442,7 @@ class SpringBootJoranConfigurator extends JoranConfigurator { if (file.exists()) { InputStreamSource content = file.getContent(); Assert.state(content != null, "Unable to get file content"); - byte[] existingContent; - try (InputStream inputStream = content.getInputStream()) { - existingContent = inputStream.readAllBytes(); - } + byte[] existingContent = toByteArray(content); if (!Arrays.equals(this.newContent, existingContent)) { throw new IllegalStateException( "Logging configuration differs from the configuration that has already been written. " @@ -457,6 +454,12 @@ class SpringBootJoranConfigurator extends JoranConfigurator { } } + private byte[] toByteArray(InputStreamSource content) throws IOException { + try (InputStream inputStream = content.getInputStream()) { + return inputStream.readAllBytes(); + } + } + } } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java index afb3f7d951f..0b08bbe8ac2 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/logback/SpringBootJoranConfiguratorTests.java @@ -16,17 +16,10 @@ package org.springframework.boot.logging.logback; -import java.io.ByteArrayInputStream; -import java.io.FilterInputStream; -import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; import ch.qos.logback.classic.BasicConfigurator; import ch.qos.logback.classic.LoggerContext; @@ -39,12 +32,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.aot.generate.ClassNameGenerator; -import org.springframework.aot.generate.DefaultGenerationContext; -import org.springframework.aot.generate.GeneratedFiles; -import org.springframework.aot.generate.GeneratedFiles.FileHandler; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; -import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; @@ -52,14 +40,10 @@ import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.context.aot.AbstractAotProcessor; -import org.springframework.core.io.InputStreamSource; -import org.springframework.javapoet.ClassName; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.mock; /** * Tests for {@link SpringBootJoranConfigurator}. @@ -299,29 +283,6 @@ class SpringBootJoranConfiguratorTests { }); } - @Test - @WithPropertyXmlResource - void aotContributionClosesExistingFileContent() throws Exception { - withSystemProperty(AbstractAotProcessor.AOT_PROCESSING, "true", () -> { - initialize("property.xml"); - BeanFactoryInitializationAotContribution contribution = (BeanFactoryInitializationAotContribution) this.context - .getObject(BeanFactoryInitializationAotContribution.class.getName()); - assertThat(contribution).isNotNull(); - List closed = new ArrayList<>(); - GeneratedFiles generatedFiles = (kind, path, handler) -> { - AtomicBoolean fileClosed = new AtomicBoolean(); - closed.add(fileClosed); - handler.accept(new ClosableContentFileHandler(fileClosed)); - }; - DefaultGenerationContext generationContext = new DefaultGenerationContext( - new ClassNameGenerator(ClassName.get(Object.class)), generatedFiles); - assertThatIllegalStateException() - .isThrownBy(() -> contribution.applyTo(generationContext, mock(BeanFactoryInitializationCode.class))) - .withMessageContaining("Logging configuration differs"); - assertThat(closed).isNotEmpty().allMatch(AtomicBoolean::get); - }); - } - private void withSystemProperty(String name, String value, Action action) throws Exception { System.setProperty(name, value); try { @@ -357,30 +318,6 @@ class SpringBootJoranConfiguratorTests { } - /** - * {@link FileHandler} whose existing content records when its stream is closed. - */ - private static final class ClosableContentFileHandler extends FileHandler { - - private ClosableContentFileHandler(AtomicBoolean closed) { - super(true, () -> (InputStreamSource) () -> new FilterInputStream( - new ByteArrayInputStream("existing".getBytes(StandardCharsets.UTF_8))) { - - @Override - public void close() throws IOException { - closed.set(true); - super.close(); - } - - }); - } - - @Override - protected void copy(InputStreamSource content, boolean override) { - } - - } - @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @WithResource(name = "property-default-value.xml", content = """