Ribbon load balancer with webclient differs from rest template one (not properly balanced) - load-balancing

I've tried to use WebClient with LoadBalancerExchangeFilterFunction:
WebClient config:
#Bean
public WebClient myWebClient(final LoadBalancerExchangeFilterFunction lbFunction) {
return WebClient.builder()
.filter(lbFunction)
.defaultHeader(ACCEPT, APPLICATION_JSON_VALUE)
.defaultHeader(CONTENT_ENCODING, APPLICATION_JSON_VALUE)
.build();
}
Then I've noticed that calls to underlying service are not properly load balanced - there is constant difference of RPS on each instance.
Then I've tried to move back to RestTemplate. And it's working fine.
Config for RestTemplate:
private static final int CONNECT_TIMEOUT_MILLIS = 18 * DateTimeConstants.MILLIS_PER_SECOND;
private static final int READ_TIMEOUT_MILLIS = 18 * DateTimeConstants.MILLIS_PER_SECOND;
#LoadBalanced
#Bean
public RestTemplate restTemplateSearch(final RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.errorHandler(errorHandlerSearch())
.requestFactory(this::bufferedClientHttpRequestFactory)
.build();
}
private ClientHttpRequestFactory bufferedClientHttpRequestFactory() {
final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(CONNECT_TIMEOUT_MILLIS);
requestFactory.setReadTimeout(READ_TIMEOUT_MILLIS);
return new BufferingClientHttpRequestFactory(requestFactory);
}
private ResponseErrorHandler errorHandlerSearch() {
return new DefaultResponseErrorHandler() {
#Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().is5xxServerError();
}
};
}
Load balancing using WebClient config up to 11:25, then switching back to RestTemplate:
Is there a reason why there is such difference and how I can use WebClient to have same amount of RPS on each instance? Clue might be that older instances are getting more requests than new ones.
I've tried bit of debugging and same (defaults like ZoneAwareLoadBalancer) logic is being called.

I did simple POC and everything works exactly the same with web client and rest template for default configuration.
Rest server code:
#SpringBootApplication
internal class RestServerApplication
fun main(args: Array<String>) {
runApplication<RestServerApplication>(*args)
}
class BeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
override fun initialize(context: GenericApplicationContext) {
serverBeans().initialize(context)
}
}
fun serverBeans() = beans {
bean("serverRoutes") {
PingRoutes(ref()).router()
}
bean<PingHandler>()
}
internal class PingRoutes(private val pingHandler: PingHandler) {
fun router() = router {
GET("/api/ping", pingHandler::ping)
}
}
class PingHandler(private val env: Environment) {
fun ping(serverRequest: ServerRequest): Mono<ServerResponse> {
return Mono
.fromCallable {
// sleap added to simulate some work
Thread.sleep(2000)
}
.subscribeOn(elastic())
.flatMap {
ServerResponse.ok()
.syncBody("pong-${env["HOSTNAME"]}-${env["server.port"]}")
}
}
}
In application.yaml add:
context.initializer.classes: com.lbpoc.server.BeansInitializer
Dependencies in gradle:
implementation('org.springframework.boot:spring-boot-starter-webflux')
Rest client code:
#SpringBootApplication
internal class RestClientApplication {
#Bean
#LoadBalanced
fun webClientBuilder(): WebClient.Builder {
return WebClient.builder()
}
#Bean
#LoadBalanced
fun restTemplate() = RestTemplateBuilder().build()
}
fun main(args: Array<String>) {
runApplication<RestClientApplication>(*args)
}
class BeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
override fun initialize(context: GenericApplicationContext) {
clientBeans().initialize(context)
}
}
fun clientBeans() = beans {
bean("clientRoutes") {
PingRoutes(ref()).router()
}
bean<PingHandlerWithWebClient>()
bean<PingHandlerWithRestTemplate>()
}
internal class PingRoutes(private val pingHandlerWithWebClient: PingHandlerWithWebClient) {
fun router() = org.springframework.web.reactive.function.server.router {
GET("/api/ping", pingHandlerWithWebClient::ping)
}
}
class PingHandlerWithWebClient(private val webClientBuilder: WebClient.Builder) {
fun ping(serverRequest: ServerRequest) = webClientBuilder.build()
.get()
.uri("http://rest-server-poc/api/ping")
.retrieve()
.bodyToMono(String::class.java)
.onErrorReturn(TimeoutException::class.java, "Read/write timeout")
.flatMap {
ServerResponse.ok().syncBody(it)
}
}
class PingHandlerWithRestTemplate(private val restTemplate: RestTemplate) {
fun ping(serverRequest: ServerRequest) = Mono.fromCallable {
restTemplate.getForEntity("http://rest-server-poc/api/ping", String::class.java)
}.flatMap {
ServerResponse.ok().syncBody(it.body!!)
}
}
In application.yaml add:
context.initializer.classes: com.lbpoc.client.BeansInitializer
spring:
application:
name: rest-client-poc-for-load-balancing
logging:
level.org.springframework.cloud: DEBUG
level.com.netflix.loadbalancer: DEBUG
rest-server-poc:
listOfServers: localhost:8081,localhost:8082
Dependencies in gradle:
implementation('org.springframework.boot:spring-boot-starter-webflux')
implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
You can try it with two or more instances for server and it works exactly the same with web client and rest template.
Ribbon use by default zoneAwareLoadBalancer and if you have only one zone all instances for server will be registered in "unknown" zone.
You might have a problem with keeping connections by web client. Web client reuse the same connection in multiple requests, rest template do not do that. If you have some kind of proxy between your client and server then you might have a problem with reusing connections by web client. To verify it you can modify web client bean like this and run tests:
#Bean
#LoadBalanced
fun webClientBuilder(): WebClient.Builder {
return WebClient.builder()
.clientConnector(ReactorClientHttpConnector { options ->
options
.compression(true)
.afterNettyContextInit { ctx ->
ctx.markPersistent(false)
}
})
}
Of course it's not a good solution for production but doing that you can check if you have a problem with configuration inside your client application or maybe problem is outside, something between your client and server. E.g. if you are using kubernetes and register your services in service discovery using server node IP address then every call to such service will go though kube-proxy load balancer and will be (by default round robin will be used) routed to some pod for that service.

