Skip to content

Commit 19d77ae

Browse files
committed
Initial commit
1 parent c3fa0c3 commit 19d77ae

14 files changed

+376
-2
lines changed

Diff for: .gitignore

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Compiled class file
2+
*.class
3+
4+
# Log file
5+
*.log
6+
7+
# BlueJ files
8+
*.ctxt
9+
10+
# Mobile Tools for Java (J2ME)
11+
.mtj.tmp/
12+
13+
# Package Files #
14+
*.jar
15+
*.war
16+
*.nar
17+
*.ear
18+
*.zip
19+
*.tar.gz
20+
*.rar
21+
22+
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23+
hs_err_pid*
24+
25+
# Eclipse
26+
.classpath
27+
.project
28+
.settings/
29+
30+
# Intellij
31+
.idea/
32+
*.iml
33+
*.iws
34+
35+
# Mac
36+
.DS_Store
37+
38+
# Maven
39+
log/
40+
target/

Diff for: README.md

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,26 @@
1-
# virtual-threads-spring
2-
Spring demo application to compare controllers using CompletableFuture vs. virtual threads
1+
# Virtual Threads Spring Demo
2+
3+
Spring demo application with two controllers:
4+
* `ControllerEvolution1_Sequential`: sequential code on platform threads or virtual threads
5+
* `ControllerEvolution2_CompletableFuture`: asynchronous code using CompletableFuture
6+
7+
Start the application in dev mode using:
8+
9+
```shell
10+
mvn spring-boot:run
11+
```
12+
13+
Then query the endpoints using:
14+
15+
```shell
16+
curl localhost:8080/stage1-seq/product/1
17+
curl localhost:8080/stage2-cf/product/1
18+
```
19+
20+
To switch to virtual threads, uncomment the two bean definitions in `SpringVirtualThreadTestApplication.java`, then access this endpoint again:
21+
22+
```shell
23+
curl localhost:8080/stage1-seq/product/1
24+
```
25+
26+
Within IntelliJ, you can also open the `endpoints-test.http` file to run the HTTP requests.

Diff for: endpoints-test.http

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Stage 1: Sequential code using either platform threads or virtual threads
2+
GET localhost:8080/stage1-seq/product/1
3+
4+
### Stage 2: Asynchronous code using CompletableFuture
5+
GET localhost:8080/stage2-cf/product/1

