Skip to content

Commit 697d5e6

Browse files
committed
Configuration options for virtual threads (on JDK 21)
VirtualThreadDelegate built on JDK 21 for multi-release jar. Includes dedicated VirtualThreadTaskExecutor as lean option. Includes setVirtualThreads flag on SimpleAsyncTaskExecutor. Includes additional default methods on AsyncTaskExecutor. Closes gh-30241
1 parent d8d7e0a commit 697d5e6

File tree

16 files changed

+385
-64
lines changed

16 files changed

+385
-64
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ plugins {
1010
id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
1111
id 'de.undercouch.download' version '5.4.0'
1212
id 'me.champeau.jmh' version '0.7.0' apply false
13+
id 'me.champeau.mrjar' version '0.1.1'
1314
}
1415

1516
ext {

ci/images/get-jdk-url.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ case "$1" in
99
echo "https://github.com/bell-sw/Liberica/releases/download/20.0.1+10/bellsoft-jdk20.0.1+10-linux-amd64.tar.gz"
1010
;;
1111
java21)
12-
echo "https://download.java.net/java/early_access/jdk21/18/GPL/openjdk-21-ea+18_linux-x64_bin.tar.gz"
12+
echo "https://download.java.net/java/early_access/jdk21/20/GPL/openjdk-21-ea+20_linux-x64_bin.tar.gz"
1313
;;
1414
*)
1515
echo $"Unknown java version"

spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -77,12 +77,6 @@ public void execute(Runnable task) {
7777
}
7878
}
7979

80-
@Deprecated
81-
@Override
82-
public void execute(Runnable task, long startTimeout) {
83-
execute(task);
84-
}
85-
8680
@Override
8781
public Future<?> submit(Runnable task) {
8882
FutureTask<Object> future = new FutureTask<>(task, null);

spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java

-6
Original file line numberDiff line numberDiff line change
@@ -364,12 +364,6 @@ public void execute(Runnable task) {
364364
}
365365
}
366366

367-
@Deprecated
368-
@Override
369-
public void execute(Runnable task, long startTimeout) {
370-
execute(task);
371-
}
372-
373367
@Override
374368
public Future<?> submit(Runnable task) {
375369
ExecutorService executor = getThreadPoolExecutor();

spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java

-6
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,6 @@ public void execute(Runnable task) {
282282
}
283283
}
284284