You have to configure Ribbon config to modify the load balancing behavior (please read below).
By default (which you have found yourself) the ZoneAwareLoadBalancer is being used. In the source code for ZoneAwareLoadBalancer we read:
(highlighted by me are some mechanics which could result in the RPS pattern you see):
The key metric used to measure the zone condition is Average Active Requests, which is aggregated per rest client per zone. It is the
total outstanding requests in a zone divided by number of available targeted instances (excluding circuit breaker tripped instances).
This metric is very effective when timeout occurs slowly on a bad zone.
The LoadBalancer will calculate and examine zone stats of all available zones. If the Average Active Requests for any zone has reached a configured threshold, this zone will be dropped from the active server list. In case more than one zone has reached the threshold, the zone with the most active requests per server will be dropped.
Once the the worst zone is dropped, a zone will be chosen among the rest with the probability proportional to its number of instances.
If your traffic is being served by one zone (perhaps the same box?) then you might get into some additionally confusing situations.
Please also note that without using LoadBallancedFilterFunction the average RPS is the same as when you use it (on the graph all lines converge to the median line) after the change, so globally looking both load balancing strategies consume the same amount of available bandwidth but in a different manner.
To modify your Ribbon client settings, try following:
public class RibbonConfig {
#Autowired
IClientConfig ribbonClientConfig;
#Bean
public IPing ribbonPing (IClientConfig config) {
return new PingUrl();//default is a NoOpPing
}
#Bean
public IRule ribbonRule(IClientConfig config) {
return new AvailabilityFilteringRule(); // here override the default ZoneAvoidanceRule
}
}
Then don't forget to globally define your Ribbon client config:
#SpringBootApplication
#RibbonClient(name = "app", configuration = RibbonConfig.class)
public class App {
//...
}
Hope this helps!

Related

Spring Cloud Gateway Filter with external configuration service call

