my Spring-WebMvc application which uses WebClient (reactor) loses the context in a @SpringBootTest.
I have a @SpringBootApplication which offers a @RestController endpoint to the caller. When the caller requests this endpoint, my application makes a Rest call to a third-party Rest-Endpoint by using a WebClient.
The webclient has two filters configured:
- ExchangeFilterFunction.ofRequestProcessor
- ExchangeFilterFunction.ofResponseProcessor
The filters need to write information (request and response body) to a ThreadLocal. After the requests to the third-party api is done, i want to read the information of the thread-local into a custom DataStructure and return it to the caller of my api.
This works when i run the application but not in an integration test. Here are some details about the application:
I use
Hooks.enableAutomaticContextPropagation()I create a ThreadLocalAccessor
ContextRegistry.getInstance().registerThreadLocalAccessor("THREAD_LOCAL",
InformationBasket.THREAD_LOCAL::get,
InformationBasket.THREAD_LOCAL::set,
InformationBasket.THREAD_LOCAL::remove
);
- The ThreadLocal is "wrapped" in a simple class:
public class InformationBasket {
public static final ThreadLocal<Map<String, String>> THREAD_LOCAL = new ThreadLocal<>();
}
- I have the context-propagation dependency on the classpath:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>context-propagation</artifactId>
<version>1.0.4</version>
<scope>compile</scope>
</dependency>
- The filters of the webclient look like this:
private ExchangeFilterFunction getRequestFilter() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
final ThreadLocal<Map<String, String>> threadLocal = InformationBasket.THREAD_LOCAL;
final Map<String, String> arg = threadLocal.get();
log.info("--> ThreadLocal in RequestFilter: {}", threadLocal);
log.info("--> Map in ThreadLocal in RequestFilter: {}", arg);
arg.put("Request", "some-request-data");
return Mono.just(clientRequest);
});
}
private ExchangeFilterFunction getResponseFilter() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
final ThreadLocal<Map<String, String>> threadLocal = InformationBasket.THREAD_LOCAL;
final Map<String, String> arg = threadLocal.get();
log.info("<-- ThreadLocal in ResponseFilter: {}", threadLocal);
log.info("<-- Map in ThreadLocal in ResponseFilter: {}", arg);
arg.put("Response", "some-response-data");
return Mono.just(clientResponse);
});
}
- The RestController looks like this:
@GetMapping("/simple")
public String simpleEndpoint() {
InformationBasket.THREAD_LOCAL.set(new HashMap<>());
final ThreadLocal<Map<String, String>> threadLocal = InformationBasket.THREAD_LOCAL;
log.info("ThreadLocal in Endpoint: {}", threadLocal);
final URI uri = UriComponentsBuilder.fromHttpUrl(url).path("3rd/").path("order").build().toUri();
final String response = webClient.get()
.uri(uri)
.retrieve()
.toEntity(String.class)
.mapNotNull(HttpEntity::getBody)
.block();
// We expect that the Map of the ThreadLocal contains data from both filters (request and response)
InformationBasket.THREAD_LOCAL.get().forEach((key, value) -> {
log.info("Key: {} Value: {}", key, value);
});
return response;
}
- The integration test looks like this:
@SpringBootTest(classes = ReactorTestingApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SimpleControllerTest {
@LocalServerPort
int port;
@Autowired
private WebTestClient client;
@Test
void simpleEndpoint_success() throws Exception {
client.get().uri("/simple")
.exchange()
.expectStatus()
.isOk();
}
}
The logs in the test look like this:
2023-09-11T12:30:53.265+02:00 INFO 16745 --- \[o-auto-1-exec-1\] c.e.reactortesting.SimpleController : ThreadLocal in Endpoint: java.lang.ThreadLocal@49f16008
2023-09-11T12:30:53.271+02:00 INFO 16745 --- \[o-auto-1-exec-1\] c.e.reactortesting.SimpleController : --\> ThreadLocal in RequestFilter: java.lang.ThreadLocal@49f16008
2023-09-11T12:30:53.272+02:00 INFO 16745 --- \[o-auto-1-exec-1\] c.e.reactortesting.SimpleController : --\> Map in ThreadLocal in RequestFilter: {}
------
\--\> Exception: java.lang.NullPointerException: Cannot invoke "java.util.Map.put(Object, Object)" because "arg" is null
The logs in the running application look like this:
2023-09-11T12:33:42.083+02:00 INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController : ThreadLocal in Endpoint: java.lang.ThreadLocal@771db298
2023-09-11T12:33:42.092+02:00 INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController : --\> ThreadLocal in RequestFilter: java.lang.ThreadLocal@771db298
2023-09-11T12:33:42.092+02:00 INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController : --\> Map in ThreadLocal in RequestFilter: {}
2023-09-11T12:33:42.409+02:00 INFO 16902 --- \[ctor-http-nio-2\] c.e.reactortesting.SimpleController : \<-- ThreadLocal in ResponseFilter: java.lang.ThreadLocal@771db298
2023-09-11T12:33:42.409+02:00 INFO 16902 --- \[ctor-http-nio-2\] c.e.reactortesting.SimpleController : \<-- Map in ThreadLocal in ResponseFilter: {Request=some-request-data}
2023-09-11T12:33:42.418+02:00 INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController : Key: Response Value: some-response-data
2023-09-11T12:33:42.418+02:00 INFO 16902 --- \[nio-8080-exec-2\] c.e.reactortesting.SimpleController : Key: Request Value: some-request-data
As you can see, the application behaves different in the IntegrationTest. What is causing this different behaviour?
Thank you in advance. Best,J
- Tried to use MockMvc, WebTestClient
- Tried to use different versions of the context-propagation library
- Tried
Mono.deferContextual()in the filter-functions