From e90be0c65b6dc9d7551d333429c91394544bddf7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 27 Oct 2023 11:01:21 +0200 Subject: [PATCH] Add examples using Virtual Threads. Closes #665 --- README.adoc | 8 +- .../jpa/simple/SimpleConfiguration.java | 2 + .../jpa/simple/SimpleUserRepository.java | 4 +- .../jpa/simple/VirtualThreadsTests.java | 114 ++++++++++++++++++ redis/pom.xml | 1 + redis/pubsub/README.md | 4 + redis/pubsub/pom.xml | 27 +++++ .../example/springdata/redis/PubSubTests.java | 109 +++++++++++++++++ .../redis/PubSubVirtualThreadsTests.java | 76 ++++++++++++ .../redis/RedisTestConfiguration.java | 24 ++++ .../src/test/resources/application.properties | 1 + 11 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 jpa/example/src/test/java/example/springdata/jpa/simple/VirtualThreadsTests.java create mode 100644 redis/pubsub/README.md create mode 100644 redis/pubsub/pom.xml create mode 100644 redis/pubsub/src/test/java/example/springdata/redis/PubSubTests.java create mode 100644 redis/pubsub/src/test/java/example/springdata/redis/PubSubVirtualThreadsTests.java create mode 100644 redis/pubsub/src/test/java/example/springdata/redis/RedisTestConfiguration.java create mode 100644 redis/pubsub/src/test/resources/application.properties diff --git a/README.adoc b/README.adoc index 4865cc18..9ce0564f 100644 --- a/README.adoc +++ b/README.adoc @@ -29,13 +29,16 @@ Local Elasticsearch instance must be running to run the tests. == Spring Data JPA * `eclipselink` - Sample project to show how to use Spring Data JPA with Spring Boot and https://www.eclipse.org/eclipselink/[Eclipselink]. -* `example` - Probably the project you want to have a look at first. Contains a variety of sample packages, showcasing the different levels at which you can use Spring Data JPA. Have a look at the `simple` package for the most basic setup. +* `example` - Probably the project you want to have a look at first. +Contains a variety of sample packages, showcasing the different levels at which you can use Spring Data JPA. +Have a look at the `simple` package for the most basic setup. +Contains also examples running on Virtual Threads. * `interceptors` - Example of how to enrich the repositories with AOP. * `jpa21` - Shows support for JPA 2.1 specific features (stored procedures support). * `multiple-datasources` - Examples of how to use Spring Data JPA with multiple `DataSource`s. * `query-by-example` - Example project showing usage of Query by Example with Spring Data JPA. * `security` - Example of how to integrate Spring Data JPA Repositories with Spring Security. -* `showcase` - Refactoring show case of how to improve a plain-JPA-based persistence layer by using Spring Data JPA (read: removing close to all of the implementation code). Follow the `demo.txt` file for detailed instructions. +* `showcase` - Refactoring show case of how to improve a plain-JPA-based persistence layer by using Spring Data JPA (read: removing close to all of the implementation code).Follow the `demo.txt` file for detailed instructions. * `vavr` - Shows the support of https://www.vavr.io[Vavr] collection types as return types for query methods. == Spring Data LDAP @@ -72,6 +75,7 @@ Local Elasticsearch instance must be running to run the tests. * `cluster` - Example for Redis Cluster support. * `example` - Example for basic Spring Data Redis setup. +* `pubsub` - Example project to show Pub/Sub usage using Platform and Virtual Threads. * `reactive` - Example project to show reactive template support. * `repositories` - Example demonstrating Spring Data repository abstraction on top of Redis. * `sentinel` - Example for Redis Sentinel support. diff --git a/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleConfiguration.java b/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleConfiguration.java index 5b3c378a..f509ade1 100644 --- a/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleConfiguration.java +++ b/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleConfiguration.java @@ -16,9 +16,11 @@ package example.springdata.jpa.simple; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; /** * @author Oliver Gierke */ @SpringBootApplication +@EnableAsync class SimpleConfiguration {} diff --git a/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleUserRepository.java b/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleUserRepository.java index 5b6aeaae..75bfcc65 100644 --- a/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleUserRepository.java +++ b/jpa/example/src/main/java/example/springdata/jpa/simple/SimpleUserRepository.java @@ -24,7 +24,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.ListCrudRepository; import org.springframework.scheduling.annotation.Async; /** @@ -35,7 +35,7 @@ import org.springframework.scheduling.annotation.Async; * @author Thomas Darimont * @author Christoph Strobl */ -public interface SimpleUserRepository extends CrudRepository { +public interface SimpleUserRepository extends ListCrudRepository { /** * Find the user with the given username. This method will be translated into a query using the diff --git a/jpa/example/src/test/java/example/springdata/jpa/simple/VirtualThreadsTests.java b/jpa/example/src/test/java/example/springdata/jpa/simple/VirtualThreadsTests.java new file mode 100644 index 00000000..af581aa3 --- /dev/null +++ b/jpa/example/src/test/java/example/springdata/jpa/simple/VirtualThreadsTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.jpa.simple; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration test showing the basic usage of {@link SimpleUserRepository} with Virtual Threads. + * + * @author Mark Paluch + */ +@Transactional +@SpringBootTest(properties = "spring.threads.virtual.enabled=true") +@EnabledOnJre(JRE.JAVA_21) +class VirtualThreadsTests { + + @Autowired SimpleUserRepository repository; + private User user; + + @BeforeEach + void setUp() { + + user = new User(); + user.setUsername("foobar"); + user.setFirstname("firstname"); + user.setLastname("lastname"); + } + + /** + * This repository invocation runs on a dedicated virtual thread. + */ + @Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + void supportsVirtualThreads() throws Exception { + + BlockingQueue thread = new LinkedBlockingQueue<>(); + repository.save(new User("Customer1", "Foo")); + repository.save(new User("Customer2", "Bar")); + + try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) { + executor.setVirtualThreads(true); + + var future = executor.submit(() -> { + thread.add(Thread.currentThread().toString()); + return repository.findAll(); + }); + + List users = future.get(); + String threadName = thread.poll(1, TimeUnit.SECONDS); + + assertThat(threadName).contains("VirtualThread"); + assertThat(users).hasSize(2); + } + + repository.deleteAll(); + } + + /** + * Here we demonstrate the usage of {@link CompletableFuture} as a result wrapper for asynchronous repository query + * methods running on Virtual Threads. Note, that we need to disable the surrounding transaction to be able to + * asynchronously read the written data from another thread within the same test method. + */ + @Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + void asyncUsesVirtualThreads() throws Exception { + + BlockingQueue thread = new LinkedBlockingQueue<>(); + repository.save(new User("Customer1", "Foo")); + repository.save(new User("Customer2", "Bar")); + + var future = repository.readAllBy().thenAccept(users -> { + + assertThat(users).hasSize(2); + thread.add(Thread.currentThread().toString()); + }); + + future.join(); + String threadName = thread.poll(1, TimeUnit.SECONDS); + + assertThat(threadName).contains("VirtualThread"); + + repository.deleteAll(); + } +} diff --git a/redis/pom.xml b/redis/pom.xml index 27a797f6..8d99e58e 100644 --- a/redis/pom.xml +++ b/redis/pom.xml @@ -18,6 +18,7 @@ cluster example + pubsub reactive repositories sentinel diff --git a/redis/pubsub/README.md b/redis/pubsub/README.md new file mode 100644 index 00000000..5514d13d --- /dev/null +++ b/redis/pubsub/README.md @@ -0,0 +1,4 @@ +# Spring Data Redis Pub/Sub Example + +This project contains samples of specific features of Spring Data Redis. + diff --git a/redis/pubsub/pom.xml b/redis/pubsub/pom.xml new file mode 100644 index 00000000..9ba42bdf --- /dev/null +++ b/redis/pubsub/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + spring-data-redis-pubsub + Spring Data Redis - Pub/Sub + + + org.springframework.data.examples + spring-data-redis-examples + 2.0.0.BUILD-SNAPSHOT + ../pom.xml + + + + + + ${project.groupId} + spring-data-redis-example-utils + ${project.version} + test + + + + + diff --git a/redis/pubsub/src/test/java/example/springdata/redis/PubSubTests.java b/redis/pubsub/src/test/java/example/springdata/redis/PubSubTests.java new file mode 100644 index 00000000..566465ad --- /dev/null +++ b/redis/pubsub/src/test/java/example/springdata/redis/PubSubTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.redis; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collection; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Show usage of Redis Pub/Sub operations. + * + * @author Mark Paluch + */ +@SpringBootTest +public class PubSubTests { + + @Autowired RedisConnectionFactory connectionFactory; + + @Autowired StringRedisTemplate redisTemplate; + + @Test + void shouldListenToPubSubEvents() throws Exception { + + BlockingQueue events = new LinkedBlockingDeque<>(); + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.afterPropertiesSet(); + container.addMessageListener( + (message, pattern) -> events.add(String.format("%s@%s", new String(message.getBody()), new String(pattern))), + ChannelTopic.of("my-channel")); + + container.start(); + + redisTemplate.convertAndSend("my-channel", "Hello, world!"); + + String event = events.poll(5, TimeUnit.SECONDS); + + container.stop(); + container.destroy(); + + assertThat(event).isEqualTo("Hello, world!@my-channel"); + } + + @Test + void shouldNotifyListener() throws Exception { + + BlockingQueue events = new LinkedBlockingDeque<>(); + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.afterPropertiesSet(); + + MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(new MyListener(events)); + messageListenerAdapter.afterPropertiesSet(); + messageListenerAdapter.setSerializer(StringRedisSerializer.UTF_8); + container.addMessageListener(messageListenerAdapter, ChannelTopic.of("my-channel")); + + container.start(); + + redisTemplate.convertAndSend("my-channel", "Hello, world!"); + + String event = events.poll(5, TimeUnit.SECONDS); + + container.stop(); + container.destroy(); + + assertThat(event).isEqualTo("Hello, world!@my-channel"); + } + + static class MyListener { + private final Collection events; + + public MyListener(Collection events) { + this.events = events; + } + + public void handleMessage(String message, String channel) { + events.add(String.format("%s@%s", message, channel)); + } + } +} diff --git a/redis/pubsub/src/test/java/example/springdata/redis/PubSubVirtualThreadsTests.java b/redis/pubsub/src/test/java/example/springdata/redis/PubSubVirtualThreadsTests.java new file mode 100644 index 00000000..8c4b7d60 --- /dev/null +++ b/redis/pubsub/src/test/java/example/springdata/redis/PubSubVirtualThreadsTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 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 + * + * http://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 example.springdata.redis; + +import static org.assertj.core.api.Assertions.*; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +/** + * Show usage of Redis Pub/Sub operations using Virtual Threads. + * + * @author Mark Paluch + */ +@SpringBootTest(properties = "spring.threads.virtual.enabled=true") +@EnabledOnJre(JRE.JAVA_21) +public class PubSubVirtualThreadsTests { + + @Autowired RedisConnectionFactory connectionFactory; + + @Autowired AsyncTaskExecutor taskExecutor; + + @Autowired StringRedisTemplate redisTemplate; + + @Test + void shouldListenToPubSubEvents() throws Exception { + + BlockingQueue events = new LinkedBlockingDeque<>(); + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.setTaskExecutor(taskExecutor); + container.afterPropertiesSet(); + container.addMessageListener( + (message, pattern) -> events + .add(String.format("%s on Thread %s", new String(message.getBody()), Thread.currentThread())), + ChannelTopic.of("my-channel")); + + container.start(); + + redisTemplate.convertAndSend("my-channel", "Hello, world!"); + + String event = events.poll(5, TimeUnit.SECONDS); + + container.stop(); + container.destroy(); + + assertThat(event).isNotNull().contains("Hello, world!").contains("VirtualThread"); + } +} diff --git a/redis/pubsub/src/test/java/example/springdata/redis/RedisTestConfiguration.java b/redis/pubsub/src/test/java/example/springdata/redis/RedisTestConfiguration.java new file mode 100644 index 00000000..cec2d756 --- /dev/null +++ b/redis/pubsub/src/test/java/example/springdata/redis/RedisTestConfiguration.java @@ -0,0 +1,24 @@ +/* + * Copyright 2014-2021 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 example.springdata.redis; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Mark Paluch + */ +@SpringBootApplication +public class RedisTestConfiguration {} diff --git a/redis/pubsub/src/test/resources/application.properties b/redis/pubsub/src/test/resources/application.properties new file mode 100644 index 00000000..bac6605c --- /dev/null +++ b/redis/pubsub/src/test/resources/application.properties @@ -0,0 +1 @@ +logging.level.root=WARN