Hi, Spring fans!
Before we get started, do something for me quickly. If you haven’t already, go [download SKDMAN](https://sdkman.io/).
Then run:
sdk install java 21-graalce && sdk default java 21-graalce
There you have it. You now have Java 21 and graalvm supporting Java 21 on your machine, ready to go. Java 21 is, in my estimation, the most critical release of Java, perhaps ever, in that it implies a whole new world of opportunities for people using Java. It brings a slew of nice APIs and additions, like pattern matching, culminating years of features slowly and steadily adding to the platform. But the most prominent feature, by far, is the new support for violent threads project Loom). Virtual threads and graalvm native images mean that today, you can write code that delivers performance and scalability on par with the likes of C, Rust, or Go while retaining the robust and familiar ecosystem of the JVM.
There’s never been a better time to be a JVM developer.
I just posted a video exploring new features and opportunities in Java 21 and GraalVM.
<iframe width="560" height="315" src="https://www.youtube.com/embed/8VJ_dSdV3pY?si=D7ecMMusRby85GC4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
In this blog, I hope to visit the same things, with some added data that lends itself to text.
First things first. If it wasn’t apparent from the above installation, I recommend installing graalvm first. It is OpenJDK, so you get all the OpenJDK bits, but it can also create GraalVM native images.
Why graalvm native images? well, it’s fast and super resource efficient. traditionally, that truism always had a rebuttal: "Yah, well, the JIT is still faster in plain old Java," to which I’d counter, "Yah, well, you can much more readily scale up new instances at a fraction of the footprint to account for whatever lost throughput you had, and still be ahead in terms of resource consumption spend!" Which was true.
But now we don’t even have to have that nuanced discussion. Per the graalvm release blog, Oracle’s GraalVM native image with profile-guided optimization performance is now consistently ahead of JIT on benchmarks where it was only in some places ahead. Oracle GraalVM isn’t necessarily the same as the open-source GraalVM distribution, but the point is that the higher echelons of performance now exceed the JRE JIT.
This excellent post from 10MinuteMail looks at how they used GraalVM and Spring Boot 3 to reduce their startup time from ~30 seconds down to about 3 milliseconds, and memory usage from 6.6GB down to 1GB, with the same throughput and CPU utilization. Amazing.
So many features in Java 21 build upon features first introduced in Java 17 (and, in some cases, earlier than that!). Let’s review some of those features before examining their final manifestation in Java 21.
Do you know that Java supports multiline strings? It’s one of my favorite features, and it has made using JSON, JDBC, JPA QL, etc., more pleasant than ever:
link:src/test/java/bootiful/java21/MultilineStringTest.java[role=include]
Nothing too surprising. Easy to understand. Triple quotes start and stop the multiline string. You can strip the leading, trailing, and indent space, too.
Records are one of my favorite features of Java! They’re freaking fantastic! Do you have a class whose identity is equivalent to the fields in the class? Sure you do. Think about your basic entities, your events, your DTOs, etc. Whenever you’ve used Lombok’s @Data
, you could just as easily use a record
. They have analogs in Kotlin (data class
) and Scala (case class
), so plenty of people know about them, too. It’s great that they’re finally in Java.
link:src/test/java/bootiful/java21/RecordTest.java[role=include]
This concise syntax results in a class with a constructor, associated storage in the course, getters (e.g.: event.name()
), a valid equals
, and a good toString()
implementation.
I rarely used the existing switch
statement because it was clunky, and usually, there were other patterns, like the visitor pattern, that got me most of the benefits. Now there’s a new switch
that is an expression, not a statement, so I can assign the results of the switch
to a variable, or return it.
Here’s an example of reworking a classic switch to use the new enhanced switch
:
link:src/test/java/bootiful/java21/EnhancedSwitchTest.java[role=include]
-
This is a classic implementation with the older, clunkier switch statement
-
This is the new switch expression
The new instanceof
test allows us to avoid the clunky check-and-cast of yore, which looks like this:
var animal = (Object) new Dog ();
if (animal instanceof Dog ){
var fido = (Dog) animal;
fido.bark();
}
And replace it with:
var animal = (Object) new Dog ();
if (animal instanceof Dog fido ){
fido.bark();
}
The smart instanceof
automatically assigns a downcast variable for use in the scope of the test. There’s no need to specify the class Dog
twice in the same block. The smart instanceof
operator usage is the first real taste of pattern matching in the Java platform. The idea behind pattern matching is simple: match types and extract data from those types.
Technically sealed types are part of Java 17, too, but they don’t buy you much yet. The basic idea is that, in the olden times, the only way to limit the extensibility of a type was through visibility modifiers (public
, private
, etc.). In the sealed
keyword, you can explicitly permit which classes may subclass another class. This is a fantastic leap forward because it gives the compiler visibility into which type might extend a given type, which allows it to do optimizations and to help us at compile time to understand whether all possible cases say in an enhanced switch
expression - have been covered. Let’s take a look at it in action.
link:src/test/java/bootiful/java21/SealedTypesTest.java[role=include]
-
We have an explicitly sealed interactive that only permits three types. The enhanced
switch
expression below will fail if we add a new class. -
Classes that implement that sealed interface must either be declared
sealed
and thus declare which classes it permits as subclasses, or it must be declared asfinal
. -
We could use the new
instance of
check to make shorter work of working with each possible type, but we get no compiler help here. -
unless we use the enhahced
switch
with pattern matching, as we do here.
Notice how clunky the classic version is. Ugh. I am so glad to be done with that. Another nice thing is that the switch
expression will now tell us whether we’ve covered all possible cases, like with the enum
. Thanks, compiler!
With all these things combined, we’re starting to wade comfortably into Java 21 land. From here on down, we’ll look at features that have come since java 17.
The enhanced switch
expression and pattern matching are remarkable, and it makes me wonder how using Akka so many years ago would’ve felt using Java with this excellent new syntax. Pattern matching has an even nicer interaction when taken together with records because records - as discussed earlier - are the resumes of their components, and the compiler knows this. So, it can hoist those components into new variables, too. You can also use this pattern-matching syntax in if
checks.
link:src/test/java/bootiful/java21/RecordsTest.java[role=include]
-
We have a special case where if we get a particular event, we want to shut down, not produce a
String
, so we’ll use the new pattern matching support with anif
statement. -
Here, we’re matching not just the type but extracting out the
User user
of theUserDeletedEvent
. -
Here, we’re matching not just the type, but we’re extracting out the
String name
of theUserCreatedEvent
.
All these things started to take root in earlier versions of Java but culminate here in Java 21 in what you might call data-oriented programming. It is not a replacement for object-oriented programming but a compliment to it. You can use things like pattern matching, enhanced switch, and the instanceof
operator to give your code a new polymorphism without exposing the dispatch point in your public API.
There are so many other features new in Java 21. There’s a bunch of small but nice things and, of course, project Loom or virtual threads. (Virtual threads alone are worth the price of admission!) Let’s dive right into some of these fantastic features.
In AI and algorithms, efficient mathematics is more important than ever. The new JDK has some nice improvements here, including parallel multiplication for BigIntegers and various overloads for divisions that throw an exception if there’s an overflow. Not just if there’s a divide-by-zero error.
link:src/test/java/bootiful/java21/MathematicsTest.java[role=include]
-
This first operation is one of several overloads that make division safer and more predictable
-
There’s new support for parallelized multiplication with
BigInteger
instances. Remember that this is only really useful if theBigInteger
has thousands of bits…
If you’re doing asynchronous programming (yes, that’s still a thing, even with Project Loom), then you’ll be pleased to know our old friend Future<T>
now makes available a state
instance that you can switch
on to see the status of the ongoing asynchronous operation.
link:src/test/java/bootiful/java21/FutureTest.java[role=include]
-
This returns a
state
object that lets us enumerate the submittedThread
states. It pairs nicely with the enhancedswitch
feature.
The HTTP client API is where you might want to wrap async operations in the future and use Project Loom. The HTTP Client API has existed since Java 11, which is now a full ten releases in the distant past! But, now it has this spiffy new auto-closeable API.
link:src/test/java/bootiful/java21/HttpTest.java[role=include]
-
We want to close the
HttpClient
automatically. Note that if you do launch any threads and send HTTP requests in them, you should not use auto-closeable unless care is taken only to let it reach the end of the scope after all the threads have finished executing.
I used HttpResponse.BodyHandlers.ofString
to get a String
response in that example. You can get all sorts of objects back, not just String
. But String
results are nice because they are a great segue to another fantastic feature in Java
21: the new support for working with String
instances. This class shows two of my favorites: a repeat
operation for StringBuilder
and a way to detect the presence of Emojis in a String
.
link:src/test/java/bootiful/java21/StringsTest.java[role=include]
-
This first example demonstrates using the
StringBuilder
to repeat aString
(can we all collectively get rid of our variousStringUtils
, yet?) -
This second example demonstrates detecting an emoji in a
String
.
Small quality-of-life improvements, I agree, but nice nonetheless.
You’ll need an ordered collection to sort those String
instance. Java offers a few of them, LinkedHashMap
, List
, etc., but they didn’t have a common ancestor. Now they do; welcome, SequencedCollection
! In this example, we work with a simple ArrayList<String>
and use the fancy new factory methods for things like a LinkedHashSet
. This new factory method does some math internally to guarantee that it won’t have to rebalance (and thus slowly rehash everything) before you’ve added as many elements as you’ve stipulated in the constructor.
link:src/test/java/bootiful/java21/SequencedCollectionTest.java[role=include]
-
This overrides the first-place element
-
This returns the first-place element
There are similar methods for getLast
and addLast
, and there’s even support for reversing a collection, with the reverse
method.
Finally, we get to Loom. You’ve no doubt heard a lot about Loom. The basic idea is to make scalable the code you wrote in college! What do I mean by that? Let’s write a simple network service that prints out whatever is given to us. We must read from one InputStream
and accrue everything into a new buffer (a ByteArrayOutputStream
). Then, when the request finishes, we’ll print the contents o the ByteArrayOutputStream
. The problem is we might get a lot of data simultaneously. So, we will use threads to handle more than one request at the same time.
Here’s the code:
link:src/test/java/bootiful/java21/NetworkServiceApplication.java[role=include]
It’s pretty trivial Networking-101 stuff. Create a ServerSocket
, and wait for new clients (represented by instances of Socket
) to appear. As each one arrives, hand it off to a thread from a threadpool. Each Thread reads data from the client Socket
instance’s InputStream
references. Clients might disconnect, experience latency, or have a large chunk of data to send, all of which is a problem because there are only so many threads available and we must not waste our precious little time on them.
We’re using threads to avoid a pileup of requests we can’t handle fast enough. But here again we’re defeated because, before Java 21, threads were expensive! They cost about two megabytes of RAM for each Thread
. And so we pool them in a thread pool and reuse them. But even there, if we have too many requests, we’ll end up in a situation where none of the threads in the pool are available. They’re all stuck waiting on some request or another to finish. Well, sort of. Many are just sitting there, waiting for the next byte
from the InputStream
, but they’re unavailable for use.
The threads are blocked. They’re probably waiting for data from the client. The unfortunate state of things is that the server, waiting on that data, has no choice but to sit there, parked on a thread, not allowing anybody else to use it.
Until now, that is. Java 21 introduces a new sort of thread, a virtual thread. Now, we can create millions of threads for the heap. It’s easy. But fundamentally, the facts on the ground are that the actual threads, on which virtual threads execute, are expensive. So, how can the JRE let us have millions of threads for actual work? It has a vastly improved runtime that now notices when we block and suspend execution on the Thread until the thing we’re waiting for arrives. Then, it quietly puts us back on another thread. The actual threads act as carriers for virtual threads, allowing us to start millions of threads.
Java 21 has improvements in all the places that historically block threads, like blocking IO with InputStream
and OutputStream
, and Thread.sleep
, so now they correctly signal to the runtime that it is ok to reclaim the Thread and repurpose it for other virtual threads, allowing work to progress even when a virtual thread is 'blocked'. You can see that in this example, which I shamelessly stole from José Paumard, one of the Java Developer Advocates at Oracle whose work I love.
link:src/test/java/bootiful/java21/LoomTest.java[role=include]
-
We’re using a new factory method in Java 21 to create a virtual thread. There’s an alternative factory method to create a
factory
method.
This example launches a lot of threads, to the point where it creates contention and will need to share the operating system carrier threads. Then it causes the threads to sleep
. Sleeping would typically block, but not in virtual threads.
We’ll sample one of the threads (the first one launched) before and after each sleep to note the name of the carrier thread on which our virtual thread is running before and after each sleep. Notice that they’ve changed! The runtime has moved our virtual thread on and off different carrier threads with no changes to our code! That’s the magic of Project Loom. Virtually (pardon the pun) no code changes, and much improved scalability (thread reuse), on par with what you might otherwise only be able to get with something like reactive programming.
What about our network service? We do require one change. But it’s a basic one. Swap out the thread pool, like this:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
...
}
Everything else remains the same, and now we get unparalleled scale! Spring Boot applications typically have a lot of Executor
instances in play for all sorts of things, like integration, messaging, web services, etc. If you’re using Spring Boot 3.2, coming out in November 2023, and Java 21, then you can use this new property, and Spring Boot will automatically plug in virtual thread pools for you! Neat.
spring.threads.virtual.enabled=true
Java 21 is a huge deal. It offers syntax on par with many more modern languages and scalability that’s as good or better than many modern languages without complicating the code with things like async/await, reactive programming, etc.
If you want a native image, there is also the GraalVM project, which provides an ahead-of-time (AOT) compiler for Java 21. You can GraalVM to compile your highly scalable Boot applications into GraalVM native images that start in no time and take a tiny fraction of the RAM they took on the JVM. These applications also benefit from the beauty of Project Loom, blessing them with the unparalleled scale.
./gradlew nativeCompile
Nice! We’ve now got a small binary that starts up in a small fraction of time, takes a tiny fraction of the RAM, and scales as well as the most scalable runtimes. Congrats! You’re a Java developer, and there’s never been a better time to be a Java developer!