3737import java .util .LinkedList ;
3838import java .util .List ;
3939import java .util .Map ;
40+ import java .util .concurrent .ExecutorService ;
4041import java .util .function .Function ;
4142import java .util .function .UnaryOperator ;
4243
6869import org .apache .hc .client5 .http .impl .DefaultUserTokenHandler ;
6970import org .apache .hc .client5 .http .impl .IdleConnectionEvictor ;
7071import org .apache .hc .client5 .http .impl .NoopUserTokenHandler ;
72+ import org .apache .hc .client5 .http .impl .VirtualThreadSupport ;
7173import org .apache .hc .client5 .http .impl .auth .BasicCredentialsProvider ;
7274import org .apache .hc .client5 .http .impl .auth .BasicSchemeFactory ;
7375import org .apache .hc .client5 .http .impl .auth .BearerSchemeFactory ;
@@ -237,6 +239,14 @@ private ExecInterceptorEntry(
237239
238240 private List <Closeable > closeables ;
239241
242+ private boolean useVirtualThreads ;
243+ private String virtualThreadNamePrefix = "hc-vt-" ;
244+ private ExecutorService virtualThreadExecutor ;
245+ private boolean shutdownVirtualThreadExecutor = true ;
246+ private TimeValue virtualThreadShutdownWait = TimeValue .ofSeconds (2 );
247+
248+ private boolean virtualThreadRunHandler ;
249+
240250 public static HttpClientBuilder create () {
241251 return new HttpClientBuilder ();
242252 }
@@ -805,6 +815,130 @@ public final HttpClientBuilder setProxySelector(final ProxySelector proxySelecto
805815 return this ;
806816 }
807817
818+ /**
819+ * Enables or disables execution of the transport layer on virtual threads (JDK 21+).
820+ * <p>
821+ * When enabled and no custom executor is supplied via
822+ * {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService) virtualThreadExecutor(..)},
823+ * the builder will create a per-task virtual-thread executor at build time.
824+ * </p>
825+ * <p>
826+ * If virtual threads are not available at runtime and no custom executor is provided,
827+ * {@link #build()} may throw {@link UnsupportedOperationException} depending on configuration.
828+ * </p>
829+ *
830+ * @return this instance.
831+ * @since 5.6
832+ */
833+ public HttpClientBuilder useVirtualThreads () {
834+ this .useVirtualThreads = true ;
835+ return this ;
836+ }
837+
838+ /**
839+ * Sets the thread name prefix for virtual threads created by this builder.
840+ * <p>
841+ * This prefix is only applied when the builder creates the virtual-thread executor itself.
842+ * If a custom executor is supplied via {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService)},
843+ * the prefix is ignored.
844+ * </p>
845+ *
846+ * @param prefix the desired name prefix; if {@code null}, {@code "hc-vt-"} is used.
847+ * @return this instance.
848+ * @since 5.6
849+ */
850+ public HttpClientBuilder virtualThreadNamePrefix (final String prefix ) {
851+ this .virtualThreadNamePrefix = prefix != null ? prefix : "hc-vt-" ;
852+ return this ;
853+ }
854+
855+ /**
856+ * Supplies a custom executor to run transport work (typically a per-task virtual-thread executor).
857+ * <p>
858+ * Passing a custom executor automatically enables virtual-thread execution. Ownership semantics are
859+ * controlled by {@code shutdownOnClose}:
860+ * </p>
861+ * <ul>
862+ * <li>{@code true}: the client will shut down the executor during {@code close()}.</li>
863+ * <li>{@code false}: the executor is treated as shared and will <em>not</em> be shut down by the client.</li>
864+ * </ul>
865+ * <p>
866+ * This method does not validate that the supplied executor actually creates virtual threads; callers are
867+ * responsible for providing an appropriate executor.
868+ * </p>
869+ *
870+ * @param exec the executor to use for transport work.
871+ * @param shutdownOnClose whether the client should shut down the executor on close.
872+ * @return this instance.
873+ * @since 5.6
874+ */
875+ public HttpClientBuilder virtualThreadExecutor (final ExecutorService exec , final boolean shutdownOnClose ) {
876+ this .virtualThreadExecutor = exec ;
877+ this .shutdownVirtualThreadExecutor = shutdownOnClose ;
878+ this .useVirtualThreads = true ; // ensure VT path is active
879+ return this ;
880+ }
881+
882+ /**
883+ * Supplies a custom executor to run transport work (typically a per-task virtual-thread executor).
884+ * <p>
885+ * Passing a custom executor automatically enables virtual-thread execution and treats the executor as
886+ * <em>shared</em> (it will not be shut down by the client). To change ownership semantics, use
887+ * {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService, boolean)}.
888+ * </p>
889+ * <p>
890+ * This method does not validate that the supplied executor actually creates virtual threads; callers are
891+ * responsible for providing an appropriate executor.
892+ * </p>
893+ *
894+ * @param exec the executor to use for transport work.
895+ * @return this instance.
896+ * @since 5.6
897+ */
898+ public final HttpClientBuilder virtualThreadExecutor (final ExecutorService exec ) {
899+ return virtualThreadExecutor (exec , false );
900+ }
901+
902+ /**
903+ * Configures the maximum time to wait for the virtual-thread executor to terminate during
904+ * a graceful {@link CloseableHttpClient#close() close()} (i.e., {@code CloseMode.GRACEFUL}).
905+ * <p>
906+ * This value is only used when a virtual-thread executor is in use and the client owns it
907+ * (i.e., {@code shutdownVirtualThreadExecutor == true}). For immediate close, the executor
908+ * is shut down without waiting.
909+ * </p>
910+ *
911+ * @param waitTime the time to await executor termination; may be {@code null} to use the default.
912+ * @return this instance.
913+ * @since 5.6
914+ */
915+ public final HttpClientBuilder virtualThreadShutdownWait (final TimeValue waitTime ) {
916+ this .virtualThreadShutdownWait = waitTime ;
917+ return this ;
918+ }
919+
920+
921+ /**
922+ * Configures the client to run the user-supplied {@link org.apache.hc.core5.http.io.HttpClientResponseHandler}
923+ * <p>
924+ * on a virtual thread as well as the transport layer. By default, the response handler runs on the caller thread.
925+ * <p>
926+ * <p>
927+ * This has an effect only when virtual threads are enabled via {@link #useVirtualThreads()} or
928+ * <p>
929+ * {@link #virtualThreadExecutor(java.util.concurrent.ExecutorService)}.
930+ * </p>
931+ *
932+ * @return this builder
933+ * @since 5.6
934+ */
935+ public HttpClientBuilder virtualThreadsRunHandler () {
936+ this .virtualThreadRunHandler = true ;
937+ return this ;
938+ }
939+
940+
941+
808942 /**
809943 * Request exec chain customization and extension.
810944 * <p>
@@ -1121,7 +1255,7 @@ public CloseableHttpClient build() {
11211255 closeablesCopy .add (connManagerCopy );
11221256 }
11231257
1124- return new InternalHttpClient (
1258+ final CloseableHttpClient base = new InternalHttpClient (
11251259 connManagerCopy ,
11261260 requestExecCopy ,
11271261 execChain ,
@@ -1133,6 +1267,20 @@ public CloseableHttpClient build() {
11331267 contextAdaptor (),
11341268 defaultRequestConfig != null ? defaultRequestConfig : RequestConfig .DEFAULT ,
11351269 closeablesCopy );
1270+
1271+ // VT on? wrap, otherwise return base
1272+ if (useVirtualThreads ) {
1273+ final ExecutorService vtExecToUse = virtualThreadExecutor != null
1274+ ? virtualThreadExecutor
1275+ : (VirtualThreadSupport .isAvailable ()
1276+ ? VirtualThreadSupport .newVirtualThreadPerTaskExecutor (virtualThreadNamePrefix )
1277+ : null );
1278+ if (vtExecToUse != null ) {
1279+ return new VirtualThreadCloseableHttpClient (base , vtExecToUse , shutdownVirtualThreadExecutor , virtualThreadShutdownWait , virtualThreadRunHandler );
1280+ }
1281+ }
1282+ return base ;
1283+
11361284 }
11371285
1138- }
1286+ }
0 commit comments