I am working on a Spring Cloud Gateway app that has a filter controlling access to certain paths or features based on a configuration held by a different service. So if a path is associated with feature x then only allow access if the configuration service returns that feature x is enabled.
The configuration is returned as a Mono and then flatMapped to check the enabled features. This all appears to work correctly. If the feature is enabled then the request is allowed to proceed through the chain. If the feature is disabled, then the response status is set to forbidden and the request marked as complete. However, this does not appear to stop the filter chain, and the request continues to be processed and eventually returns a 200 response.
If the feature configuration is not returned from an external source and is immediately available then this logic works correctly, but this involves a blocking call and does not seem desirable. I cannot see what is wrong with the first approach. It seems to be similar to examples available elsewhere.
I believe my question is similar to this one:
https://stackoverflow.com/questions/73496938/spring-cloud-api-gateway-custom-filters-with-external-api-for-authorization/75095356#75095356
Filter 1
This is the way I would like to do this:
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
logger.info("Feature Security Filter")
// getFeatures returns Mono<Map<String, Boolean>>
return featureConfigService.getFeatures().flatMap { features ->
val path = exchange.request.path.toString()
val method = exchange.request.method.toString()
if (featureMappings.keys.any { it.matcher(path).matches() }) {
val pathIsRestricted = featureMappings
.filter { it.key.matcher(path).matches() }
.filter { features[it.value.requiresFeature] != true || !it.value.methodsAllowed.contains(method) }
.isNotEmpty()
if (pathIsRestricted) {
logger.warn("Access to path [$method|$path] restricted. ")
exchange.response.statusCode = HttpStatus.FORBIDDEN
exchange.response.setComplete()
// processing should stop here but continues through other filters
}
}
chain.filter(exchange);
}
}
Filter 2
This way works but involves a blocking call in featureService.
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
logger.info("Feature Security Filter")
// this call returns a Map<String, Boolean> instead of a Mono
val features = featureService.getFeatureConfig()
val path = exchange.request.path.toString()
val method = exchange.request.method.toString()
if (featureMappings.keys.any { it.matcher(path).matches() }) {
val pathIsRestricted = featureMappings
.filter { it.key.matcher(path).matches() }
.filter { features[it.value.requiresFeature] != true || !it.value.methodsAllowed.contains(method) }
.isNotEmpty()
if (pathIsRestricted) {
logger.warn("Access to path [$method|$path] restricted. ")
val response: ServerHttpResponse = exchange.response
response.statusCode = HttpStatus.FORBIDDEN;
return response.setComplete()
// this works as this request will complete here
}
}
return chain.filter(exchange)
}
When the tests run I can see that a path is correctly logged as restricted, and the response status is set to HttpStatus.FORBIDDEN as expected, but the request continues to be processed by filters later in the chain, and eventually returns a 200 response.
I've tried returning variations on Mono.error and onErrorComplete but I get the same behaviour. I am new to Spring Cloud Gateway and cannot see what I am doing wrong
After doing a few tests, I figured out that Filters are executed after route filters even if you set high order. If you need to filter requests before routing, you can use WebFilter. Here is a working Java example based on your requirements.
package com.test.test.filters;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Map;
#Configuration
#Slf4j
public class TestGlobalFilter implements WebFilter, Ordered {
private Mono<Map<String, Boolean>> test() {
return Mono.just(Map.of("test", Boolean.TRUE));
}
#Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
log.info("Feature Security Filter");
// getFeatures returns Mono<Map<String, Boolean>>
return test().flatMap(features -> {
final var isRestricted = features.get("test");
if (Boolean.TRUE.equals(isRestricted)) {
log.info("Feature Security stop");
exchange.getResponse().setStatusCode(HttpStatus. FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
});
}
}

Spring Oauth 2.3.8 clashes with Spring security 5.4.6

I'm using a spring security in my app. I need it to secure two api's endpoints. Here is security config:
#Configuration
#EnableWebSecurity
class SecurityConfig(
private val authProps: AuthenticationProperties
) : WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
val encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
auth.inMemoryAuthentication()
.passwordEncoder(encoder)
.withUser(authProps.user)
.password(encoder.encode(authProps.password))
.roles("USER")
}
override fun configure(http: HttpSecurity) {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/firstapi/**", "/secondapi/**").authenticated()
.anyRequest().permitAll()
.and()
.httpBasic()
}
}
Also, i'm integrating with an external service, that uses oauth2, here it's restTemplate config -
#Configuration
class ExternalServiceConfiguration {
#Bean("externalProperties")
#ConfigurationProperties("http.external.api")
fun externalHttpClientConfig() = HttpClientProperties()
#Bean("externalDetails")
#ConfigurationProperties("http.external.api.security.oauth2")
fun externalOAuth2Details() = ResourceOwnerPasswordResourceDetails()
#Bean("externalRestTemplate")
fun externalClientRestTemplate(
#Qualifier("externalProperties") externalHttpClientProperties: HttpClientProperties,
#Qualifier("externalDetails") externalOAuth2Details: ResourceOwnerPasswordResourceDetails,
customizerProviders: ObjectProvider<RestTemplateCustomizer>,
objectMapper: ObjectMapper,
): RestTemplate {
val template = OAuth2RestTemplate(externalOAuth2Details).apply {
messageConverters = listOf(MappingJackson2HttpMessageConverter(objectMapper))
requestFactory = requestFactory(externalHttpClientProperties)
errorHandler = IntegrationResponseErrorHandler()
}
customizerProviders.orderedStream().forEach { it.customize(template) }
return template
}
}
Somehow spring-security-oauth clashes with spring-security. When i try to obtain a token i'm failing at lib class method:
AccessTokenProviderChain.obtainAccessToken(...)
, because instead of null i have an AnonymousAuthenticationToken authentication at context, when it calls
SecurityContextHolder.getContext().getAuthentication()
So spring security merges anonymous context somehow, i am not sure when and how, so that it affects external calls.
Could anyone help me with advice, how can i find the collision???

Problems testing spring webflux Webclient with high load

I am trying to learn Spring Webflux comming from C# and NetCore, we have a very similar problem like this post, where a third party service provider has some response time problems.
But testing with spring-webclient is doubling the response time, I do not know if I am missing something
I tried to create a similar example with:
A computer running 3 servers
Demo server that just simulates some random delay time (port 8080)
Test Server in C# using async to call my "Wait" server (port 5000)
Test Server with spring and webclient to call my "Wait" server (port 8081)
Other computer running JMeter with 1000 clients and 10 rounds each one
Some code
Wait server
Just a simple route
#Configuration
class TestRouter(private val middlemanDemo: MiddlemanDemo) {
#Bean
fun route() = router {
GET("/testWait", middlemanDemo::middleTestAndGetWait)
}
}
The handler has a Random generator with a seed, so each test can generate the same sequence of delays
#Service
class TestWaiter {
companion object RandomManager {
private lateinit var random: Random
init {
resetTimer()
}
#Synchronized
fun next(): Long {
val random = random.nextLong(0, 10)
return random * 2
}
fun resetTimer() {
random = Random(12345)
}
}
private val logger = LoggerFactory.getLogger(javaClass)
fun testAndGetWait(request: ServerRequest): Mono<ServerResponse> {
val wait = next()
logger.debug("Wait is: {}", wait)
return ServerResponse
.ok()
.json()
.bodyValue(wait)
.delayElement(Duration.ofSeconds(wait))
}
fun reset(request: ServerRequest): Mono<ServerResponse> {
logger.info("Random reset")
resetTimer()
return ServerResponse
.ok()
.build()
}
}
Load testing the server with JMeter I can see a steady response time of around 9-10 seconds and a max throughput of 100/sec:
C# async Demo server
Trying a middle man with C#, this server just calls the main demo server:
The controller
[HttpGet]
public async Task<string> Get()
{
return await _waiterClient.GetWait();
}
And the service with the httpClient
private readonly HttpClient _client;
public WaiterClient(HttpClient client)
{
_client = client;
client.BaseAddress = new Uri("http://192.168.0.121:8080");
}
public async Task<string> GetWait()
{
var response = await _client.GetAsync("/testWait");
var waitTime = await response.Content.ReadAsStringAsync();
return waitTime;
}
}
Testing this service gives the same response time, with a little less throughput for the overhead, but it is understandable
The spring-webclient implementation
This client is also really simple, just one route
#Configuration
class TestRouter(private val middlemanDemo: MiddlemanDemo) {
#Bean
fun route() = router {
GET("/testWait", middlemanDemo::middleTestAndGetWait)
}
}
The handler just calls the service using the webclient
#Service
class MiddlemanDemo {
private val client = WebClient.create("http://127.0.0.1:8080")
fun middleTestAndGetWait(request: ServerRequest): Mono<ServerResponse> {
return client
.get()
.uri("/testWait")
.retrieve()
.bodyToMono(Int::class.java)
.flatMap(::processResponse)
}
fun processResponse(delay: Int): Mono<ServerResponse> {
return ServerResponse
.ok()
.bodyValue(delay)
}
}
However, running the tests, the throughput only get to 50/sec
And the response time doubles like if I had another wait, until the load goes down again
I think it may be caused by pool acquire time.
I assume your server gets over 1k TPS and each request looks to take about 9 seconds. But the default HTTP client connection pool is 500. Please refer to Projector Reactor - Connection Pool.
Please check the logs have PoolAcquireTimeoutException or whether your server takes some time to wait pool acquisition.
I am marking KL.Lee answer because it pointed me in the right way, but I will add the complete solution for anyone to find:
The key was to create a connection pool according to my needs. The default is 500 as JK.Lee mentioned.
#Service
class MiddlemanDemo(webClientBuilder: WebClient.Builder) {
private val client: WebClient
init {
val provider = ConnectionProvider.builder("fixed")
.maxConnections(2000) // This is the important part
.build()
val httpClient = HttpClient
.create(provider)
client = webClientBuilder
.clientConnector(ReactorClientHttpConnector(httpClient))
.baseUrl("http://localhost:8080")
.build()
}
fun middleTestAndGetWait(request: ServerRequest): Mono<ServerResponse> {
return client
.get()
.uri("/testWait")
.retrieve()
.bodyToMono(Int::class.java)
.flatMap(::processResponse)
}
fun processResponse(delay: Int): Mono<ServerResponse> {
return ServerResponse
.ok()
.bodyValue(delay)
}
}

Transactions with ReactiveCrudRepository with spring-data-r2dbc

I'm trying to implement transactions with spring-data-r2dbc repositories in combination with the TransactionalDatabaseClient as such:
class SongService(
private val songRepo: SongRepo,
private val databaseClient: DatabaseClient
){
private val tdbc = databaseClient as TransactionalDatabaseClient
...
...
fun save(song: Song){
return tdbc.inTransaction{
songRepo
.save(mapRow(song, albumId)) //Mapping to a row representation
.delayUntil { savedSong -> tdbc.execute.sql(...).fetch.rowsUpdated() } //saving a many to many relation
.map(::mapSong) //Mapping back to actual song and retrieve the relationship data.
}
}
}
I currently have a config class (annotated with #Configuration and #EnableR2dbcRepositories) that extends from AbstractR2dbcConfiguration. In here I override the databaseClient method to return a TransactionalDatabaseClient. This should be the same instance as in the SongService class.
When running the code in a test with just subscribing and printing, I get org.springframework.transaction.NoTransactionException: ReactiveTransactionSynchronization not active and the relationship data is not returned.
When using project Reactors stepverifier though, i get java.lang.IllegalStateException: Connection is closed. Also in this case, the relationship data is not returned.
Just for the record, I have seen https://github.com/spring-projects/spring-data-r2dbc/issues/44
Here is a working Java example:
#Autowired TransactionalDatabaseClient txClient;
#Autowired Mono<Connection> connection;
//You Can also use: #Autowired Mono<? extends Publisher> connectionPublisher;
public Flux<Void> example {
txClient.enableTransactionSynchronization(connection);
// Or, txClient.enableTransactionSynchronization(connectionPublisher);
Flux<AuditConfigByClub> audits = txClient.inTransaction(tx -> {
txClient.beginTransaction();
return tx.execute().sql("SELECT * FROM audit.items")
.as(Item.class)
.fetch()
.all();
}).doOnTerminate(() -> {
txClient.commitTransaction();
});
txClient.commitTransaction();
audits.subscribe(item -> System.out.println("anItem: " + item));
return Flux.empty()
}
I just started reactive so not too sure what I'm doing with my callbacks haha. But I decided to go with TransactionalDatabaseClient over DatabaseClient or Connection since I'll take all the utility I can get while R2dbc is in its current state.
In your code did you actually instantiate a Connection object? If so I think you would have done it in your configuration. It can be utilized throughout the app the same as DatabaseClient, but it is slightly more intricate.
If not:
#Bean
#Override // I also used abstract config
public ConnectionFactory connectionFactory() {
...
}
#Bean
TransactionalDatabaseClient txClient() {
...
}
//TransactionalDatabaseClient will take either of these as arg in
//#enableTransactionSynchronization method
#Bean
public Publisher<? extends Connection> connectionPublisher() {
return connectionFactory().create();
}
#Bean
public Mono<Connection> connection() {
return = Mono.from(connectionFactory().create());
}
If you are having problems translating to Kotlin, there is an alternative way to enable synchronization that could work:
// From what I understand, this is a useful way to move between
// transactions within a single subscription
TransactionResources resources = TransactionResources.create();
resources.registerResource(Resource.class, resource);
ConnectionFactoryUtils
.currentReactiveTransactionSynchronization()
.subscribe(currentTx -> sync.registerTransaction(Tx));
Hope this translates well for Kotlin.

Is there a way to specify webSessionManager when using WebTestClientAutoConfiguration?

I am using Webflux in Spring Boot 2.0.3.RELEASE to create REST API. With that implementation, I customize and use the webSessionManager as below.
#EnableWebFluxSecurity
#Configuration
class SecurityConfiguration {
#Bean
fun webSessionManager(): WebSessionManager {
return DefaultWebSessionManager().apply {
sessionIdResolver = HeaderWebSessionIdResolver().apply {
headerName = "X-Sample"
}
sessionStore = InMemoryWebSessionStore()
}
}
// ...
}
And in order to test the REST API, I created a test code as follows. (addUser and signin are extension functions.)
#RunWith(SpringRunner::class)
#SpringBootTest
#AutoConfigureWebTestClient
#FixMethodOrder(MethodSorters.NAME_ASCENDING)
class UserTests {
#Autowired
private lateinit var client: WebTestClient
#Test
fun testGetUserInfo() {
client.addUser(defaultUser)
val sessionKey = client.signin(defaultUser)
client.get().uri(userPath)
.header("X-Sample", sessionKey)
.exchange()
.expectStatus().isOk
.expectBody()
.jsonInStrict("""
{
"user": {
"mail_address": "user#example.com"
}
}
""".trimIndent())
}
// ...
}
The test failed. It is refused by authorization. However, if I start the server and run it from curl it will succeed in the authorization.
After investigating the cause, it turned out that org.springframework.test.web.reactive.server.AbstractMockServerSpec set webSessionManager to DefaultWebSessionManager. Default is used, not the webSessionManager I customized. For this reason, it could not get the session ID.
AbstractMockServerSpec.java#L41
AbstractMockServerSpec.java#L72-L78
How can I change the webSessionManager of AbstractMockServerSpec?
Also, I think that it is better to have the following implementation, what do you think?
abstract class AbstractMockServerSpec<B extends WebTestClient.MockServerSpec<B>>
implements WebTestClient.MockServerSpec<B> {
// ...
private WebSessionManager sessionManager = null;
// ...
#Override
public WebTestClient.Builder configureClient() {
WebHttpHandlerBuilder builder = initHttpHandlerBuilder();
builder.filters(theFilters -> theFilters.addAll(0, this.filters));
if (this.sessionManager != null) {
builder.sessionManager(this.sessionManager);
}
this.configurers.forEach(configurer -> configurer.beforeServerCreated(builder));
return new DefaultWebTestClientBuilder(builder);
}
// ...
}
Spring Framework's AbstractMockServerSpec is providing a method to customize the WebSessionManager already.
Thanks for opening SPR-17094, this problem will be solved with that ticket - the AbstractMockServerSpec is already looking into the application context for infrastructure bits, this should check for a WebSessionManager as well.