Two weeks ago we had Spring Framework 6.1.0, while today Spring Boot 3.2.0 hit production, so we’ll devote the entire new edition to news in the Spring ecosystem – because there are quite a few of those, and some of them are particularly important.
Spring has decided to catch up with the rest of the ecosystem and has introduced support for two very important innovations from the high-performance applications point of view: Virtual Threads and the CRAC Project.
Those who are programming in Spring are likely to encounter this text, and they may not yet be familiar with the concept of Virtual Threads as they haven’t been introduced to the ecosystem yet. Hence, a very brief intro to the topic.
Virtual Threads, a feature of JDK 21 added under Project Loom, brings a new dimension to Java’s concurrency approach. Unlike conventional threads that are managed by the operating system and are relatively heavy (by using “heavy”, I mean “generic”), virtual threads are lightweight and managed by the JVM. This feature enables the creation of a large number of threads without the usual overhead associated with traditional ones. Virtual Threads are particularly useful in I/O-intensive applications, as they allow for efficient concurrency handling with reduced memory usage, especially in scenarios involving blocking operations. However, this is just a tip of the iceberg. If you’re interested in learning more about Virtual Threads, the best resource would be The Ultimate Guide to Java Virtual Threads. Here, instead of a single paragraph, you’ll find over an hour’s worth of reading material. The concept of virtual threads is similar to the well-know owl drawing instruction – simple at first glance, but the devil lies in the details.
Spring has been exploring these threads for quite some time, dating back to when they were referred to as Fibers (yes, I’m quite seasoned, I even gave a presentation about them as recently as 2018). However, instead of rewriting everything in framework internals, like a magpie attracted to a sparkler, Spring’s strategy involves a gradual modification of the framework’s concurrency models. This is done to leverage the benefits of virtual threads without causing a major upheaval in the API.
Support in Spring is conditional and relies on two factors: operating the application with JDK 21 and activating the
spring.threads.virtual.enabled configuration option. At this point, both Tomcat and Jetty servers in Spring Boot utilize virtual threads for processing queries. This implies that application code, like controller methods managing network requests, will operate on virtual threads, potentially enhancing application performance. However, I recommend verifying this more empirically, as each application might have slightly different bottlenecks.
The story doesn’t end there, though: the incorporation of virtual threads into Spring comes with some additional twists.
VirtualThreadTaskExecutor has been created, and when virtual threads are activated, both
SimpleAsyncTaskScheduler use them by default. This change has a lot of side effects, including changes in behaviour of the
@EnableAsync method, asynchronous request handling in Spring MVC, and support for blocking execution in Spring WebFlux. It also impacts the performance of individual integrations, like listeners for RabbitMQ or Kafka, Spring Data Redis, and Apache Pulsar (which we’ll delve into shortly). I must confess, I wish there was a bit more granularity and the option to decide where we really want to employ Virtual Threads – it would simplify the migration of larger projects. Maybe in some future versions.
So far, there hasn’t been a detailed analysis from Spring itself regarding the effect of Virtual Threads on web applications. However, I suggest checking out the outstanding series from Quarkus, a Spring’s competitor that has already delved into this subject quite deep. The implications and best practices should be comparable.
Before describing the second significant topic, the CRaC project, it’s necessary to first explain the issue known as the Cold Start problem.
You may already be aware that Java applications typically require some warmup time before they can begin processing traffic in the performant way. This is a characteristic of both traditional server applications, which need a Just-in-Time (JiT) compilation process with dynamic profiling to reach optimal performance, and Serverless applications, which simply (in corner cases) needs to set up whole environment from scratch to handle a single request.
Luckily, there are multiple solutions that mitigate this “cold start” issue – for instance, GraalVM enables us to generate “native” versions of Java applications. Additionally, techniques like Fast Startup or AppCDS can be utilized to cache separate JVM runtime segments, preventing initializing them from scratch at startup. However, what if we could consistently reuse the pre-heated, pre-compiled application code? Or even better – the entire memory of the process Java process, already loaded with data, e.g. for ML models? To accomplish this, we should consider a mechanism known as CRIU.
Checkpoint/Restore in Userspace (CRIU) is a feature of Linux that enables a running application process to be ‘dumped’ to disk. The next instance can then be initiated from the point where the previous snapshot was captured, thereby decreasing the start-up time.
Do I have any retro gamers among my readers? If so, the process is akin to the Save State functionality in emulators. Techniques like AppCDS are similar to traditional saving – we choose the parts that will enable us to restore the application state later, and simply save them. Save State doesn’t mess with such subtlety – as computers have advanced and we have more storage space, we just dump the entire state of memory to disk (which in the case of old consoles can be a staggering 1Mb, for example) and then replay it exactly as it was when required.
CRIU presents its own challenges, both in terms of security and convenience, as it’s a generic operating system function that operates outside the JVM. Each application has unique characteristics and, depending on whether it’s an application server, a web application, or a batch job, the timing for writing to disk varies. This can be hard to understand without the context of the virtual machine. As stated by OpenLiberty in their article, Faster start-up for Java applications on Open Liberty with CRIU:
It would be useful to have an API so that the application can specify when it would like a snapshot to be taken; this would be a valuable addition to the Java specification.
OpenLiberty has subsequently created its own system utilizing CRIU.
Spring has chosen a competitive solution – it’s about time to make the switch to CRaC.
Project CRaC (Checkpoint/Restore in Application Continuation) is a tooling solution that utilizes the checkpoint/restore mechanism in Java applications. This allows the state of the running JVM (checkpoint) to be preserved and later restored, thereby enabling applications to operate more swiftly by skipping the initial loading and warm-up stages. Compared to the ‘bare’ CRIU, it incorporates suitable hookups and improvements to facilitate the use of the entire system with the JVM.
To utilize the integration of the Spring Framework with Project CRaC in conjunction with Virtual Threads, certain prerequisites need to be fulfilled. These include a JVM with checkpoint/restore functionality enabled, which is currently only supported on Linux by Azul or Liberica. However, only the latter’s version 21 allows concurrent use of Virtual Threads. Additionally, the
org.crac:crac library (version 1.4.0 or higher) must be attached to the classpath, and specific command-line parameters such as
-XX:CRaCCheckpointTo=PATH need to be specified. The checkpoint process generates files that encapsulate the complete state of JVM memory, potentially including sensitive data. This necessitates a thorough evaluation of security implications. It’s also worth noting that this process impacts the randomness of
java.util.Random, making it ‘slightly’ less random as all processes are initiated from the same seed.
Integration with Spring is quite comprehensive, as it seamlessly integrates into an application’s natural lifecycle. The checkpoint can be triggered using the
jcmd application.jar JDK.checkpoint command. This command effectively halts all beans running by Spring, allowing them to close resources. Once restored, these same beans are restarted, reopening resources as needed. Beyond manually triggering this, a significant feature of this integration is the ability to set up the automatic initiation of checkpoint/restore upon application start-up. By setting the Java system property
-Dspring.context.checkpoint=onRefresh, a checkpoint is automatically created at startup at the
LifecycleProcessor.onRefresh stage. In practical terms, this means that all (non-lazy) singletons are already instantiated, the
InitializingBean.afterPropertiesSet callbacks have been executed, but the
Lifecycle.start methods have not yet been run, and the
ContextRefreshedEvent has not yet been sent out.
What more can be found in the new edition?
Spring Framework 6.1
Spring Framework 6.1 is widely compatible with JDK 21. This new version has introduced enhancements within the ‘application container’ itself, especially regarding lifecycle management features. It now includes features like the capability to pause and resume tasks, and improved control over task closure in
ThreadPoolTaskScheduler. The latest version also offers better observability for
@Scheduled methods and improved ‘factories’ for creating validators. It also provides built-in method validation support for controller method parameters in both Spring MVC and WebFlux. This removes the requirement for
@Validated annotations at the controller class level. Speaking of annotations,
@PropertySource now extends support for symbols in SpEL (Spring Expression Language) expressions.
Also introducing RestClient, a new synchronous HTTP client similar to WebClient, but preconfigured and tailored for REST queries. The changes in the context of database access are also interesting – as an interesting aspect of the release is the introduction of JdbcClient, a unified facade for JDBC. Furthermore, support for R2DBC has been added. The list of changes to the data layer is rounded off by improvements to the application life cycle in terms of transactivity.
A list of all the changes can be found here.
Spring Boot 3.2.0
In addition to the new features of Spring Framework 6.1, Spring Boot 3.2.0 introduces several new integrations and features. Notably, there is extended support for Apache Pulsar. The already mentioned
RestClient interface from Spring Framework 6.1 provides developers with a functional blocking HTTP API. There’s also support for
JdbcClient. A significant addition is the automatic Correlation Id logging when using Micrometer, which could potentially save many developers from the unpleasant situation of lacking sufficient information during a crash. Auto-configuration support is also provided for Micrometer annotations:
Version 3.2.0 enhances the construction of Docker images following the Cloud-Native Buildpacks standard from the Cloud Native Computing Foundation. By default, tasks for building Docker images now utilize the configuration from the host.
There are numerous minor modifications, you can find a complete list of them here.
The more interesting supporting project
Spring for Apache Pulsar
Apache Pulsar competes with Kafka, utilizing an architecture that separates storage space from computing power. This provides the ability to independently scale these components, achieving varying degrees of resource isolation. This is in contrast to Kafka’s unified architecture, where computing power and storage are closely linked, necessitating the simultaneous scaling of resources.
Spring for Apache Pulsar 1.0.0 is a tool that aids in the creation of Apache Pulsar-based applications. The primary dependency is the
spring-pulsar-spring-boot-starter module, which streamlines the configuration and development of applications. This tool automatically configures key components like the
PulsarClient, utilized by both message senders and receivers. It also facilitates the easy production and consumption of messages via the
PulsarTemplate and the simple subscription and response to messages through the
@PulsarListener annotations. Moreover, this tool provides support for TLS and various authentication methods.
I’ll also mention Spring Integration 6.2 briefly. It contains a lot as usual, but nothing that will significantly alter anyone’s life. However, if you utilize Kafka, MongoDB or Debezium – it’s worth checking out.
Spring Security 6.2 and Spring Session 3.2.0
Significant modifications in Spring Security 6.2 encompass the automatic addition of
.cors() when a
CorsConfigurationSource bean is present, and the ease of creating various configurations: a fresh
AbstractConfiguredSecurityBuilder.with(...) method, and the simplification of the OAuth2 client component configuration. Moreover, support for OIDC Back-channel logout has been introduced, enhancements to
SecurityContext propagation, adjustable
RedirectStrategy and HTTP Basic request parsing have been made.
The entity, Spring Session 3.2.0, which I’ve always associated closely with security, emphasizes on two main enhancements. The initial one is the introduction of
SessionIdGenerator, which allows for the creation of custom session IDs. The latter is an enhancement to the capability of securely deserialising Redis sessions.
The Spring Authorization Server 1.2 has been launched as well. However, most of the updates in this version are merely patchnotes, so it’s not as significant.
There are additional releases, but I’ve concentrated on those that I find most intriguing. My preferred method of staying updated on new version is the Spring Calendar, which I also suggest for you.