285-
@Deprecated
286-
@Override
287-
public void execute(Runnable task, long startTimeout) {
288-
execute(task);
289-
}
290-
291285
@Override
292286
public Future<?> submit(Runnable task) {
293287
ExecutorService executor = getScheduledExecutor();

spring-core/spring-core.gradle

+10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
22
import org.springframework.build.shadow.ShadowSource
33

4+
plugins {
5+
id 'me.champeau.mrjar'
6+
}
7+
48
description = "Spring Core"
59

610
apply plugin: "kotlin"
711
apply plugin: "kotlinx-serialization"
812

13+
multiRelease {
14+
targetVersions 17, 21
15+
}
16+
917
def javapoetVersion = "1.13.0"
1018
def objenesisVersion = "3.3"
1119

1220
configurations {
21+
java21Api.extendsFrom(api)
22+
java21Implementation.extendsFrom(implementation)
1323
javapoet
1424
objenesis
1525
graalvm

spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.concurrent.Callable;
2020
import java.util.concurrent.CompletableFuture;
2121
import java.util.concurrent.Future;
22+
import java.util.concurrent.FutureTask;
2223

2324
import org.springframework.util.concurrent.FutureUtils;
2425

@@ -60,6 +61,8 @@ public interface AsyncTaskExecutor extends TaskExecutor {
6061

6162
/**
6263
* Execute the given {@code task}.
64+
* <p>As of 6.1, this method comes with a default implementation that simply
65+
* delegates to {@link #execute(Runnable)}, ignoring the timeout completely.
6366
* @param task the {@code Runnable} to execute (never {@code null})
6467
* @param startTimeout the time duration (milliseconds) within which the task is
6568
* supposed to start. This is intended as a hint to the executor, allowing for
@@ -72,27 +75,41 @@ public interface AsyncTaskExecutor extends TaskExecutor {
7275
* @deprecated as of 5.3.16 since the common executors do not support start timeouts
7376
*/
7477
@Deprecated
75-
void execute(Runnable task, long startTimeout);
78+
default void execute(Runnable task, long startTimeout) {
79+
execute(task);
80+
}
7681

7782
/**
7883
* Submit a Runnable task for execution, receiving a Future representing that task.
7984
* The Future will return a {@code null} result upon completion.
85+
* <p>As of 6.1, this method comes with a default implementation that delegates
86+
* to {@link #execute(Runnable)}.
8087
* @param task the {@code Runnable} to execute (never {@code null})
8188
* @return a Future representing pending completion of the task
8289
* @throws TaskRejectedException if the given task was not accepted
8390
* @since 3.0
8491
*/
85-
Future<?> submit(Runnable task);
92+
default Future<?> submit(Runnable task) {
93+
FutureTask<Object> future = new FutureTask<>(task, null);
94+
execute(future);
95+
return future;
96+
}
8697

8798
/**
8899
* Submit a Callable task for execution, receiving a Future representing that task.
89100
* The Future will return the Callable's result upon completion.
101+
* <p>As of 6.1, this method comes with a default implementation that delegates
102+
* to {@link #execute(Runnable)}.
90103
* @param task the {@code Callable} to execute (never {@code null})
91104
* @return a Future representing pending completion of the task
92105
* @throws TaskRejectedException if the given task was not accepted
93106
* @since 3.0
94107
*/
95-
<T> Future<T> submit(Callable<T> task);
108+
default <T> Future<T> submit(Callable<T> task) {
109+
FutureTask<T> future = new FutureTask<>(task);
110+
execute(future, TIMEOUT_INDEFINITE);
111+
return future;
112+
}
96113

97114
/**
98115
* Submit a {@code Runnable} task for execution, receiving a {@code CompletableFuture}

spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -66,6 +66,9 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
6666
/** Internal concurrency throttle used by this executor. */
6767
private final ConcurrencyThrottleAdapter concurrencyThrottle = new ConcurrencyThrottleAdapter();
6868

69+
@Nullable
70+
private VirtualThreadDelegate virtualThreadDelegate;
71+
6972
@Nullable
7073
private ThreadFactory threadFactory;
7174

@@ -97,6 +100,16 @@ public SimpleAsyncTaskExecutor(ThreadFactory threadFactory) {
97100
}
98101

99102

103+
/**
104+
* Switch this executor to virtual threads. Requires Java 21 or higher.
105+
* <p>The default is {@code false}, indicating platform threads.
106+
* Set this flag to {@code true} in order to create virtual threads instead.
107+
* @since 6.1
108+
*/
109+
public void setVirtualThreads(boolean virtual) {
110+
this.virtualThreadDelegate = (virtual ? new VirtualThreadDelegate() : null);
111+
}
112+
100113
/**
101114
* Specify an external factory to use for creating new Threads,
102115
* instead of relying on the local properties of this executor.
@@ -238,11 +251,16 @@ public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
238251
* Template method for the actual execution of a task.
239252
* <p>The default implementation creates a new Thread and starts it.
240253
* @param task the Runnable to execute
254+
* @see #setVirtualThreads
241255
* @see #setThreadFactory
242256
* @see #createThread
243257
* @see java.lang.Thread#start()
244258
*/
245259
protected void doExecute(Runnable task) {
260+
if (this.virtualThreadDelegate != null) {
261+
this.virtualThreadDelegate.startVirtualThread(nextThreadName(), task);
262+
}
263+
246264
Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
247265
thread.start();
248266
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.task;
18+
19+
import java.util.concurrent.ThreadFactory;
20+
21+
/**
22+
* Internal delegate for virtual thread handling on JDK 21.
23+
* This is a dummy version for reachability on JDK <21.
24+
*
25+
* @author Juergen Hoeller
26+
* @since 6.1
27+
* @see VirtualThreadTaskExecutor
28+
*/
29+
class VirtualThreadDelegate {
30+
31+
public VirtualThreadDelegate() {
32+
throw new UnsupportedOperationException("Virtual threads not supported on JDK <21");
33+
}
34+
35+
public ThreadFactory virtualThreadFactory() {
36+
throw new UnsupportedOperationException();
37+
}
38+
39+
public ThreadFactory virtualThreadFactory(String threadNamePrefix) {
40+
throw new UnsupportedOperationException();
41+
}
42+
43+
public Thread startVirtualThread(String name, Runnable task) {
44+
throw new UnsupportedOperationException();
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.task;
18+
19+
import java.util.concurrent.ThreadFactory;
20+
21+
/**
22+
* A {@link TaskExecutor} implementation based on virtual threads in JDK 21+.
23+
* The only configuration option is a thread name prefix.
24+
*
25+
* <p>For additional features such as concurrency limiting or task decoration,
26+
* consider using {@link SimpleAsyncTaskExecutor#setVirtualThreads} instead.
27+
*
28+
* @author Juergen Hoeller
29+
* @since 6.1
30+
* @see SimpleAsyncTaskExecutor
31+
*/
32+
public class VirtualThreadTaskExecutor implements AsyncTaskExecutor {
33+
34+
private final ThreadFactory virtualThreadFactory;
35+
36+
37+
/**
38+
* Create a new {@code VirtualThreadTaskExecutor} without thread naming.
39+
*/
40+
public VirtualThreadTaskExecutor() {
41+
this.virtualThreadFactory = new VirtualThreadDelegate().virtualThreadFactory();
42+
}
43+
44+
/**
45+
* Create a new {@code VirtualThreadTaskExecutor} with thread names based
46+
* on the given thread name prefix followed by a counter (e.g. "test-0").
47+
* @param threadNamePrefix the prefix for thread names (e.g. "test-")
48+
*/
49+
public VirtualThreadTaskExecutor(String threadNamePrefix) {
50+
this.virtualThreadFactory = new VirtualThreadDelegate().virtualThreadFactory(threadNamePrefix);
51+
}
52+
53+
54+
/**
55+
* Return the underlying virtual {@link ThreadFactory}.
56+
* Can also be used for custom thread creation elsewhere.
57+
*/
58+
public final ThreadFactory getVirtualThreadFactory() {
59+
return this.virtualThreadFactory;
60+
}
61+
62+
@Override
63+
public void execute(Runnable task) {
64+
this.virtualThreadFactory.newThread(task).start();
65+
}
66+
67+
}

spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -98,12 +98,6 @@ public void execute(Runnable task) {
9898
}
9999
}
100100

101-
@Deprecated
102-
@Override
103-
public void execute(Runnable task, long startTimeout) {
104-
execute(task);
105-
}
106-
107101
@Override
108102
public Future<?> submit(Runnable task) {
109103
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.task;
18+
19+
import java.util.concurrent.ThreadFactory;
20+
21+
/**
22+
* Internal delegate for virtual thread handling on JDK 21.
23+
* This is the actual version compiled against JDK 21.
24+
*
25+
* @author Juergen Hoeller
26+
* @since 6.1
27+
* @see VirtualThreadTaskExecutor
28+
*/
29+
class VirtualThreadDelegate {
30+
31+
private final Thread.Builder threadBuilder = Thread.ofVirtual();
32+
33+
public ThreadFactory virtualThreadFactory() {
34+
return this.threadBuilder.factory();
35+
}
36+
37+
public ThreadFactory virtualThreadFactory(String threadNamePrefix) {
38+
return this.threadBuilder.name(threadNamePrefix, 0).factory();
39+
}
40+
41+
public Thread startVirtualThread(String name, Runnable task) {
42+
return this.threadBuilder.name(name).start(task);
43+
}
44+
45+
}

spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ void convertsToAbsolutePathForClassRelativeAccess() {
214214

215215
@Test
216216
void directoryNotReadable() throws Exception {
217-
Resource fileDir = new ClassPathResource("org/springframework/core");
217+
Resource fileDir = new ClassPathResource("example/type");
218218
assertThat(fileDir.getURL()).asString().startsWith("file:");
219219
assertThat(fileDir.exists()).isTrue();
220220
assertThat(fileDir.isReadable()).isFalse();

0 commit comments

Comments
 (0)