[MUSIC PLAYING]
AARON WANJALA: Hello, everyone.
I'm Aaron Wanjala, and I'm very excited to be here at IO
talking to you.
I'm a Java developer advocate here at Google Cloud,
but before joining Google, I spent several years
as a dev building, shipping, and maintaining Spring and Angular
apps.
As a Java developer advocate, it's
my job to sit between Google Cloud and you, the Java dev
community, in order to help our products to be
as useful as possible to you.
That could mean coming to conferences
and doing workshops like this, contributing to projects
directly, or just hearing you folks out
and bringing your feedback to product teams.
That's why I'm excited to be here.
I get to share what I hope is some useful information
with you, and most importantly, invite
you to start a conversation.
So if you have any feedback on this workshop
or Google Cloud's projects, in general,
I'm available on Twitter.
DMs are open.
Feel free to reach out.
In today's workshop, we'll be focusing
on the Spring Native project.
We'll start by explaining what it is and talking about some
of its recent milestones.
After that, we'll go over some good use
cases for Spring Native apps, and finally, we'll
work through implementing it together.
So without further ado, what is Spring Native?
Simply put, Spring Native is a project
to make it easier for developers to compile their Spring
applications into native executables.
Running your applications as native executables,
rather than on the JVM means smaller artifacts,
faster startup times, and lower memory usage, all of which
allows you to get more work done with resources that you already
have.
We'll get into the details of why that is in just a moment.
Spring Native makes use of several concepts
and technologies to make this happen,
including ahead-of-time compilation and GraalVM.
We'll talk a bit about these before we actually
start using Spring Native.
Currently, the project is in an experimental phase
and is separate from the main Spring and Spring Boot
projects.
And I know that could make you pause
when thinking about implementing it in a larger project,
I've been there.
But Spring Native has been and especially promising project,
which is probably why at Spring I 2021,
it was announced that it would be merged into Spring Boot 3.0
and have first class support.
With that announcement in mind, I
think now is a perfect time to take a closer
look at the project and to think about how it can help us run
our workloads more efficiently.
OK, let's discuss compilation.
The first thing we should discuss
are the differences between Java standard just-in-time
compilation, and the ahead-of-time compilation
that we'll be ending with today.
When developers run Java C, normally a compile time,
we turn our .Java source code into .class byte code.
These class files can only really
be understood by the Java virtual machine itself,
so before our code can actually be executed on our server
at runtime, the JVM must interpret or further compile it
into native code just in time for it
to be executed at runtime.
This approach has one big advantage and one big drawback.
On the one hand, we get to write our code once and run it
everywhere, the popular Java tag line,
and that gives us portability because we
know that the JVM will figure out
how to execute it on whichever system
we end up deploying it to.
On the other hand, the interpretation step
is quite expensive when compared to just running native code
directly.
As a result, we often end up trading performance
for portability, even in situations
where that portability doesn't buy us all that much.
Ahead-of-time compilation takes the opposite approach
by compiling all reachable code into a native executable
at compile time so that we can simply run
our code on the target machine.
The result here is a trade off in the opposite direction,
giving up portability in exchange for a smaller
application that executes our code directly at runtime
without the need for further compilation,
meaning that your app can start up faster and uses less memory.
Of course, each approach can shine in their own use cases.
The standard JVM is very well optimized
for long-running processes, while AOT compilation
is great for short-lived workloads
where a fast startup time is important.
As a fun fact, AOT compilation was introduced
as an experimental feature in JDK 9,
though its implementation was expensive to maintain,
and it never quite caught on, so it was quietly
removed in Java 17 in favor of developers
just using GraalVM, which is what
Spring Native makes use of.
Speaking of GraalVM, what exactly is it?
GraalVM is a highly-optimized, open source JDK distribution
that boasts extremely fast startup times,
AOT native compilation, and polyglot capabilities
that allow developers to mix multiple languages
into a single application.
Graal is an active development with lots
of notable milestones, but the one I'll quickly highlight here
is Java 17 support, which is pretty recent,
having only happened on January 18, 2022.
So to be a little bit more specific with our earlier
Spring Native definition, Spring Native
enables the use of GraalVM's native image compiler
to turn Spring applications into native executables.
This process involves performing a static analysis
of your application at compile time
to find all methods and configuration
in your application that are reachable from the entry point.
This essentially creates a closed-world conception
of your application, where all code
is assumed to be known at compile time
and no new code is allowed to be loaded at runtime.
It's important to note that native image generation is
a memory intensive process that takes longer than compiling
a regular application and imposes limitations
on certain aspects of Java.
In some cases, no actual code changes
are required for an application to work with Spring Native.
However, there are some things like dynamic class loading
or dynamic proxies that need to be enumerated or configured
ahead of time in order to work.
In those situations, Spring Native
provides things like native hints
to simplify the process, which are well documented.
All right.
With all that background covered,
we can finally write some code.
Before we start implementing Spring Native,
we'll need to create and deploy an app to establish
a performance baseline that we can compare
to the native version later.
Today, we'll be going through this on the Cloud Shell,
on a new Google Cloud project to minimize
the configuration required, but you can follow along
on your own machine as well.
We'll start by getting our app from start.spring.io.
The starter app uses Spring Boot 2.6.4,
which is the latest version that's
supported by the Spring Native project at time of writing.
Note that since the release of GraalVM 21.0.3,
you could use Java 17 for the sample as well.
We will be sticking with Java 11 today
to minimize the config involved though.
Once we have our zip on the command line,
we can create a sub-directory for our project
and unzip the folder in there.
Once we have the project opened in our IDE of choice,
I'll be using Cloud editor, we'll
quickly add a sign of life and something
to showcase the Spring Native performance once we run it.
One quick tip for others using Cloud Code,
I find that I get better code completion and import
suggestions when I first opened my source
directory as a Workspace.
All we're doing here is adding two fields, start time
and ready time, to help us understand the startup
time once we run our application, instructing
the main function to save the start time,
then using the application ready event to log when Spring
says our application is ready.
Once we have those two times, we're
ready to take the duration between them
and display the startup time when a user visits our app.
At this point, our baseline app is ready to go.
So feel free to build an image and run it locally
in order to get an idea of the startup time
before we convert it to a native application.
To build our image, we can run the Spring Boot Build-Image
Maven Goal.
Once that's built, you can also use
something like Dive or Docker images demo
to get an idea of the size of the baseline image.
And if we want to run our locally-built regular Docker
image, we can do so at Docker Run.
Now that we have our app, we'll deploy it
and take note of the startup times, which
will compare to our native app startup times later on.
Depending on the type of application you're building,
there are several options for hosting your stuff.
However, since our example is a very simple, straightforward
web application, we can keep things really simple
and just rely on Cloud Run.
If you're following along on your own machine,
make sure to have the gcloud CLI tool installed an updated.
Since this project is still in the experimental phase,
we'll have to configure our app to be
able to find experimental artifacts, which are not
available in Maven Central Repository.
This will involve making changes to our palm, which you can do
in the editor of your choice.
Next, we'll need to bring in the dependencies required
to run a Spring application as a native image.
Note that this step is not necessary
if you're using Gradle.
Now, we'll add the ahead of time plugin
to improve native image compatibility and artifact
footprint.
And now, we'll update Spring Boot Mavin plug-in
to enable native image support and use the Marketo builder
to build our native image.
Note that the tiny builder image is just one of several options.
It's a good choice for our use case
because it has very few extra libraries and utilities,
which helps us to minimize our attack surface.
But if, for example, you were building
an app that needed access to some common C libraries
or you weren't yet sure of your app's requirements,
then the full builder might be a better fit.
Now, that our apps are configured,
we should be able to build our image
and run our native compiled app.
Before running the build, here are a few things
to keep in mind.
This will take more time than a regular build.
Think a few minutes.
This build process can take a lot of memory.
Think a few gigabytes.
This build process requires the Docker daemon to be reachable.
And while in this example, we're going through the process
to manually build, you can also configure
you build phases to automatically trigger
a native build profile.
With that in mind, just like before,
we can build our app with the Spring Boot Build-Image Goal.
And once that's built, we can run it the same way.
At this point, we're in a great position
to see both sides of the native application equation.
We've given up a bit of time and extra memory usage
at compile time, but in exchange, we
get an application that can start up far more quickly
and consume significantly less memory,
depending on the workload.
If we run something like Dive or Docker Images Demo
to compare the size of the native image to the original,
we can see a dramatic reduction in the image's size.
We should also note that in more complex use cases,
there are additional modifications needed
to inform the AOT compiler of what
your app will do at runtime.
For that reason, certain predictable workloads,
like batch jobs may be very well suited to this, while others
could be a bigger lift.
In order to deploy our app to cloud run,
we'll need to get our native image
into an image store or package manager like artifact registry.
We can get started on this by creating a repository,
then we'll need to make sure that we're
authenticated to push images to our new registry.
The gcloud CLI simplifies this process quite a bit.
Now, we'll tag our image.
Finally, we can use Docker Push to send it
to artifact registry.
We are now ready to take the native application
image that we've stored in artifact registry
and deploy it to Cloud Run.
Since we've built and deployed our app as a native image,
we can rest assured that our application is
making excellent use of our infrastructure costs
as it runs.
And now that our native service is ready,
we can compare it's startup time to the original.
Congratulations on building and deploying
a Spring Native application on Google Cloud.
Hopefully, this tutorial encourages
you to get more familiar with the Spring Native project,
and keep it in mind should it meet your needs in the future.
Whether you created a new Google Cloud project for this workshop
or are reusing an existing one, take care
to avoid unnecessary charges from the resources we made use
of.