How To Implement Java Virtual Thread In Spring Boot Application
Welcome to the post about implementation of Java virtual thread in a spring boot application. This post is not about detail of virtual thread. We will just create a spring boot application that use virtual thread in server.
In this sample, we will connect to a external source to get data. That can be database, cache, queue etc. It is going to be Redis in this post.
Let’s start with pom. xml file.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0-M4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.farukbozan.medium</groupId>
<artifactId>virtual-thread-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>virtual-thread-web</name>
<description>virtual-thread-web</description>
<properties>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
<java.version>19</java.version>
<tomcat.version>10.1.0-M17</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<type>jar</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>19</source>
<target>19</target>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-libs-milestone</id>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-libs-milestone</id>
<url>https://repo.spring.io/libs-milestone</url>
</pluginRepository>
</pluginRepositories>
</project>
As you see above, we use some repositories and milestone of parent pom. This is the trick that how we can use virtual thread. Current releases of spring don’t support Java virtual thread.
Now we create an application.yaml file.
server:
tomcat:
threads:
max: 1
spring:
redis:
cluster:
nodes: redis-host-address
timeout: 3000
socket-timeout: 1500
We use a trick to see benefit of virtual threads clearly. Thread count of tomcat server is set to 1. That means our server can accept only one request at same time because of single thread.
Now it’s turn to implement models that holds on Redis.
package com.farukbozan.medium.virtualthreadweb.model;
public class LocationItemCacheModel {
private Long id;
private String name;
private String slug;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
}
package com.farukbozan.medium.virtualthreadweb.model;
public class LocationInfoCacheModel {
private LocationItemCacheModel city;
private LocationItemCacheModel district;
private LocationItemCacheModel locality;
private LocationItemCacheModel town;
public LocationItemCacheModel getCity() {
return city;
}
public void setCity(LocationItemCacheModel city) {
this.city = city;
}
public LocationItemCacheModel getDistrict() {
return district;
}
public void setDistrict(LocationItemCacheModel district) {
this.district = district;
}
public LocationItemCacheModel getLocality() {
return locality;
}
public void setLocality(LocationItemCacheModel locality) {
this.locality = locality;
}
public LocationItemCacheModel getTown() {
return town;
}
public void setTown(LocationItemCacheModel town) {
this.town = town;
}
}
Creating a service that read-write cache. You can change body of service as you wish.
package com.farukbozan.medium.virtualthreadweb.service;
import com.farukbozan.medium.virtualthreadweb.model.LocationInfoCacheModel;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class LocationCacheAdapter {
@Cacheable(cacheNames = "cache-name", key = "{#townId}", unless = "#result = null")
public Optional<LocationInfoCacheModel> getLocationInfo(Long townId) {
return Optional.ofNullable(null);
}
}
A controller class to trigger service and Redis cache.
package com.farukbozan.medium.virtualthreadweb.controller;
import com.farukbozan.medium.virtualthreadweb.model.LocationInfoCacheModel;
import com.farukbozan.medium.virtualthreadweb.service.LocationCacheAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
import java.util.logging.Logger;
@RestController
@RequestMapping("/virtual-thread")
public class VirtualThreadController {
@Autowired
private LocationCacheAdapter locationCacheAdapter;
@GetMapping
public Optional<LocationInfoCacheModel> getLocationInfo() throws InterruptedException {
var locationInfo = locationCacheAdapter.getLocationInfo(10046L);
Thread.sleep(2000);
Logger.getLogger(VirtualThreadController.class.getName()).info("Cache read");
return locationInfo;
}
}
We use Thread sleep mechanism to simulate Redis connection-read-write delay. 2000 ms is very long but we can see virtual thread benefits more clearly.
Finally configuration classes. First is Redis configuration.
package com.farukbozan.medium.virtualthreadweb.configuration;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Objects;
@Configuration
@EnableCaching
public class RedisConfiguration {
@Value(value = "${spring.redis.cluster.nodes}")
private String clusterNode;
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory(new RedisClusterConfiguration(List.of(clusterNode)));
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(jedisConnectionFactory());
var objectMapper = createObjectMapper();
var jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean("cache-manager")
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate) {
var redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(redisTemplate.getConnectionFactory()));
var redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
redisTemplate.getValueSerializer()));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
private ObjectMapper createObjectMapper() {
var objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
return objectMapper;
}
}
Second is the where all things are changed. If this below configuration is enables, our tomcat server will use virtual thread way. Otherwise classical Java thread way.
package com.farukbozan.medium.virtualthreadweb.configuration;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//@Configuration
public class VirtualThreadConfig {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
// enable async servlet support
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
return new TaskExecutorAdapter(executorService::execute);
}
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
And the last thing we need is a load of server. We can use Apache JMeter to generate load on server. Our server has single thread, so it can not create response until previous request is finished.
When
//@Configuration
changed to
@Configuration
tomcat will accept next request because of virtual thread and Thread.sleep combine.
Finally i want to share a JMeter file sample to create load test.
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
<stringProp name="TestPlan.comments"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">1</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">5</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/virtual-thread"</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
You can create a file with extension of jmx and copy above content, and run it on JMeter. And then check logs timestamp to see the difference. You can change logging in class
VirtualThreadController
It is great. Now we have a Spring boot application that use virtual thread instead of classical thread.
See you on next post.