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
This commit is contained in:
Juergen Hoeller 2023-05-08 11:22:47 +02:00
parent d8d7e0a762
commit 697d5e6247
16 changed files with 385 additions and 64 deletions

View File

@ -10,6 +10,7 @@ plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1' apply false id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
id 'de.undercouch.download' version '5.4.0' id 'de.undercouch.download' version '5.4.0'
id 'me.champeau.jmh' version '0.7.0' apply false id 'me.champeau.jmh' version '0.7.0' apply false
id 'me.champeau.mrjar' version '0.1.1'
} }
ext { ext {

View File

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

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -77,12 +77,6 @@ public class SimpleThreadPoolTaskExecutor extends SimpleThreadPool
} }
} }
@Deprecated
@Override
public void execute(Runnable task, long startTimeout) {
execute(task);
}
@Override @Override
public Future<?> submit(Runnable task) { public Future<?> submit(Runnable task) {
FutureTask<Object> future = new FutureTask<>(task, null); FutureTask<Object> future = new FutureTask<>(task, null);

View File

@ -364,12 +364,6 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
} }
} }
@Deprecated
@Override
public void execute(Runnable task, long startTimeout) {
execute(task);
}
@Override @Override
public Future<?> submit(Runnable task) { public Future<?> submit(Runnable task) {
ExecutorService executor = getThreadPoolExecutor(); ExecutorService executor = getThreadPoolExecutor();

View File

@ -282,12 +282,6 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport
} }
} }
@Deprecated
@Override
public void execute(Runnable task, long startTimeout) {
execute(task);
}
@Override @Override
public Future<?> submit(Runnable task) { public Future<?> submit(Runnable task) {
ExecutorService executor = getScheduledExecutor(); ExecutorService executor = getScheduledExecutor();

View File

@ -1,15 +1,25 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.springframework.build.shadow.ShadowSource import org.springframework.build.shadow.ShadowSource
plugins {
id 'me.champeau.mrjar'
}
description = "Spring Core" description = "Spring Core"
apply plugin: "kotlin" apply plugin: "kotlin"
apply plugin: "kotlinx-serialization" apply plugin: "kotlinx-serialization"
multiRelease {
targetVersions 17, 21
}
def javapoetVersion = "1.13.0" def javapoetVersion = "1.13.0"
def objenesisVersion = "3.3" def objenesisVersion = "3.3"
configurations { configurations {
java21Api.extendsFrom(api)
java21Implementation.extendsFrom(implementation)
javapoet javapoet
objenesis objenesis
graalvm graalvm

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.core.task;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import org.springframework.util.concurrent.FutureUtils; import org.springframework.util.concurrent.FutureUtils;
@ -60,6 +61,8 @@ public interface AsyncTaskExecutor extends TaskExecutor {
/** /**
* Execute the given {@code task}. * Execute the given {@code task}.
* <p>As of 6.1, this method comes with a default implementation that simply
* delegates to {@link #execute(Runnable)}, ignoring the timeout completely.
* @param task the {@code Runnable} to execute (never {@code null}) * @param task the {@code Runnable} to execute (never {@code null})
* @param startTimeout the time duration (milliseconds) within which the task is * @param startTimeout the time duration (milliseconds) within which the task is
* supposed to start. This is intended as a hint to the executor, allowing for * supposed to start. This is intended as a hint to the executor, allowing for
@ -72,27 +75,41 @@ public interface AsyncTaskExecutor extends TaskExecutor {
* @deprecated as of 5.3.16 since the common executors do not support start timeouts * @deprecated as of 5.3.16 since the common executors do not support start timeouts
*/ */
@Deprecated @Deprecated
void execute(Runnable task, long startTimeout); default void execute(Runnable task, long startTimeout) {
execute(task);
}
/** /**
* Submit a Runnable task for execution, receiving a Future representing that task. * Submit a Runnable task for execution, receiving a Future representing that task.
* The Future will return a {@code null} result upon completion. * The Future will return a {@code null} result upon completion.
* <p>As of 6.1, this method comes with a default implementation that delegates
* to {@link #execute(Runnable)}.
* @param task the {@code Runnable} to execute (never {@code null}) * @param task the {@code Runnable} to execute (never {@code null})
* @return a Future representing pending completion of the task * @return a Future representing pending completion of the task
* @throws TaskRejectedException if the given task was not accepted * @throws TaskRejectedException if the given task was not accepted
* @since 3.0 * @since 3.0
*/ */
Future<?> submit(Runnable task); default Future<?> submit(Runnable task) {
FutureTask<Object> future = new FutureTask<>(task, null);
execute(future);
return future;
}
/** /**
* Submit a Callable task for execution, receiving a Future representing that task. * Submit a Callable task for execution, receiving a Future representing that task.
* The Future will return the Callable's result upon completion. * The Future will return the Callable's result upon completion.
* <p>As of 6.1, this method comes with a default implementation that delegates
* to {@link #execute(Runnable)}.
* @param task the {@code Callable} to execute (never {@code null}) * @param task the {@code Callable} to execute (never {@code null})
* @return a Future representing pending completion of the task * @return a Future representing pending completion of the task
* @throws TaskRejectedException if the given task was not accepted * @throws TaskRejectedException if the given task was not accepted
* @since 3.0 * @since 3.0
*/ */
<T> Future<T> submit(Callable<T> task); default <T> Future<T> submit(Callable<T> task) {
FutureTask<T> future = new FutureTask<>(task);
execute(future, TIMEOUT_INDEFINITE);
return future;
}
/** /**
* Submit a {@code Runnable} task for execution, receiving a {@code CompletableFuture} * Submit a {@code Runnable} task for execution, receiving a {@code CompletableFuture}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -66,6 +66,9 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
/** Internal concurrency throttle used by this executor. */ /** Internal concurrency throttle used by this executor. */
private final ConcurrencyThrottleAdapter concurrencyThrottle = new ConcurrencyThrottleAdapter(); private final ConcurrencyThrottleAdapter concurrencyThrottle = new ConcurrencyThrottleAdapter();
@Nullable
private VirtualThreadDelegate virtualThreadDelegate;
@Nullable @Nullable
private ThreadFactory threadFactory; private ThreadFactory threadFactory;
@ -97,6 +100,16 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
} }
/**
* Switch this executor to virtual threads. Requires Java 21 or higher.
* <p>The default is {@code false}, indicating platform threads.
* Set this flag to {@code true} in order to create virtual threads instead.
* @since 6.1
*/
public void setVirtualThreads(boolean virtual) {
this.virtualThreadDelegate = (virtual ? new VirtualThreadDelegate() : null);
}
/** /**
* Specify an external factory to use for creating new Threads, * Specify an external factory to use for creating new Threads,
* instead of relying on the local properties of this executor. * instead of relying on the local properties of this executor.
@ -238,11 +251,16 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* Template method for the actual execution of a task. * Template method for the actual execution of a task.
* <p>The default implementation creates a new Thread and starts it. * <p>The default implementation creates a new Thread and starts it.
* @param task the Runnable to execute * @param task the Runnable to execute
* @see #setVirtualThreads
* @see #setThreadFactory * @see #setThreadFactory
* @see #createThread * @see #createThread
* @see java.lang.Thread#start() * @see java.lang.Thread#start()
*/ */
protected void doExecute(Runnable task) { protected void doExecute(Runnable task) {
if (this.virtualThreadDelegate != null) {
this.virtualThreadDelegate.startVirtualThread(nextThreadName(), task);
}
Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
thread.start(); thread.start();
} }

View File

@ -0,0 +1,47 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.task;
import java.util.concurrent.ThreadFactory;
/**
* Internal delegate for virtual thread handling on JDK 21.
* This is a dummy version for reachability on JDK <21.
*
* @author Juergen Hoeller
* @since 6.1
* @see VirtualThreadTaskExecutor
*/
class VirtualThreadDelegate {
public VirtualThreadDelegate() {
throw new UnsupportedOperationException("Virtual threads not supported on JDK <21");
}
public ThreadFactory virtualThreadFactory() {
throw new UnsupportedOperationException();
}
public ThreadFactory virtualThreadFactory(String threadNamePrefix) {
throw new UnsupportedOperationException();
}
public Thread startVirtualThread(String name, Runnable task) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.task;
import java.util.concurrent.ThreadFactory;
/**
* A {@link TaskExecutor} implementation based on virtual threads in JDK 21+.
* The only configuration option is a thread name prefix.
*
* <p>For additional features such as concurrency limiting or task decoration,
* consider using {@link SimpleAsyncTaskExecutor#setVirtualThreads} instead.
*
* @author Juergen Hoeller
* @since 6.1
* @see SimpleAsyncTaskExecutor
*/
public class VirtualThreadTaskExecutor implements AsyncTaskExecutor {
private final ThreadFactory virtualThreadFactory;
/**
* Create a new {@code VirtualThreadTaskExecutor} without thread naming.
*/
public VirtualThreadTaskExecutor() {
this.virtualThreadFactory = new VirtualThreadDelegate().virtualThreadFactory();
}
/**
* Create a new {@code VirtualThreadTaskExecutor} with thread names based
* on the given thread name prefix followed by a counter (e.g. "test-0").
* @param threadNamePrefix the prefix for thread names (e.g. "test-")
*/
public VirtualThreadTaskExecutor(String threadNamePrefix) {
this.virtualThreadFactory = new VirtualThreadDelegate().virtualThreadFactory(threadNamePrefix);
}
/**
* Return the underlying virtual {@link ThreadFactory}.
* Can also be used for custom thread creation elsewhere.
*/
public final ThreadFactory getVirtualThreadFactory() {
return this.virtualThreadFactory;
}
@Override
public void execute(Runnable task) {
this.virtualThreadFactory.newThread(task).start();
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -98,12 +98,6 @@ public class TaskExecutorAdapter implements AsyncListenableTaskExecutor {
} }
} }
@Deprecated
@Override
public void execute(Runnable task, long startTimeout) {
execute(task);
}
@Override @Override
public Future<?> submit(Runnable task) { public Future<?> submit(Runnable task) {
try { try {

View File

@ -0,0 +1,45 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.task;
import java.util.concurrent.ThreadFactory;
/**
* Internal delegate for virtual thread handling on JDK 21.
* This is the actual version compiled against JDK 21.
*
* @author Juergen Hoeller
* @since 6.1
* @see VirtualThreadTaskExecutor
*/
class VirtualThreadDelegate {
private final Thread.Builder threadBuilder = Thread.ofVirtual();
public ThreadFactory virtualThreadFactory() {
return this.threadBuilder.factory();
}
public ThreadFactory virtualThreadFactory(String threadNamePrefix) {
return this.threadBuilder.name(threadNamePrefix, 0).factory();
}
public Thread startVirtualThread(String name, Runnable task) {
return this.threadBuilder.name(name).start(task);
}
}

View File

@ -214,7 +214,7 @@ class ClassPathResourceTests {
@Test @Test
void directoryNotReadable() throws Exception { void directoryNotReadable() throws Exception {
Resource fileDir = new ClassPathResource("org/springframework/core"); Resource fileDir = new ClassPathResource("example/type");
assertThat(fileDir.getURL()).asString().startsWith("file:"); assertThat(fileDir.getURL()).asString().startsWith("file:");
assertThat(fileDir.exists()).isTrue(); assertThat(fileDir.exists()).isTrue();
assertThat(fileDir.isReadable()).isFalse(); assertThat(fileDir.isReadable()).isFalse();

View File

@ -68,8 +68,8 @@ class ResourceTests {
@ParameterizedTest(name = "{index}: {0}") @ParameterizedTest(name = "{index}: {0}")
@MethodSource("resource") @MethodSource("resource")
void resourceIsValid(Resource resource) throws Exception { void resourceIsValid(Resource resource) throws Exception {
assertThat(resource.getFilename()).isEqualTo("Resource.class"); assertThat(resource.getFilename()).isEqualTo("ResourceTests.class");
assertThat(resource.getURL().getFile()).endsWith("Resource.class"); assertThat(resource.getURL().getFile()).endsWith("ResourceTests.class");
assertThat(resource.exists()).isTrue(); assertThat(resource.exists()).isTrue();
assertThat(resource.isReadable()).isTrue(); assertThat(resource.isReadable()).isTrue();
assertThat(resource.contentLength()).isGreaterThan(0); assertThat(resource.contentLength()).isGreaterThan(0);
@ -80,9 +80,9 @@ class ResourceTests {
@ParameterizedTest(name = "{index}: {0}") @ParameterizedTest(name = "{index}: {0}")
@MethodSource("resource") @MethodSource("resource")
void resourceCreateRelative(Resource resource) throws Exception { void resourceCreateRelative(Resource resource) throws Exception {
Resource relative1 = resource.createRelative("ClassPathResource.class"); Resource relative1 = resource.createRelative("ClassPathResourceTests.class");
assertThat(relative1.getFilename()).isEqualTo("ClassPathResource.class"); assertThat(relative1.getFilename()).isEqualTo("ClassPathResourceTests.class");
assertThat(relative1.getURL().getFile().endsWith("ClassPathResource.class")).isTrue(); assertThat(relative1.getURL().getFile().endsWith("ClassPathResourceTests.class")).isTrue();
assertThat(relative1.exists()).isTrue(); assertThat(relative1.exists()).isTrue();
assertThat(relative1.isReadable()).isTrue(); assertThat(relative1.isReadable()).isTrue();
assertThat(relative1.contentLength()).isGreaterThan(0); assertThat(relative1.contentLength()).isGreaterThan(0);
@ -92,9 +92,9 @@ class ResourceTests {
@ParameterizedTest(name = "{index}: {0}") @ParameterizedTest(name = "{index}: {0}")
@MethodSource("resource") @MethodSource("resource")
void resourceCreateRelativeWithFolder(Resource resource) throws Exception { void resourceCreateRelativeWithFolder(Resource resource) throws Exception {
Resource relative2 = resource.createRelative("support/ResourcePatternResolver.class"); Resource relative2 = resource.createRelative("support/PathMatchingResourcePatternResolverTests.class");
assertThat(relative2.getFilename()).isEqualTo("ResourcePatternResolver.class"); assertThat(relative2.getFilename()).isEqualTo("PathMatchingResourcePatternResolverTests.class");
assertThat(relative2.getURL().getFile()).endsWith("ResourcePatternResolver.class"); assertThat(relative2.getURL().getFile()).endsWith("PathMatchingResourcePatternResolverTests.class");
assertThat(relative2.exists()).isTrue(); assertThat(relative2.exists()).isTrue();
assertThat(relative2.isReadable()).isTrue(); assertThat(relative2.isReadable()).isTrue();
assertThat(relative2.contentLength()).isGreaterThan(0); assertThat(relative2.contentLength()).isGreaterThan(0);
@ -104,9 +104,9 @@ class ResourceTests {
@ParameterizedTest(name = "{index}: {0}") @ParameterizedTest(name = "{index}: {0}")
@MethodSource("resource") @MethodSource("resource")
void resourceCreateRelativeWithDotPath(Resource resource) throws Exception { void resourceCreateRelativeWithDotPath(Resource resource) throws Exception {
Resource relative3 = resource.createRelative("../SpringVersion.class"); Resource relative3 = resource.createRelative("../CollectionFactoryTests.class");
assertThat(relative3.getFilename()).isEqualTo("SpringVersion.class"); assertThat(relative3.getFilename()).isEqualTo("CollectionFactoryTests.class");
assertThat(relative3.getURL().getFile()).endsWith("SpringVersion.class"); assertThat(relative3.getURL().getFile()).endsWith("CollectionFactoryTests.class");
assertThat(relative3.exists()).isTrue(); assertThat(relative3.exists()).isTrue();
assertThat(relative3.isReadable()).isTrue(); assertThat(relative3.isReadable()).isTrue();
assertThat(relative3.contentLength()).isGreaterThan(0); assertThat(relative3.contentLength()).isGreaterThan(0);
@ -128,12 +128,12 @@ class ResourceTests {
} }
private static Stream<Arguments> resource() throws URISyntaxException { private static Stream<Arguments> resource() throws URISyntaxException {
URL resourceClass = ResourceTests.class.getResource("Resource.class"); URL resourceClass = ResourceTests.class.getResource("ResourceTests.class");
Path resourceClassFilePath = Paths.get(resourceClass.toURI()); Path resourceClassFilePath = Paths.get(resourceClass.toURI());
return Stream.of( return Stream.of(
arguments(named("ClassPathResource", new ClassPathResource("org/springframework/core/io/Resource.class"))), arguments(named("ClassPathResource", new ClassPathResource("org/springframework/core/io/ResourceTests.class"))),
arguments(named("ClassPathResource with ClassLoader", new ClassPathResource("org/springframework/core/io/Resource.class", ResourceTests.class.getClassLoader()))), arguments(named("ClassPathResource with ClassLoader", new ClassPathResource("org/springframework/core/io/ResourceTests.class", ResourceTests.class.getClassLoader()))),
arguments(named("ClassPathResource with Class", new ClassPathResource("Resource.class", ResourceTests.class))), arguments(named("ClassPathResource with Class", new ClassPathResource("ResourceTests.class", ResourceTests.class))),
arguments(named("FileSystemResource", new FileSystemResource(resourceClass.getFile()))), arguments(named("FileSystemResource", new FileSystemResource(resourceClass.getFile()))),
arguments(named("FileSystemResource with File", new FileSystemResource(new File(resourceClass.getFile())))), arguments(named("FileSystemResource with File", new FileSystemResource(new File(resourceClass.getFile())))),
arguments(named("FileSystemResource with File path", new FileSystemResource(resourceClassFilePath))), arguments(named("FileSystemResource with File path", new FileSystemResource(resourceClassFilePath))),
@ -220,29 +220,29 @@ class ResourceTests {
@Test @Test
void sameResourceIsEqual() { void sameResourceIsEqual() {
String file = getClass().getResource("Resource.class").getFile(); String file = getClass().getResource("ResourceTests.class").getFile();
Resource resource = new FileSystemResource(file); Resource resource = new FileSystemResource(file);
assertThat(resource).isEqualTo(new FileSystemResource(file)); assertThat(resource).isEqualTo(new FileSystemResource(file));
} }
@Test @Test
void sameResourceFromFileIsEqual() { void sameResourceFromFileIsEqual() {
File file = new File(getClass().getResource("Resource.class").getFile()); File file = new File(getClass().getResource("ResourceTests.class").getFile());
Resource resource = new FileSystemResource(file); Resource resource = new FileSystemResource(file);
assertThat(resource).isEqualTo(new FileSystemResource(file)); assertThat(resource).isEqualTo(new FileSystemResource(file));
} }
@Test @Test
void sameResourceFromFilePathIsEqual() throws Exception { void sameResourceFromFilePathIsEqual() throws Exception {
Path filePath = Paths.get(getClass().getResource("Resource.class").toURI()); Path filePath = Paths.get(getClass().getResource("ResourceTests.class").toURI());
Resource resource = new FileSystemResource(filePath); Resource resource = new FileSystemResource(filePath);
assertThat(resource).isEqualTo(new FileSystemResource(filePath)); assertThat(resource).isEqualTo(new FileSystemResource(filePath));
} }
@Test @Test
void sameResourceFromDotPathIsEqual() { void sameResourceFromDotPathIsEqual() {
Resource resource = new FileSystemResource("core/io/Resource.class"); Resource resource = new FileSystemResource("core/io/ResourceTests.class");
assertThat(new FileSystemResource("core/../core/io/./Resource.class")).isEqualTo(resource); assertThat(new FileSystemResource("core/../core/io/./ResourceTests.class")).isEqualTo(resource);
} }
@Test @Test
@ -254,7 +254,7 @@ class ResourceTests {
@Test @Test
void readableChannelProvidesContent() throws Exception { void readableChannelProvidesContent() throws Exception {
Resource resource = new FileSystemResource(getClass().getResource("Resource.class").getFile()); Resource resource = new FileSystemResource(getClass().getResource("ResourceTests.class").getFile());
try (ReadableByteChannel channel = resource.readableChannel()) { try (ReadableByteChannel channel = resource.readableChannel()) {
ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength()); ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength());
channel.read(buffer); channel.read(buffer);
@ -293,8 +293,8 @@ class ResourceTests {
@Test @Test
void sameResourceWithRelativePathIsEqual() throws Exception { void sameResourceWithRelativePathIsEqual() throws Exception {
Resource resource = new UrlResource("file:core/io/Resource.class"); Resource resource = new UrlResource("file:core/io/ResourceTests.class");
assertThat(new UrlResource("file:core/../core/io/./Resource.class")).isEqualTo(resource); assertThat(new UrlResource("file:core/../core/io/./ResourceTests.class")).isEqualTo(resource);
} }
@Test @Test
@ -322,14 +322,14 @@ class ResourceTests {
@Test @Test
void factoryMethodsProduceEqualResources() throws Exception { void factoryMethodsProduceEqualResources() throws Exception {
Resource resource1 = new UrlResource("file:core/io/Resource.class"); Resource resource1 = new UrlResource("file:core/io/ResourceTests.class");
Resource resource2 = UrlResource.from("file:core/io/Resource.class"); Resource resource2 = UrlResource.from("file:core/io/ResourceTests.class");
Resource resource3 = UrlResource.from(resource1.getURI()); Resource resource3 = UrlResource.from(resource1.getURI());
assertThat(resource2.getURL()).isEqualTo(resource1.getURL()); assertThat(resource2.getURL()).isEqualTo(resource1.getURL());
assertThat(resource3.getURL()).isEqualTo(resource1.getURL()); assertThat(resource3.getURL()).isEqualTo(resource1.getURL());
assertThat(UrlResource.from("file:core/../core/io/./Resource.class")).isEqualTo(resource1); assertThat(UrlResource.from("file:core/../core/io/./ResourceTests.class")).isEqualTo(resource1);
assertThat(UrlResource.from("file:/dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); assertThat(UrlResource.from("file:/dir/test.txt?argh").getFilename()).isEqualTo("test.txt");
assertThat(UrlResource.from("file:\\dir\\test.txt?argh").getFilename()).isEqualTo("test.txt"); assertThat(UrlResource.from("file:\\dir\\test.txt?argh").getFilename()).isEqualTo("test.txt");
assertThat(UrlResource.from("file:\\dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); assertThat(UrlResource.from("file:\\dir/test.txt?argh").getFilename()).isEqualTo("test.txt");
@ -433,7 +433,6 @@ class ResourceTests {
public String getDescription() { public String getDescription() {
return name; return name;
} }
@Override @Override
public InputStream getInputStream() throws IOException { public InputStream getInputStream() throws IOException {
throw new FileNotFoundException(); throw new FileNotFoundException();
@ -459,7 +458,6 @@ class ResourceTests {
public InputStream getInputStream() { public InputStream getInputStream() {
return new ByteArrayInputStream(new byte[] {'a', 'b', 'c'}); return new ByteArrayInputStream(new byte[] {'a', 'b', 'c'});
} }
@Override @Override
public String getDescription() { public String getDescription() {
return ""; return "";

View File

@ -0,0 +1,142 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.task;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Juergen Hoeller
* @since 6.1
*/
class VirtualThreadTaskExecutorTests {
@Test
void virtualThreadsWithoutName() {
final Object monitor = new Object();
VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor();
ThreadNameHarvester task = new ThreadNameHarvester(monitor);
executeAndWait(executor, task, monitor);
assertThat(task.getThreadName()).isEmpty();
assertThat(task.isVirtual()).isTrue();
}
@Test
void virtualThreadsWithNamePrefix() {
final Object monitor = new Object();
VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("test-");
ThreadNameHarvester task = new ThreadNameHarvester(monitor);
executeAndWait(executor, task, monitor);
assertThat(task.getThreadName()).isEqualTo("test-0");
assertThat(task.isVirtual()).isTrue();
}
@Test
void simpleWithVirtualThreadFactory() {
final Object monitor = new Object();
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(Thread.ofVirtual().name("test").factory());
ThreadNameHarvester task = new ThreadNameHarvester(monitor);
executeAndWait(executor, task, monitor);
assertThat(task.getThreadName()).isEqualTo("test");
assertThat(task.isVirtual()).isTrue();
}
@Test
void simpleWithVirtualThreadFlag() {
final String customPrefix = "chankPop#";
final Object monitor = new Object();
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix);
executor.setVirtualThreads(true);
ThreadNameHarvester task = new ThreadNameHarvester(monitor);
executeAndWait(executor, task, monitor);
assertThat(task.getThreadName()).startsWith(customPrefix);
assertThat(task.isVirtual()).isTrue();
}
private void executeAndWait(TaskExecutor executor, Runnable task, Object monitor) {
synchronized (monitor) {
executor.execute(task);
try {
monitor.wait();
}
catch (InterruptedException ignored) {
}
}
}
private static final class NoOpRunnable implements Runnable {
@Override
public void run() {
// no-op
}
}
private static abstract class AbstractNotifyingRunnable implements Runnable {
private final Object monitor;
protected AbstractNotifyingRunnable(Object monitor) {
this.monitor = monitor;
}
@Override
public final void run() {
synchronized (this.monitor) {
try {
doRun();
}
finally {
this.monitor.notifyAll();
}
}
}
protected abstract void doRun();
}
private static final class ThreadNameHarvester extends AbstractNotifyingRunnable {
private String threadName;
private boolean virtual;
protected ThreadNameHarvester(Object monitor) {
super(monitor);
}
public String getThreadName() {
return this.threadName;
}
public boolean isVirtual() {
return this.virtual;
}
@Override
protected void doRun() {
Thread thread = Thread.currentThread();
this.threadName = thread.getName();
this.virtual = thread.isVirtual();
}
}
}

View File

@ -11,9 +11,9 @@
<suppress files="(^(?!.+[\\/]src[\\/]main[\\/]java[\\/].*package-info\.java))|(.*framework-docs.*)|(.*spring-(context-indexer|instrument|jcl).*)" checks="RegexpSinglelineJava" id="packageLevelNonNullFieldsAnnotation" /> <suppress files="(^(?!.+[\\/]src[\\/]main[\\/]java[\\/].*package-info\.java))|(.*framework-docs.*)|(.*spring-(context-indexer|instrument|jcl).*)" checks="RegexpSinglelineJava" id="packageLevelNonNullFieldsAnnotation" />
<!-- Global: tests and test fixtures --> <!-- Global: tests and test fixtures -->
<suppress files="[\\/]src[\\/](test|testFixtures)[\\/]java[\\/]" checks="AnnotationLocation|AnnotationUseStyle|AtclauseOrder|AvoidNestedBlocks|FinalClass|HideUtilityClassConstructor|InnerTypeLast|JavadocStyle|JavadocType|JavadocVariable|LeftCurly|MultipleVariableDeclarations|NeedBraces|OneTopLevelClass|OuterTypeFilename|RequireThis|SpringCatch|SpringJavadoc|SpringNoThis"/> <suppress files="[\\/]src[\\/](test|testFixtures)[\\/](java|java21)[\\/]" checks="AnnotationLocation|AnnotationUseStyle|AtclauseOrder|AvoidNestedBlocks|FinalClass|HideUtilityClassConstructor|InnerTypeLast|JavadocStyle|JavadocType|JavadocVariable|LeftCurly|MultipleVariableDeclarations|NeedBraces|OneTopLevelClass|OuterTypeFilename|RequireThis|SpringCatch|SpringJavadoc|SpringNoThis"/>
<suppress files="[\\/]src[\\/](test|testFixtures)[\\/]java[\\/]org[\\/]springframework[\\/].+(Tests|Suite)" checks="IllegalImport" id="bannedJUnitJupiterImports"/> <suppress files="[\\/]src[\\/](test|testFixtures)[\\/]java[\\/]org[\\/]springframework[\\/].+(Tests|Suite)" checks="IllegalImport" id="bannedJUnitJupiterImports"/> <suppress files="[\\/]src[\\/](test|testFixtures)[\\/](java|java21)[\\/]org[\\/]springframework[\\/].+(Tests|Suite)" checks="IllegalImport" id="bannedJUnitJupiterImports"/>
<suppress files="[\\/]src[\\/](test|testFixtures)[\\/]java[\\/]" checks="SpringJUnit5" message="should not be public"/> <suppress files="[\\/]src[\\/](test|testFixtures)[\\/]java[\\/]" checks="SpringJUnit5" message="should not be public"/> <suppress files="[\\/]src[\\/](test|testFixtures)[\\/](java|java21)[\\/]" checks="SpringJUnit5" message="should not be public"/>
<!-- JMH benchmarks --> <!-- JMH benchmarks -->
<suppress files="[\\/]src[\\/]jmh[\\/]java[\\/]org[\\/]springframework[\\/]" checks="JavadocVariable|JavadocStyle|InnerTypeLast"/> <suppress files="[\\/]src[\\/]jmh[\\/]java[\\/]org[\\/]springframework[\\/]" checks="JavadocVariable|JavadocStyle|InnerTypeLast"/>