I have seen the conversation in spring-projects/spring-framework#34393, but I wanted to re-open the conversation because I disagree with the reasoning for why the issue was closed as won't fix and would like to make a new case to help people avoid the issue I see my teams running into constantly.

To recap, when a developer sets spring.threads.virtual.enabled to true, they have what I think is a reasonable expectation that the Spring framework will do it's best to make choices to configure anything that it can configure to use virtual threading for them. It does this in a lot of cases and, one of those cases, is to configure the default SimpleAsyncTaskExecutor to use virtual threads.

In the case of JDKClientHttpRequestFactory it will detect that no executor has been set on the HttpClient and create a brand new, dedicated SimpleAsyncTaskExecutor, but it doesn't bother consulting the spring.threads.virtual.enabled setting to see if it should be configured to use virtual threads.

IMHO, this level of indirection that JDKClientHttpRequestFactory provides is Spring's chance to "help" meet the expectations of developers and, today, it doesn't.

NOTE: if the developer "brings their own" HttpClient and it happens to have an executor already set, the logic in JDKClientHttpRequestFactory already won't override it, so making changes here should be non-breaking. It's really just about updating the implementation to configure the dedicated SimpleAsyncTaskExecutor with virtual threads when spring.threads.virtual.enabled is true.

Comment From: bclozel

Thanks for reaching out. This cannot be solved on the Spring Framework side for several reasons. RestClient.create() is a static call that does not consider the environment. Also, we cannot make RestClient depend on org.springframework.boot.autoconfigure.thread.Threading as this is a Spring Boot class and this would create a dependency cycle.

I think I should have considered spring-projects/spring-framework#34393 as a Spring Boot issue in the first place. I'm moving this issue to be considered by the Spring Boot team. There, we can consider whether enabling the spring.threads.virtual.enabled should trigger a client request factory change to use virtual threads. This would only be available if you are injecting the auto-configured RestClient.Builder to build your client instance. Would this work for you @drub0y ?

Comment From: drub0y

Totally understand the RestClient.create() case, but I think it's fair to say if you drop down to that level of API you should know you're bypassing all manner of injection/autoconfiguration... obviously wouldn't hurt to note that in documentation of course.

While we do use RestClient.Builder today, we're still the ones having to directly instantiate and configure the HttpClient and wrap it with the JdkHttpClientHttpRequestFactory and assign it via RestClient.Builder's requestExecutor(...) and that's where all the new'ing comes in that obviously bypasses Spring configuration and leads to this unfortunate situation.

If I'm following your proposal, yes it would be helpful if RestClient.Builder had some kind of dedicated useJdkClientRequestExecutor(JdkHttpClientBuilder builder) that properly configured the JDKClientHttpRequestFactory behind the scenes (taking into account the virtual threads property at that point) while also allowing us to configure the specifics of the HttpClient via that JdkHttpClientBuilder (which already exists). In fact, if this was the case then it would mean that you could just inject/use the default SimpleAsyncTaskExecutor which would have already been correctly initialized according to the spring.threads.virtual.enabled property and there'd be no extra work to do other than to link it up correctly.

Finally, let's not forget "full" autoconfiguration where JDK client is auto-selected and the app itself decides it doesn't want to configure the HttpClient directly at all. Obviously that scenario should also be defaulting to configuring the HttpClient with the default SimpleAsyncTaskExecutor for the best possible out-of-the-box experience.

Comment From: bclozel

If I'm following your proposal, yes it would be helpful if RestClient.Builder had some kind of dedicated useJdkClientRequestExecutor(JdkHttpClientBuilder builder) that properly configured the JDKClientHttpRequestFactory behind the scenes (taking into account the virtual threads property at that point) while also allowing us to configure the specifics of the HttpClient via that JdkHttpClientBuilder (which already exists). In fact, if this was the case then it would mean that you could just inject/use the default SimpleAsyncTaskExecutor which would have already been correctly initialized according to the spring.threads.virtual.enabled property and there'd be no extra work to do other than to link it up correctly.

You've lost me. We can't add a method in RestClient.Builder (a Framework class) that takes JdkHttpClientBuilder (a Spring Boot class) as an argument, for the same reason: Framework cannot depend on Spring Boot.

My proposal was mainly that the RestClient.Builder instance contributed as a bean by Spring Boot would have a JdkClientHttpRequestFactory with a virtual threads executor when the configuration option is enabled. You are right, if we do so we must ensure that we wouldn't override an opinion provided about the executor directly in HttpClient.

The use case would mainly look like this:

@Component
class SomeComponent {

  public SomeComponent(RestClient.Builder clientBuilder) {
    // the client builder would use a virtual threads executor
    //  if "spring.threads.virtual.enabled" is true
  }
}

If you need to provide somehow a custom HttpClient instance, I'm not sure we can improve the situation there and I think that creating the request factory yourself is the best choice still.

Comment From: nosan

I believe in 3.5, this can probably be done with ClientHttpRequestFactoryBuilderCustomizer?

    @Bean
    @ConditionalOnBean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    @ConditionalOnClass(HttpClient.class)
    @ConditionalOnThreading(Threading.VIRTUAL)
    @Order(0)
    ClientHttpRequestFactoryBuilderCustomizer<JdkClientHttpRequestFactoryBuilder> jdkClientExecutorHttpRequestFactoryBuilderCustomizer(
            @Qualifier(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) ObjectProvider<TaskExecutor> taskExecutor) {
        return (builder) -> builder
            .withHttpClientCustomizer((httpClientBuilder) -> taskExecutor.ifAvailable(httpClientBuilder::executor));
    }

Comment From: mhalbritter

Yes, this would work. However, this would also overwrite any Executor already set on the HttpClient.Builder. AFAIK there's no way to check if the builder already has an executor set.

Comment From: nosan

AFAIK there's no way to check if the builder already has an executor set.

Indeed, there is no way to check if the builder already has an Executor set except by using reflection, but I am not happy with that solution.

However, this would also overwrite any Executor already set on the HttpClient.Builder.

Yes, it would overwrite an Executor if it was set before the Spring Boot customizer. However, in that case, users can declare their own Customizer bean with the @Order annotation and customize the builder with a custom Executor after Boot's one.