Table of contents
-
- 1. GraalVM
-
- 1.1 Generate local executable application
- 1.2 Generate docker image
- 2. Support virtual threads
-
- 2.1 Stress test when virtual thread is not enabled
- 2.2 Stress test when virtual thread is enabled
- 3. HTTP Interface
1. GraalVM
- Using GraalVM to compile SpringBoot applications into local executable image files can significantly improve startup speed, peak performance, and reduce memory applications.
- Traditional applications are compiled into bytecode, then interpreted by the JVM and finally compiled into machine code for running. Spring Native is compiled into machine code in advance through AOT, and is directly statically compiled into an executable file at runtime, such as windows. .exe file on, does not rely on JVM. GraalVM's just-in-time and AOT compilers can significantly improve application performance.
- AOT: Ahead-of-Time Compilation, precompilation was proposed as an experimental feature in Java9. Compile Java classes into native code, reducing startup time and memory usage of Java applications.
1.1 Generate local executable application
- Plug-ins used by maven
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
- Generate local application commands
mvn -Pnative native:compile
- gradle requires the following plugins
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
id 'org.graalvm.buildtools.native' version '0.9.28'
}
- Generate local application commands
gradle nativeCompile
- Or click
nativeCompile
under build in IDEA
- Finally, a local executable file will be generated under
build/native/nativeCompile
, which can be run by double-clicking it.
- Startup speed is only milliseconds
- Reference https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.developing-your-first-application.native-build-tools.maven
1.2 Generate docker image
- Commands used by maven
mvn -Pnative spring-boot:build-image
- Commands used by gradle
gradle bootBuildImage
- Or click
bootBuildImage
under build in IDEA - Because other dependencies will be pulled through docker, you need to start docker at this time. Through the docker client, you will find that there are 3 more images. The created time displayed later is wrong, so you don’t need to care.
paketobuildpacks/run-jammy-tiny
paketobuildpacks/builder-jammy-tiny
# tptpbfysrtIt will be different every time
pack.local/builder/tptpbfysrt
- Then it will automatically use
pack.local/builder/...
to start a container to create the image, which will download a lot of things. If the execution fails, just click start again to run it. The downloaded files will not be re-downloaded. Don't Re-execute thegradle bootBuildImage
command, otherwise the process of downloading dependencies will be executed from scratch, which is very time-consuming.
- After successful execution, a newly generated image file will appear in images.
- Start using the following command
# myproject:0.0.1-SNAPSHOTChange to your own project name and version
docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT
- Reference https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.developing-your-first-application.buildpacks.maven
2. Support virtual threads
- Spring Boot3.2 starts virtual threads, you need to use JDK21 and set the following properties
spring.threads.virtual.enabled=true
- When virtual threads are enabled, Tomcat and Jetty will use virtual threads to handle requests. This means that application code that handles network requests (such as methods in a Controller) will run on a virtual thread. The following is the code for Tomcat to enable virtual threads, in
tomcat-embed-core-10.1.19.jar!\org\apache\tomcat\util\net\AbstractEndpoint.class
public void createExecutor() {
internalExecutor = true;
if (getUseVirtualThreads()) {
executor = new VirtualThreadExecutor(getName() + "-virt-");
} else {
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);
}
}
- Create a controller
- Start the program, call the hello interface, and view the log printing
2024-02-24T16:41:15.778+08:00 INFO 14252 --- [omcat-handler-0] com.example.demo.Controller : [Controller][hello] VirtualThread[#46,tomcat-handler-0]/runnable@ForkJoinPool-1-worker-1
- It can be seen from
VirtualThread[#46,tomcat-handler-0]
that a virtual thread is used - Close the virtual thread, call the interface again, and view the log printout
2024-02-24T16:43:14.715+08:00 INFO 15844 --- [nio-8080-exec-1] com.example.demo.Controller : [Controller][hello] Thread[#39,http-nio-8080-exec-1,5,main]
- Through
Thread[#39,http-nio-8080-exec-1,5,main]
we can see that the platform thread is used
2.1 Stress test when virtual thread is not enabled
- You need to install the pressure measurement tool plow in advance
# 100000requests,500concurrent
plow http://localhost:8080/hello -c 500 -n 100000
- An average of 3889.768 requests are processed per second, and the delay is mainly concentrated around 115 to 140ms.
2.2 Stress test when virtual thread is enabled
- An average of 9101.021 requests are processed per second, and the delay is mainly concentrated at 51.488ms.
3. HTTP Interface
-
Define the HTTP service as an interface with the @HttpExchange method and pass such an interface to the HttpServiceProxyFactory to create a proxy that performs requests through an HTTP client (such as RestClient or WebClient). Similar to Feign, it uses a declarative approach to access HTTP services. You can refer to https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface
-
Create an http interface
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
@HttpExchange("/api")
public interface HelloClient {
@GetExchange("/hello")
String hello(@RequestParam String msg);
}
- Inject a declarative client. By injecting the client with the target interface baseUrl into the HttpServiceProxyFactory, you can use RestClient, WebClient, or RestTemplate. RestClient is used here.
import com.example.springboot3.client.HelloClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
@Configuration
public class AppConfig {
@Bean
public HelloClient toClient() {
RestClient restClient = RestClient.builder().baseUrl("http://localhost:80/").build();
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(restClient)).build();
return httpServiceProxyFactory.createClient(HelloClient.class);
}
}
- Controller
import com.example.springboot3.client.HelloClient;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Resource
private HelloClient client;
@GetMapping("hello")
public String hello(@RequestParam String msg) {
return client.hello(msg);
}
}
- More specific modifications can be found at https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes