From 5ee72054143071e0ec8a60f2ff048056b760f19f Mon Sep 17 00:00:00 2001 From: Josh Long Date: Tue, 19 Mar 2024 15:54:26 -0400 Subject: [PATCH] up --- README.md | 229 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 185 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 4216c13..07be19b 100644 --- a/README.md +++ b/README.md @@ -273,11 +273,45 @@ class ManualFfi implements LanguageDemonstrationRunner { } ``` -Nice! Run this and you'll see it prints out `hello, Panama!`. You might be wondering why I didn't pick something more interesting as an example. It turns out that there's precious little that you can both take for granted across all operating systems _and_ perceive as having done something on your computer. IO seemed to be about all I could think of, and console IO is even easier to follow. +Here's the definition for the `SymbolLookup` that I put together. It is a sort of composite, trying one `SymbolLookup`, +and then another if the first should fail. -But what about GraalVM native images? It doesn't support _every_ thing you might want to do. And, at least for the moment, it doesn't run on Apple Silicon. I developed this example and set [up a GitHub Action](https://raw.githubusercontent.com/spring-tips/java22/main/.github/workflows/maven.yml) to see the results in an x86 Linux environment. Otherwise, I was pleased with the result. Remember, GraalVM wants to know about some of the dynamic things you're going to do at runtime. So, you need to tell +```java - it. For most things, you need to write a `.json` configuration file (or let Spring's AOT engine write them for you). Here, since this is so new, you have to write a GraalVM `Feature`, which in turn has callback methods that get invoked during GraalVM's native compilation. We just need to tell GraalVM the signature of the native function we'll be invoking via Panama at runtime. +@Bean +SymbolLookup symbolLookup() { + var loaderLookup = SymbolLookup.loaderLookup(); + var stdlibLookup = Linker.nativeLinker().defaultLookup(); + return name -> loaderLookup.find(name).or(() -> stdlibLookup.find(name)); +} +``` + +Run this, and you'll see it prints out `hello, Panama!`. + +You might be wondering why I didn't pick something more interesting as an example. It turns out that there's precious +little that you can both take for granted across all operating systems _and_ perceive as having done something on your +computer. IO seemed to be about all I could think of, and console IO is even easier to follow. + +But what about GraalVM native images? It doesn't support _every_ thing you might want to do. And, at least for the +moment, it doesn't run on Apple Silicon, only x86 chips. I developed this example and +set [up a GitHub Action](https://raw.githubusercontent.com/spring-tips/java22/main/.github/workflows/maven.yml) to see +the results in an x86 Linux environment. It's a bit of a pity for us Mac developers who are not using Intel chips, but +most of us aren't deploying to Apple devices in production, we're deploying to Linux and x86, so it's not a dealbreaker. + +There are some +other [limitations, too](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ForeignInterface.md). +For example, GraalVM native images only support the first `SymbolLookup`, `loaderLookup`, in our composite. If that one +doesn't work, then neither of them will work. + +GraalVM wants to know about some of the dynamic things you're going to do at runtime, including foreign function +invocation. You need to tell it ahead of time. For most other things about which it needs such information, like +reflection, serialization, resource loading, and more, you need to write a `.json` configuration file (or let Spring's +AOT engine write them for you). This feature is so new that you have to go down a few abstraction levels and write a +GraalVM `Feature` class. A `Feature` has callback methods that get invoked during GraalVM's native compilation +lifecycle. You'll tell +GraalVM the signature, the _shape_, of the native function that we'll eventually invoke at runtime. Here's +the `Feature`. +There's only one line of value. ```java package com.example.demo; @@ -291,6 +325,8 @@ public class DemoFeature implements Feature { @Override public void duringSetup(DuringSetupAccess access) { + // this is the only line that's important. NB: we're sharing + // the PRINTF_FUNCTION_DESCRIPTOR from our ManualFfi bean from earlier. RuntimeForeignAccess.registerForDowncall(PRINTF_FUNCTION_DESCRIPTOR); } @@ -300,27 +336,45 @@ public class DemoFeature implements Feature { And then we need to wire up the feature, telling GraalVM about it, by passing in the `--features` attribute to the GraalVM native image Maven plugin configuration. We also need to unlock the foreign API support and unlock experimental stuff. (I don't know why this is experimental in GraalVM native images when it's not experimental any longer in Java 22 itself). Also, we need to tell GraalVM to allow native access for all unnamed types. So, altogether, here's the final Maven plugin configuration. ```xml - - org.graalvm.buildtools - native-maven-plugin - 0.10.1 - - - --features=com.example.demo.DemoFeature - --enable-native-access=ALL-UNNAMED - -H:+ForeignAPISupport - -H:+UnlockExperimentalVMOptions - --enable-preview - - - -``` -This is an awesome result. I compiled the code in this example into a GraalVM native image running on GitHub Actions runners and then executed it. The application, which - I remind you - has the Spring JDBC support and everything on the classpath - executes in 0.031 seconds (31 milliseconds), takes tens of megabytes of RAM, and invokes the native C code, from the GraalVM native image! I'm so happy, y'all. I've waited for this day for so long. - -But this does feel a little low-level. At the end of the day, you are using a Java API to programmatically create and maintain structures in native code. It's sort of like using SQL from JDBC. JDBC lets you manipulate SQL database records in Java, but you're not writing SQL in Java and compiling it in Java and executing it in SQL. There's an abstraction delta; you're sending strings into the SQL engine and then getting records back out as `ResultSet`. The same is true for the low-level API in Panama. + + org.graalvm.buildtools + native-maven-plugin + 0.10.1 + + + --features=com.example.demo.DemoFeature + --enable-native-access=ALL-UNNAMED + -H:+ForeignAPISupport + -H:+UnlockExperimentalVMOptions + --enable-preview + + + +``` -So, they released a tool called `jextract`. You can point it at a C header file, like `stdio.h`, in which the `printf` function is defined, and it'll generate Java code that mimics the call signature of the underlying C code. I didn't use it in this example because the resulting Java code ends up being tied to the underlying platform. I pointed it to `stdio.h` and got a whole bunch of macOS specific definitions. I could hide all of that behind a runtime check for the operating system and then dynamically load a particular implementation, but, eh, this blog's already too long. If you want to see how to run `jextract`, here's the bash script I used that worked for macOS and Linux. YMMV. +This is an awesome result. I compiled the code in this example into a GraalVM native image running on GitHub Actions +runners and then executed it. The application, which - I remind you - has the Spring JDBC support, a complete and +embedded SQL 99 compliant Java database called H2, and everything on the classpath - executes in 0.031 seconds (31 +milliseconds, or 31 thousandths of a second), takes tens of megabytes of RAM, and invokes the native C code, from the +GraalVM native image! + +I'm so happy, y'all. I've waited for this day for so long. + +But this does feel a little low-level. At the end of the day, you are using a Java API to programmatically create and +maintain structures in native code. It's sort of like using SQL from JDBC. JDBC lets you manipulate SQL database records +in Java, but you're not writing SQL in Java and compiling it in Java and executing it in SQL. There's an abstraction +delta; you're sending strings into the SQL engine and then getting records back out as `ResultSet` objects. The same is +true for the low-level API in Panama. It works, but you're not invoking native code, your looking up symbols with +strings and manipulating memory. + +So, they released a separate but related tool called `jextract`. You can point it at a C header file, like `stdio.h`, in +which the `printf` function is defined, and it'll generate Java code that mimics the call signature of the underlying C +code. I didn't use it in this example because the resulting Java code ends up being tied to the underlying platform. I +pointed it to `stdio.h` and got a lot of macOS specific definitions. I could hide all of that behind a runtime check for +the operating system and then dynamically load a particular implementation, but, eh, this blog's already too long. If +you want to see how to run `jextract`, here's the bash script I used that worked for macOS and Linux. Your mileage may +vary. ```bash #!/usr/bin/env bash @@ -356,39 +410,105 @@ export PATH=$PATH:$JEXTRACT_HOME/bin jextract --output src/main/java -t com.example.stdio $STDIO ``` -Just think about it. We have easy foreign function interop, virtual threads giving us amazing scalability, and statically linked, self-contained GraalVM native image binaries. Tell me why you'd start a new project in Go, again? :) +Just think about it. We have easy foreign function interop, virtual threads giving us amazing scalability, and +statically linked, lightning fast, RAM efficient, self-contained GraalVM native image binaries. Tell me why you'd start +a new project in Go, again? :-) ## A Brave New World -Java 22 is an amazing new release. It brings with it a bevy of huge features and quality of life improvements. Remember, it can't always be this good! Nobody can introduce paradigm-changing new features consistently every six months. It's just not possible. So, let's be thankful and enjoy it while we can, shall we? :) The last release, Java 21, was, in my estimation, maybe the single biggest release I've seen since perhaps Java 5, maybe even earlier. It might be the biggest ever! +Java 22 is an amazing new release. It brings with it a bevy of huge features and quality of life improvements. Just +remember, it can't always be this good! Nobody can introduce paradigm-changing new features consistently every six +months. It's just not possible. So, let's be thankful and enjoy it while we can, shall we? :) The last release, Java 21, +was, in my estimation, maybe the single biggest release I've seen since perhaps Java 5, maybe even earlier. It might be +the biggest ever! There are a ton of features there that are well worth your attention, including _data-oriented programming_ and _virtual threads_. I covered this, and a lot more, in a blog I did to support the release six months ago, [_Hello, Java 21_](https://spring.io/blog/2023/09/20/hello-java-21). -## Virtual Threads (Yes, I Know This Was Released Six Months Ago!) +## Virtual Threads, Structured Concurrency, and Scoped Values -Virtual threads are the really important bit, though. Read the blog I just linked you to, towards the bottom. (Don't be like the Primeagen, who read the article but managed to sort of move on before even getting to the best part - the virtual threads!) +Virtual threads are the really important bit, though. Read the blog I just linked you to, towards the bottom. (Don't be +like [the Primeagen](https://www.youtube.com/watch?v=w87od6DjzAg), who read the article but managed to sort of move on +before even getting to the best part - the virtual threads! My friend... Why??) -Virtual threads are a way to squeeze more out of your cloud infrastructure spend, your hardware, etc., if you're running IO-bound services. They make it so that you can take existing code written against the blocking IO APIs in `java.io`, switch to virtual threads, and suddenly handle with nary a few lines of code changes scale to much higher levels. The effect, usually, is that your system is no longer constantly waiting for threads to be available so the average response time goes down, and - even nicer - you will see the system handle many more requests at the same time! I can't stress this enough. Virtual threads are _awesome_! And if you're using Spring Boot 3.2, you need only specify `spring.threads.virtual.enabled=true` to benefit from them! +Virtual threads are a way to squeeze more out of your cloud infrastructure spend, your hardware, etc., if you're running +IO-bound services. They make it so that you can take existing code written against the blocking IO APIs in `java.io`, +switch to virtual threads, and handle much better scale. The effect, usually, is that your system is no longer +constantly waiting for threads to be available so the average response time goes down, and, even nicer, you will see the +system handle many more requests at the same time! I can't stress this enough. Virtual threads are _awesome_! And if +you're using Spring Boot 3.2, you need only specify `spring.threads.virtual.enabled=true` to benefit from them! -Virtual threads are part of a slate of new features, which have been more than half a decade in coming, designed to make Java the lean, mean scale machine we all knew it deserved to be. And it's working! Virtual threads was 1/3, and is the only one that has been delivered in a GA form. +Virtual threads are part of a slate of new features, which have been more than half a decade in coming, designed to make +Java the lean, mean scale machine we all knew it deserved to be. And it's working! Virtual threads was one of three +features, designed to work together. Virtual threads are the only feature that has been delivered in a release form, as +yet. -There are two other features: structured concurrency and scoped values, both of which have yet to land. Structured concurrency gives you a more elegant programming model for building concurrent code, and scoped values give you an efficient and more versatile alternative to `ThreadLocal`, particularly useful in the context of virtual threads, where you can now realistically have _millions_ of threads. Imagine having duplicated data for each of those! +Structured concurrency and scoped values have both yet to land. Structured concurrency gives you a more elegant +programming model for building concurrent code, and scoped values give you an efficient and more versatile alternative +to `ThreadLocal`, particularly useful in the context of virtual threads, where you can now realistically have +_millions_ of threads. Imagine having duplicated data for each of those! These features are in preview in Java 22. I don't know that they're worth showing, just yet. Virtual threads are the magic piece, in my mind, and they are so magic precisely because you don't really need to know about them! Just set that one property, and you're off. Virtual threads give you the amazing scale of something like `async`/`await` in Python, Rust, C#, TypeScript, JavaScript, or `suspend` in Kotlin, but without the inherent verbosity of code and busy work required to use those language features. It's one of the few times where, save for maybe Go's implementation, Java is just straight-up better in the result. Go's implementation is ideal, but only because they had this baked in to the 1.0 version. Indeed, Java's implementation is more remarkable precisely because it coexists with the older platform threads model. +## Statements Before Super + +This is a nice quality of life feature. Basically, Java doesn't let you access `this` before invoking the super +constructor in a subclass. The goal was to avoid a class of bugs related to invalid state. But it's a bit heavy handed, +and forced developers to resort to `private static` auxillary methods whenever they wanted to do any sort of +non-trivial computation before invoking the super method. Here's an example of the gymanastics sometimes required. I +stole this example from [the JEP](https://openjdk.org/jeps/447) page itself: + +```java +class Sub extends Super { + + Sub(Certificate certificate) { + super(prepareByteArray(certificate)); + } + + // Auxiliary method + private static byte[] prepareByteArray(Certificate certificate) { + var publicKey = certificate.getPublicKey(); + if (publicKey == null) + throw new IllegalArgumentException("null certificate"); + return switch (publicKey) { + case RSAKey rsaKey -> ///... + case DSAPublicKey dsaKey -> ... + //... + default -> //... + }; + } + +} + +``` + +You can see the problem. This new JEP, a preview feature for now, would allow you to inline that method in the +constructor itself, promoting readability and defeating code sprawl. + ## Unnamed Variables and Patterns -When you're creating threads, or working with Java 8 streams and collectors, you're going to be creating lots of lambdas. Indeed, there are plenty of situations - like the `JdbcClient` and its `RowMapper` interface - in Spring where you'll be working with lambdas. +Unnamed variables and patterns are another quality-of-life feature. This one, however, is already delivered. -Fun fact: Lambdas were first introduced in 2014's Java 8 release. (Yes, that was a _decade_ ago! People were doing the ice bucket challenges, the world was obsessed with selfie sticks, _Frozen_, and _Flappy Bird_.) +When you're creating threads, or working with Java 8 streams and collectors, you're going to be creating lots of +lambdas. Indeed, there are plenty of situations in Spring where you'll be working with lambdas. Just think of all +the `*Template` objects, and their callback-centric methods. `JdbcClient` and `RowMapper`, eh... _spring_ to mind, +too! -Lambdas are amazing. They introduce a new unit of reuse in the Java language. And the best part is that they were designed in such a way +Fun fact: Lambdas were first introduced in 2014's Java 8 release. (Yes, that was a _decade_ ago! People were doing the +ice bucket challenges, the world was obsessed with selfie sticks, _Frozen_, and _Flappy Bird_.), but they had the +amazing quality that the almost 20 years of Java code that came before them could participate in lambdas overnight if +methods expected a single method interface implementation. - as to sort of graft onto the existing rules of the runtime, including adapting so-called _functional interfaces_ or SAMs (single abstract method) interfaces automatically to lambdas. My only complaint with them is that it was annoying having to make things final that were referenced from within the lambda that belong to a containing scope. That's since been fixed. And it is annoying having to spell out every parameter to a lambda even if I have no intention of using it, and now, with Java 22, that too has been fixed! +Lambdas are amazing. They introduce a new unit of reuse in the Java language. And the best part is that they were +designed in such a way as to sort of graft onto the existing rules of the runtime, including adapting so-called +_functional interfaces_ or SAMs (single abstract method) interfaces automatically to lambdas. My only complaint with +them is that it was annoying having to make things final that were referenced from within the lambda that belong to a +containing scope. That's since been fixed. And it is annoying having to spell out every parameter to a lambda even if I +have no intention of using it, and now, with Java 22, that too has been fixed! Here is a verbose example just to +demonstrate the use of the `_` character in two places. Because I can. ```java package com.example.demo; @@ -399,7 +519,7 @@ import org.springframework.stereotype.Component; import javax.sql.DataSource; @Component -class AnonymousLambda Parameters implements LanguageDemonstrationRunner { +class AnonymousLambdaParameters implements LanguageDemonstrationRunner { private final JdbcClient db; @@ -413,6 +533,7 @@ class AnonymousLambda Parameters implements LanguageDemonstrationRunner { @Override public void run() throws Throwable { var allCustomers = this.db.sql("select * from customer ") + // here! .query((rs, _) -> new Customer(rs.getInt("id"), rs.getString("name"))) .list(); System.out.println("all: " + allCustomers); @@ -422,11 +543,30 @@ class AnonymousLambda Parameters implements LanguageDemonstrationRunner { ``` -That class uses Spring's `JdbcClient` to query the underlying database. It pages through the results, one by one, and then invokes our lambda, which conforms to the type `RowMapper` to help in adapting our results into records that line up with my domain model. The `RowMapper` interface method `T mapRow(ResultSet rs, int rowNum) throws SQLException;` expects two parameters: the `ResultSet`, which I'll need, and the `rowNum`, which I'll almost never need. Now, I don't need to specify it! Just plug in `_`, like in Kotlin or TypeScript. Nice! +That class uses Spring's `JdbcClient` to query the underlying database. It pages through the results, one by one, and +then involves our lambda, which conforms to the type `RowMapper` to help in adapting our results into records +that line up with my domain model. The `RowMapper` interface , to which our lambda conforms, has a single +method `T mapRow(ResultSet rs, int rowNum) throws SQLException` that expects two parameters: the `ResultSet`, which I'll +need, and the `rowNum`, which I'll almost never need. Now, thanks to Java 22, I don't need to specify it. Just plug +in `_`, like in Kotlin or TypeScript. Nice! ## Gatherers -Gatherers are another nice feature that is also in preview. You may know my friend Viktor Klang from his amazing work on Akka, the actor cluster, and the Scala language, whilst he was at Lightbend. These days, he's a Java language architect at Oracle, and one of the things he's been working on is the new Gatherer API. The streams API, which was also introduced in Java 8, by the way - gave Java developers a chance, along with lambdas, to greatly simplify and modernize their existing code, and to move in a more functional-programming-centric direction. But, there are cracks in the abstraction. The Streams API has a number of very convenient operators that work for 99% of the scenarios, but when you find something for which a convenient operator doesn't exist, it can be frustrating. There have been countless proposals for new operators adding to the Streams API in the intervening ten years, and there were even discussions and concessions made in the original proposal for lambdas that the programming model be flexible [enough to support introducing new operators](https://cr.openjdk.org/~vklang/Gatherers.html). It's finally arrived! Gatherers provide a slightly more low-level abstraction that gives you the ability to plug in all sorts of new operations on Streams, without having to materialize the `Stream` as a `Collection` at any point. Here's an example I stole directly, and unabashedly, [from Viktor and the team](https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/util/stream/Gatherer.html). +Gatherers are another nice feature that is also in preview. You may know my +friend [Viktor Klang](https://twitter.com/viktorklang) from his amazing work +on [Akka](https://doc.akka.io/docs/akka/current/typed/actors.html) and for his contributions to Scala futures whilst +he was at Lightbend. These days, he's a Java language architect at Oracle, and one of the things he's been working on is +the new Gatherer API. The Stream API, which was also introduced in Java 8, by the way - gave Java developers a chance, +along with lambdas, to greatly simplify and modernize their existing code, and to move in a more +functional-programming-centric direction. It models a set of transformations on a stream of values. But, there are cracks in the abstraction. The Streams API has a number of very +convenient operators that work for 99% of the scenarios, but when you find something for which a convenient operator +doesn't exist, it can be frustrating because there was no easy way to plug one in. There have been countless proposals for new operator additions to the Streams API in +the intervening ten years, and there were even discussions and concessions made in the original proposal for lambdas +that the programming model be +flexible [enough to support introducing new operators](https://cr.openjdk.org/~vklang/Gatherers.html). It's finally +arrived, albeit as a preview feature. Gatherers provide a slightly more low-level abstraction that gives you the ability to plug in all sorts of new +operations on Streams, without having to materialize the `Stream` as a `Collection` at any point. Here's an example I +stole directly, and unabashedly, [from Viktor and the team](https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/util/stream/Gatherer.html). ```java package com.example.demo; @@ -442,13 +582,12 @@ import java.util.stream.Stream; @Component class Gatherers implements LanguageDemonstrationRunner { - private static Gatherer scan(Supplier initial, - BiFunction scanner) { + private static Gatherer scan( + Supplier initial, + BiFunction scanner) { class State { - R current = initial.get(); - } return Gatherer.ofSequential(State::new, Gatherer.Integrator.ofGreedy((state, element, downstream) -> { @@ -472,10 +611,12 @@ class Gatherers implements LanguageDemonstrationRunner { ``` -The main thrust of that code is that there's a method here, `scan`, which returns an implementation of `Gatherer`. Each ` +The main thrust of that code is that there's a method here, `scan`, which returns an implementation of `Gatherer`. Each `Gatherer` expects an initializer and an integrator. It'll come with a default combiner and a default finisher, though you can override both. This implementation reads through all those number entries and builds up a string for each entry that then accumulates after every successive string. The result is that you get `1`, then `12`, then `123`, then `1234`, etc. + +The example above demonstrates that gatherers are also composable. We actually have two `Gatherer` in play: the one that does the scanning, and the one that maps every item to uppercase, and it does it concurrently. + +Still don't quite understand? I get the feeling that's going to be okay. This is a bit in the weeds for most folks, I'd imagine. Most of us don't need to write our own Gatherers. But you _can_. My friend [Gunnar Morling](https://www.morling.dev/blog/zipping-gatherer/) did just that the other day, in fact. The genius of the Gatherers approach is that now the community can scratch its own itch. I wonder what this implies for awesome projects like Eclipse Collections or Apache Commons Collections or Guava? Will they ship Gatherers? What other projects might? I'd love to see a lot of common sense gatherers, eh, well, _gathered_ into one place. -Gatherer` expects an initializer and an integrator. It'll come with a default combiner and a default finisher, though you can override both. This implementation reads through all those entries (numbers) and builds up a string for each entry that then accumulates every successive string. The result is that you get `1`, `12`, `123`, `1234`, etc. -Still don't quite understand? I get the feeling that's going to be okay. This is a bit in the weeds for most folks, I'd imagine. Most of us don't need to write our own Gatherers. But you can. My friend [Gunnar Morling](https://www.morling.dev/blog/zipping-gatherer/) did just that the other day, in fact. I wonder what this implies for awesome projects like Eclipse Collections? Will they ship Gatherers? What other projects might? The genius of the Gatherers approach is that now the community can scratch its own itch. I'd love to see a lot of common sense gatherers, eh, well, gathered into one place. +## Conclusion -The example above demonstrates that gatherers are also composable. We actually have two `Gatherer` in play: the one that does the scanning, and the one that maps every item to uppercase, and it does it concurrently. \ No newline at end of file