diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java index 13a6c2aad8..7dbbb84362 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java @@ -226,7 +226,7 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi private final LinesWriter delegate; - private final List lines = new ArrayList<>(); + private final ThreadLocal> lines = ThreadLocal.withInitial(ArrayList::new); DeferredLinesWriter(WebApplicationContext context, LinesWriter delegate) { Assert.state(context instanceof ConfigurableApplicationContext, @@ -237,11 +237,11 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi @Override public void write(List lines) { - this.lines.addAll(lines); + this.lines.get().addAll(lines); } void writeDeferredResult() { - this.delegate.write(this.lines); + this.delegate.write(this.lines.get()); } static DeferredLinesWriter get(ApplicationContext applicationContext) { @@ -254,7 +254,7 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi } void clear() { - this.lines.clear(); + this.lines.get().clear(); } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java index fad8242f66..4de76d7388 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java @@ -15,7 +15,11 @@ */ package org.springframework.boot.test.autoconfigure.web.servlet; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -26,6 +30,8 @@ import javax.servlet.http.HttpServlet; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer.DeferredLinesWriter; +import org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer.LinesWriter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; import org.springframework.context.annotation.Bean; @@ -65,6 +71,55 @@ class SpringBootMockMvcBuilderCustomizerTests { assertThat(filters).containsExactlyInAnyOrder(testFilter, otherTestFilter); } + @Test + void whenCalledInParallelDeferredLinesWriterSeparatesOutputByThread() throws Exception { + AnnotationConfigServletWebApplicationContext context = new AnnotationConfigServletWebApplicationContext(); + MockServletContext servletContext = new MockServletContext(); + context.setServletContext(servletContext); + context.register(ServletConfiguration.class, FilterConfiguration.class); + context.refresh(); + + CapturingLinesWriter delegate = new CapturingLinesWriter(); + new DeferredLinesWriter(context, delegate); + CountDownLatch latch = new CountDownLatch(10); + for (int i = 0; i < 10; i++) { + Thread thread = new Thread(() -> { + for (int j = 0; j < 1000; j++) { + DeferredLinesWriter writer = DeferredLinesWriter.get(context); + writer.write(Arrays.asList("1", "2", "3", "4", "5")); + writer.writeDeferredResult(); + writer.clear(); + } + latch.countDown(); + }); + thread.start(); + } + latch.await(60, TimeUnit.SECONDS); + + assertThat(delegate.allWritten).hasSize(10000); + assertThat(delegate.allWritten) + .allSatisfy((written) -> assertThat(written).containsExactly("1", "2", "3", "4", "5")); + } + + private static final class CapturingLinesWriter implements LinesWriter { + + List> allWritten = new ArrayList<>(); + + private final Object monitor = new Object(); + + @Override + public void write(List lines) { + List written = new ArrayList<>(); + for (String line : lines) { + written.add(line); + } + synchronized (this.monitor) { + this.allWritten.add(written); + } + } + + } + @Configuration(proxyBeanMethods = false) static class ServletConfiguration {