diff --git a/build.gradle b/build.gradle index 7ea133d4f9..e8d3db926a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' apply false id 'de.undercouch.download' version '5.4.0' id 'me.champeau.jmh' version '0.7.0' apply false + id 'me.champeau.mrjar' version '0.1.1' } ext { diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 000b0cdcfe..fa1fd038fd 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -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" ;; 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" diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java index 8be2a3534a..811d76c9ef 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java @@ -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"); * 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 public Future submit(Runnable task) { FutureTask future = new FutureTask<>(task, null); diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java index 144b2962ec..a4c25f474d 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java @@ -364,12 +364,6 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport } } - @Deprecated - @Override - public void execute(Runnable task, long startTimeout) { - execute(task); - } - @Override public Future submit(Runnable task) { ExecutorService executor = getThreadPoolExecutor(); diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java index 42f0e5bcfa..cfafc21c0e 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java @@ -282,12 +282,6 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport } } - @Deprecated - @Override - public void execute(Runnable task, long startTimeout) { - execute(task); - } - @Override public Future submit(Runnable task) { ExecutorService executor = getScheduledExecutor(); diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index a5c503f623..d95dab656b 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -1,15 +1,25 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.springframework.build.shadow.ShadowSource +plugins { + id 'me.champeau.mrjar' +} + description = "Spring Core" apply plugin: "kotlin" apply plugin: "kotlinx-serialization" +multiRelease { + targetVersions 17, 21 +} + def javapoetVersion = "1.13.0" def objenesisVersion = "3.3" configurations { + java21Api.extendsFrom(api) + java21Implementation.extendsFrom(implementation) javapoet objenesis graalvm diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java index 4d65c37313..39cc0b5cb6 100644 --- a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java @@ -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"); * 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.CompletableFuture; import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; import org.springframework.util.concurrent.FutureUtils; @@ -60,6 +61,8 @@ public interface AsyncTaskExecutor extends TaskExecutor { /** * Execute the given {@code task}. + *

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 startTimeout the time duration (milliseconds) within which the task is * 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 - 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. * The Future will return a {@code null} result upon completion. + *

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}) * @return a Future representing pending completion of the task * @throws TaskRejectedException if the given task was not accepted * @since 3.0 */ - Future submit(Runnable task); + default Future submit(Runnable task) { + FutureTask future = new FutureTask<>(task, null); + execute(future); + return future; + } /** * Submit a Callable task for execution, receiving a Future representing that task. * The Future will return the Callable's result upon completion. + *

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}) * @return a Future representing pending completion of the task * @throws TaskRejectedException if the given task was not accepted * @since 3.0 */ - Future submit(Callable task); + default Future submit(Callable task) { + FutureTask future = new FutureTask<>(task); + execute(future, TIMEOUT_INDEFINITE); + return future; + } /** * Submit a {@code Runnable} task for execution, receiving a {@code CompletableFuture} diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java index a8de97fb51..5aad53b19f 100644 --- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -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"); * 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. */ private final ConcurrencyThrottleAdapter concurrencyThrottle = new ConcurrencyThrottleAdapter(); + @Nullable + private VirtualThreadDelegate virtualThreadDelegate; + @Nullable private ThreadFactory threadFactory; @@ -97,6 +100,16 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator } + /** + * Switch this executor to virtual threads. Requires Java 21 or higher. + *

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, * 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. *

The default implementation creates a new Thread and starts it. * @param task the Runnable to execute + * @see #setVirtualThreads * @see #setThreadFactory * @see #createThread * @see java.lang.Thread#start() */ 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.start(); } diff --git a/spring-core/src/main/java/org/springframework/core/task/VirtualThreadDelegate.java b/spring-core/src/main/java/org/springframework/core/task/VirtualThreadDelegate.java new file mode 100644 index 0000000000..ade9a548c2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/VirtualThreadDelegate.java @@ -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(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/VirtualThreadTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/VirtualThreadTaskExecutor.java new file mode 100644 index 0000000000..ae89c2d75f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/VirtualThreadTaskExecutor.java @@ -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. + * + *

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(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java index 2a9b336520..ef2f6388a9 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java @@ -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"); * 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 public Future submit(Runnable task) { try { diff --git a/spring-core/src/main/java21/org/springframework/core/task/VirtualThreadDelegate.java b/spring-core/src/main/java21/org/springframework/core/task/VirtualThreadDelegate.java new file mode 100644 index 0000000000..5971094b35 --- /dev/null +++ b/spring-core/src/main/java21/org/springframework/core/task/VirtualThreadDelegate.java @@ -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); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java index 30320564d2..b864579a06 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java @@ -214,7 +214,7 @@ class ClassPathResourceTests { @Test 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.exists()).isTrue(); assertThat(fileDir.isReadable()).isFalse(); diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java index 5f33f9bbdb..031fc7c7e4 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java @@ -68,8 +68,8 @@ class ResourceTests { @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceIsValid(Resource resource) throws Exception { - assertThat(resource.getFilename()).isEqualTo("Resource.class"); - assertThat(resource.getURL().getFile()).endsWith("Resource.class"); + assertThat(resource.getFilename()).isEqualTo("ResourceTests.class"); + assertThat(resource.getURL().getFile()).endsWith("ResourceTests.class"); assertThat(resource.exists()).isTrue(); assertThat(resource.isReadable()).isTrue(); assertThat(resource.contentLength()).isGreaterThan(0); @@ -80,9 +80,9 @@ class ResourceTests { @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceCreateRelative(Resource resource) throws Exception { - Resource relative1 = resource.createRelative("ClassPathResource.class"); - assertThat(relative1.getFilename()).isEqualTo("ClassPathResource.class"); - assertThat(relative1.getURL().getFile().endsWith("ClassPathResource.class")).isTrue(); + Resource relative1 = resource.createRelative("ClassPathResourceTests.class"); + assertThat(relative1.getFilename()).isEqualTo("ClassPathResourceTests.class"); + assertThat(relative1.getURL().getFile().endsWith("ClassPathResourceTests.class")).isTrue(); assertThat(relative1.exists()).isTrue(); assertThat(relative1.isReadable()).isTrue(); assertThat(relative1.contentLength()).isGreaterThan(0); @@ -92,9 +92,9 @@ class ResourceTests { @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceCreateRelativeWithFolder(Resource resource) throws Exception { - Resource relative2 = resource.createRelative("support/ResourcePatternResolver.class"); - assertThat(relative2.getFilename()).isEqualTo("ResourcePatternResolver.class"); - assertThat(relative2.getURL().getFile()).endsWith("ResourcePatternResolver.class"); + Resource relative2 = resource.createRelative("support/PathMatchingResourcePatternResolverTests.class"); + assertThat(relative2.getFilename()).isEqualTo("PathMatchingResourcePatternResolverTests.class"); + assertThat(relative2.getURL().getFile()).endsWith("PathMatchingResourcePatternResolverTests.class"); assertThat(relative2.exists()).isTrue(); assertThat(relative2.isReadable()).isTrue(); assertThat(relative2.contentLength()).isGreaterThan(0); @@ -104,9 +104,9 @@ class ResourceTests { @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceCreateRelativeWithDotPath(Resource resource) throws Exception { - Resource relative3 = resource.createRelative("../SpringVersion.class"); - assertThat(relative3.getFilename()).isEqualTo("SpringVersion.class"); - assertThat(relative3.getURL().getFile()).endsWith("SpringVersion.class"); + Resource relative3 = resource.createRelative("../CollectionFactoryTests.class"); + assertThat(relative3.getFilename()).isEqualTo("CollectionFactoryTests.class"); + assertThat(relative3.getURL().getFile()).endsWith("CollectionFactoryTests.class"); assertThat(relative3.exists()).isTrue(); assertThat(relative3.isReadable()).isTrue(); assertThat(relative3.contentLength()).isGreaterThan(0); @@ -128,12 +128,12 @@ class ResourceTests { } private static Stream resource() throws URISyntaxException { - URL resourceClass = ResourceTests.class.getResource("Resource.class"); + URL resourceClass = ResourceTests.class.getResource("ResourceTests.class"); Path resourceClassFilePath = Paths.get(resourceClass.toURI()); return Stream.of( - arguments(named("ClassPathResource", new ClassPathResource("org/springframework/core/io/Resource.class"))), - arguments(named("ClassPathResource with ClassLoader", new ClassPathResource("org/springframework/core/io/Resource.class", ResourceTests.class.getClassLoader()))), - arguments(named("ClassPathResource with Class", new ClassPathResource("Resource.class", ResourceTests.class))), + arguments(named("ClassPathResource", new ClassPathResource("org/springframework/core/io/ResourceTests.class"))), + arguments(named("ClassPathResource with ClassLoader", new ClassPathResource("org/springframework/core/io/ResourceTests.class", ResourceTests.class.getClassLoader()))), + arguments(named("ClassPathResource with Class", new ClassPathResource("ResourceTests.class", ResourceTests.class))), arguments(named("FileSystemResource", new FileSystemResource(resourceClass.getFile()))), arguments(named("FileSystemResource with File", new FileSystemResource(new File(resourceClass.getFile())))), arguments(named("FileSystemResource with File path", new FileSystemResource(resourceClassFilePath))), @@ -220,29 +220,29 @@ class ResourceTests { @Test void sameResourceIsEqual() { - String file = getClass().getResource("Resource.class").getFile(); + String file = getClass().getResource("ResourceTests.class").getFile(); Resource resource = new FileSystemResource(file); assertThat(resource).isEqualTo(new FileSystemResource(file)); } @Test 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); assertThat(resource).isEqualTo(new FileSystemResource(file)); } @Test 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); assertThat(resource).isEqualTo(new FileSystemResource(filePath)); } @Test void sameResourceFromDotPathIsEqual() { - Resource resource = new FileSystemResource("core/io/Resource.class"); - assertThat(new FileSystemResource("core/../core/io/./Resource.class")).isEqualTo(resource); + Resource resource = new FileSystemResource("core/io/ResourceTests.class"); + assertThat(new FileSystemResource("core/../core/io/./ResourceTests.class")).isEqualTo(resource); } @Test @@ -254,7 +254,7 @@ class ResourceTests { @Test 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()) { ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength()); channel.read(buffer); @@ -293,8 +293,8 @@ class ResourceTests { @Test void sameResourceWithRelativePathIsEqual() throws Exception { - Resource resource = new UrlResource("file:core/io/Resource.class"); - assertThat(new UrlResource("file:core/../core/io/./Resource.class")).isEqualTo(resource); + Resource resource = new UrlResource("file:core/io/ResourceTests.class"); + assertThat(new UrlResource("file:core/../core/io/./ResourceTests.class")).isEqualTo(resource); } @Test @@ -322,14 +322,14 @@ class ResourceTests { @Test void factoryMethodsProduceEqualResources() throws Exception { - Resource resource1 = new UrlResource("file:core/io/Resource.class"); - Resource resource2 = UrlResource.from("file:core/io/Resource.class"); + Resource resource1 = new UrlResource("file:core/io/ResourceTests.class"); + Resource resource2 = UrlResource.from("file:core/io/ResourceTests.class"); Resource resource3 = UrlResource.from(resource1.getURI()); assertThat(resource2.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"); @@ -433,7 +433,6 @@ class ResourceTests { public String getDescription() { return name; } - @Override public InputStream getInputStream() throws IOException { throw new FileNotFoundException(); @@ -459,7 +458,6 @@ class ResourceTests { public InputStream getInputStream() { return new ByteArrayInputStream(new byte[] {'a', 'b', 'c'}); } - @Override public String getDescription() { return ""; diff --git a/spring-core/src/test/java21/org/springframework/core/task/VirtualThreadTaskExecutorTests.java b/spring-core/src/test/java21/org/springframework/core/task/VirtualThreadTaskExecutorTests.java new file mode 100644 index 0000000000..204c715e28 --- /dev/null +++ b/spring-core/src/test/java21/org/springframework/core/task/VirtualThreadTaskExecutorTests.java @@ -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(); + } + } + +} diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 3298545cdd..496c1943a3 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -11,9 +11,9 @@ - - - + + +