Diff for: pom.xml

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.springframework.boot</groupId>
8+
<artifactId>spring-boot-starter-parent</artifactId>
9+
<version>3.1.3</version>
10+
<relativePath/>
11+
</parent>
12+
13+
<groupId>eu.happycoders</groupId>
14+
<artifactId>java21-virtual-thread-spring</artifactId>
15+
<version>1.0.0-SNAPSHOT</version>
16+
17+
<properties>
18+
<java.version>21</java.version>
19+
</properties>
20+
21+
<dependencies>
22+
<dependency>
23+
<groupId>org.springframework.boot</groupId>
24+
<artifactId>spring-boot-starter-web</artifactId>
25+
</dependency>
26+
</dependencies>
27+
28+
<build>
29+
<plugins>
30+
<plugin>
31+
<groupId>org.springframework.boot</groupId>
32+
<artifactId>spring-boot-maven-plugin</artifactId>
33+
</plugin>
34+
35+
<plugin>
36+
<groupId>com.diffplug.spotless</groupId>
37+
<artifactId>spotless-maven-plugin</artifactId>
38+
<version>2.39.0</version>
39+
<configuration>
40+
<java>
41+
<googleJavaFormat>
42+
<!-- Latest version can be found here: https://github.com/google/google-java-format/releases -->
43+
<version>1.17.0</version>
44+
<style>GOOGLE</style>
45+
</googleJavaFormat>
46+
<lineEndings>UNIX</lineEndings>
47+
</java>
48+
</configuration>
49+
</plugin>
50+
</plugins>
51+
</build>
52+
53+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package eu.happycoders.virtualthread;
2+
3+
import java.util.concurrent.Executors;
4+
import org.springframework.boot.SpringApplication;
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
7+
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.core.task.AsyncTaskExecutor;
10+
import org.springframework.core.task.support.TaskExecutorAdapter;
11+
12+
@SpringBootApplication
13+
public class SpringVirtualThreadTestApplication {
14+
15+
public static void main(String[] args) {
16+
SpringApplication.run(SpringVirtualThreadTestApplication.class, args);
17+
}
18+
19+
// Uncomment the following two methods to enable virtual threads
20+
21+
// @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
22+
// public AsyncTaskExecutor asyncTaskExecutor() {
23+
// return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
24+
// }
25+
//
26+
// @Bean
27+
// public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
28+
// return protocolHandler -> {
29+
// protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
30+
// };
31+
// }
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package eu.happycoders.virtualthread.controller;
2+
3+
import static org.springframework.http.HttpStatus.NOT_FOUND;
4+
5+
import eu.happycoders.virtualthread.model.Product;
6+
import eu.happycoders.virtualthread.model.ProductPageResponse;
7+
import eu.happycoders.virtualthread.service.ProductService;
8+
import eu.happycoders.virtualthread.service.SupplierService;
9+
import eu.happycoders.virtualthread.service.WarehouseService;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RestController;
13+
import org.springframework.web.server.ResponseStatusException;
14+
15+
@RestController
16+
public class ControllerEvolution1_Sequential {
17+
18+
private final ProductService productService;
19+
private final SupplierService supplierService;
20+
private final WarehouseService warehouseService;
21+
22+
public ControllerEvolution1_Sequential(
23+
ProductService productService,
24+
SupplierService supplierService,
25+
WarehouseService warehouseService) {
26+
this.productService = productService;
27+
this.supplierService = supplierService;
28+
this.warehouseService = warehouseService;
29+
}
30+
31+
@GetMapping("/stage1-seq/product/{productId}")
32+
public ProductPageResponse getProduct(@PathVariable("productId") String productId) {
33+
Product product =
34+
productService
35+
.getProduct(productId)
36+
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
37+
38+
boolean available = warehouseService.isAvailable(productId);
39+
40+
int shipsInDays =
41+
available ? 0 : supplierService.getDeliveryTime(product.supplier(), productId);
42+
43+
return new ProductPageResponse(product, shipsInDays, Thread.currentThread().toString());
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package eu.happycoders.virtualthread.controller;
2+
3+
import eu.happycoders.virtualthread.model.ProductPageResponse;
4+
import eu.happycoders.virtualthread.service.ProductService;
5+
import eu.happycoders.virtualthread.service.SupplierService;
6+
import eu.happycoders.virtualthread.service.WarehouseService;
7+
import java.util.concurrent.CompletableFuture;
8+
import java.util.concurrent.CompletionStage;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RestController
15+
public class ControllerEvolution2_CompletableFuture {
16+
17+
private final ProductService productService;
18+
private final SupplierService supplierService;
19+
private final WarehouseService warehouseService;
20+
21+
public ControllerEvolution2_CompletableFuture(
22+
ProductService productService,
23+
SupplierService supplierService,
24+
WarehouseService warehouseService) {
25+
this.productService = productService;
26+
this.supplierService = supplierService;
27+
this.warehouseService = warehouseService;
28+
}
29+
30+
@GetMapping("/stage2-cf/product/{productId}")
31+
public CompletionStage<ResponseEntity<ProductPageResponse>> getProduct(
32+
@PathVariable("productId") String productId) {
33+
return productService
34+
.getProductAsync(productId)
35+
.thenCompose(
36+
product -> {
37+
if (product.isEmpty()) {
38+
return CompletableFuture.completedFuture(ResponseEntity.notFound().build());
39+
}
40+
41+
return warehouseService
42+
.isAvailableAsync(productId)
43+
.thenCompose(
44+
available ->
45+
available
46+
? CompletableFuture.completedFuture(0)
47+
: supplierService.getDeliveryTimeAsync(
48+
product.get().supplier(), productId))
49+
.thenApply(
50+
daysUntilShippable ->
51+
ResponseEntity.ok(
52+
new ProductPageResponse(
53+
product.get(),
54+
daysUntilShippable,
55+
Thread.currentThread().toString())));
56+
});
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package eu.happycoders.virtualthread.model;
2+
3+
public record Product(String productId, String name, Supplier supplier) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package eu.happycoders.virtualthread.model;
2+
3+
public record ProductPageResponse(Product product, int daysUntilShippable, String thread) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package eu.happycoders.virtualthread.model;
2+
3+
public record Supplier(String name, String apiUrl) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package eu.happycoders.virtualthread.service;
2+
3+
import static eu.happycoders.virtualthread.service.SleepUtil.sleepApproximately;
4+
5+
import eu.happycoders.virtualthread.model.Product;
6+
import eu.happycoders.virtualthread.model.Supplier;
7+
import java.util.Optional;
8+
import java.util.concurrent.CompletableFuture;
9+
import org.springframework.stereotype.Service;
10+
11+
@Service
12+
public class ProductService {
13+
14+
private static final Supplier SUPPLIER1 = new Supplier("Supplier 1", "supplier1.example.com");
15+
16+
private static final Product PRODUCT1 = new Product("1", "Product 1", SUPPLIER1);
17+
18+
private static final Product PRODUCT2 = new Product("2", "Product 1", SUPPLIER1);
19+
20+
public Optional<Product> getProduct(String productId) {
21+
try {
22+
sleepApproximately(1000);
23+
} catch (InterruptedException e) {
24+
Thread.currentThread().interrupt();
25+
return Optional.empty();
26+
}
27+
28+
if (productId.equals(PRODUCT1.productId())) {
29+
return Optional.of(PRODUCT1);
30+
} else if (productId.equals(PRODUCT2.productId())) {
31+
return Optional.of(PRODUCT2);
32+
} else {
33+
return Optional.empty();
34+
}
35+
}
36+
37+
public CompletableFuture<Optional<Product>> getProductAsync(String productId) {
38+
return CompletableFuture.supplyAsync(() -> getProduct(productId));
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package eu.happycoders.virtualthread.service;
2+
3+
import java.util.concurrent.ThreadLocalRandom;
4+
5+
public final class SleepUtil {
6+
7+
private SleepUtil() {}
8+
9+
public static void sleepApproximately(long millis) throws InterruptedException {
10+
long min = millis * 9 / 10;
11+
long max = millis * 11 / 10;
12+
13+
Thread.sleep(ThreadLocalRandom.current().nextLong(min, max));
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package eu.happycoders.virtualthread.service;
2+
3+
import static eu.happycoders.virtualthread.service.SleepUtil.sleepApproximately;
4+
5+
import eu.happycoders.virtualthread.model.Supplier;
6+
import java.util.concurrent.CompletableFuture;
7+
import java.util.concurrent.ThreadLocalRandom;
8+
import org.springframework.stereotype.Service;
9+
10+
@Service
11+
public class SupplierService {
12+
13+
public int getDeliveryTime(Supplier supplier, String productId) {
14+
try {
15+
sleepApproximately(500);
16+
} catch (InterruptedException e) {
17+
Thread.currentThread().interrupt();
18+
return 0;
19+
}
20+
21+
return ThreadLocalRandom.current().nextInt(1, 5);
22+
}
23+
24+
public CompletableFuture<Integer> getDeliveryTimeAsync(Supplier supplier, String productId) {
25+
return CompletableFuture.supplyAsync(() -> getDeliveryTime(supplier, productId));
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package eu.happycoders.virtualthread.service;
2+
3+
import static eu.happycoders.virtualthread.service.SleepUtil.sleepApproximately;
4+
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.concurrent.ThreadLocalRandom;
7+
import org.springframework.stereotype.Service;
8+
9+
@Service
10+
public class WarehouseService {
11+
12+
public boolean isAvailable(String productId) {
13+
try {
14+
sleepApproximately(500);
15+
} catch (InterruptedException e) {
16+
Thread.currentThread().interrupt();
17+
return false;
18+
}
19+
20+
return ThreadLocalRandom.current().nextBoolean();
21+
}
22+
23+
public CompletableFuture<Boolean> isAvailableAsync(String productId) {
24+
return CompletableFuture.supplyAsync(() -> isAvailable(productId));
25+
}
26+
}

0 commit comments

Comments
 (0)