What is Spring Native?
Struggling in reducing the size of your Spring applications fat deployment artifacts? Its become a real problem in this Cloud era, especially when considering a microservices and/or serverless architecture. Learn how Spring Native hits a home run and puts Spring back in the game.
Announcing Spring Native | Beta Release
Spring Native is capable of dramatically shrinking the size of those fat Spring jars into standalone executables, know as native images. All of this is performed via the GraalVM native-image compiler which performs a whole set of optimizations at build time through static analysis. More on this later.
With Spring Native, we get millisecond startup times, improved performance and reduce our memory footprint substantially. This is already the case in Beta, so expect even more improvements to come.
The Problems Spring Native Solves
Spring applications have slowly bloated over the years and have unfortunately not been the best fit for a microservices and/or serverless architecture. They tend to be too big and slow to spin up in containerized environments, especially when compared to other languages.
It’s the elephant in the room that we all don’t want to take about. We love Java/Spring and have invested so much into it but the truth is that many companies/developers have looked at other options because of this.
Spring Native is coming to the rescue and just hit a home run with the beta release. The grand slam is coming!
The Spring Native project has been (and still is) a huge undertaking. The reason for this has to do with how different Spring and GraalVM behave. Especially in regards to build time versus runtime behaviors. Spring performs runtime decisions which are very dynamic in nature like creating proxies, lazy-loading etc ..
On the other hand GraalVM needs everything ahead of time (AOT) … at build time with the classpath set in stone before loading the application. So how did the GraalVM and Spring team bridge the gap?
Spring developed a Spring AOT (Ahead Of Time) parser to create configuration information files which the GraalVM compiler needs to perform the conversion to a binary. The Spring AOT parser is made available to us via either a maven or gradle plugin which I will show you later on (maven only).
This is a huge deal when we start deploying microservices and/or serverless architectures. We need fast startup times and low memory footprints. This will increase the application density on a host, improve performace, reduce costs for on-demand resources used and even be able to fit under the serverless quota limits that some Cloud providers impose on things like maximum memory consumption or payload size.
So basically, Spring Native now puts us back in the game in terms of being able to compete with other technologies, especially for deployment architectures involving containerization.
Spring Native Example | Test Drive
You can start experimenting with Spring Native as of now via start.spring.io with Spring Boot version 2.4.4 on your own as Spring Native is already available for selection in the dependencies list.
Let’s take a test drive and git clone the Spring GraalVM native samples from GitHub ..
1 |
git clone https://github.com/spring-projects-experimental/spring-native.git |
Once in the samples directory, import the petclinic-jdbc project into your IDE.
The first thing you have to look at is the pom.xml file for those new experimental dependencies and maven plugins. I have only included what is relevant to Spring Native.
You can see that we start off with a different parent pom dependency which is native based – experimental. A noteworthy build plugin is the Spring AOT parser I mentioned earlier. This plugin is critical in creating those configuration files needed by GraalVM in order to perform the compilation to a native image.
What is not so obvious is the experimental tomcat server which is there. It has been optimized for a Spring Native deployment. It will shave off 3.5M of memory from the final container image. These things add up!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<?xml version="1.0" encoding="UTF-8" ?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.experimental</groupId> <artifactId>spring-native-sample-parent</artifactId> <version>0.9.2-SNAPSHOT</version> <relativePath>../maven-parent/pom.xml</relativePath> </parent> .......... .......... omitted on purpose for clarity .......... <dependencies> <dependency> <groupId>org.springframework.experimental</groupId> <artifactId>spring-native</artifactId> </dependency> .......... .......... omitted on purpose for clarity .......... <dependency> <groupId>org.apache.tomcat.experimental</groupId> <artifactId>tomcat-embed-programmatic</artifactId> <version>${tomcat.version}</version> </dependency> .......... .......... omitted on purpose for clarity .......... <build> <plugins> <plugin> <groupId>org.springframework.experimental</groupId> <artifactId>spring-aot-maven-plugin</artifactId> <configuration> <removeYamlSupport>true</removeYamlSupport> </configuration> </plugin> .......... .......... omitted on purpose for clarity .......... |
Open up a terminal window in your IDE to build and run the native application packaged in a lightweight container image.
1 |
mvn clean package spring-boot:build-image |
You’ll notice that Spring-native builds takes much longer than your Spring-JVM based builds. Again, this is due to the GraalVM optimizations which don’t come for free. You pay a price at build time so you have to be patient 🙁
At the end of the build process, the Spring-native image gets copied into a Docker Image – performed by either Maven or Gradle plugin. Spring uses buildpacks within that plugin which is an alternative way to using a Dockerfile and is very compact/optimized.
On top of that, the docker image doesn’t include the JVM since the native image doesn’t need it – resulting in a very lightweight container at runtime. The build creates the following docker image “petclinic-jdbc:0.0.1-SNAPSHOT”.
My Spring-native build lasted 6:37 min .. ouch!
Spring Native Long Build Times | What to do?
This does raise an important point though. These long build times mean that it’s not a good fit for fast developer cycles like … code, compile,test as in your IDE. No one wants to wait several minutes extra on each development cycle. Having said that, what do we do?
In development, you’ll still want to continue to use a Spring-JVM setup in order to maintain those fast development cycles, so do not change that … it’s business as usual. However, what will change is how the application gets treated in your CI/CD pipelines. We’re able to absorb the higher build times in CI/CD environments in exchange for lighting fast deployments and lower resource usage. A great fit!
Spring Native | Run Docker Image
At the end of the build, you’ll need to list you docker images so you need to have docker – install docker. If your not too clear on how Java/Spring applications get containerized then check out my post How to Create Docker Image for Java Application
1 |
docker images |
This is what I see …
1 2 |
$ docker images | grep petclinic petclinic-jdbc 0.0.1-SNAPSHOT e302b08389f0 41 years ago 143MB |
We can see that the image is 142MB. Let’s run the Pet Clinic Spring native application with the following docker command.
1 |
docker run --name spring-native -p 8081:8080 --rm petclinic-jdbc:0.0.1-SNAPSHOT |
Here is my console output. Look at the startup time on line 32 … 0.139 seconds which is 139 milliseconds … wow, that’s incredible!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
$ docker run --name spring-native -p 8081:8080 --rm petclinic-jdbc:0.0.1-SNAPSHOT 2021-04-15 19:12:58.918 INFO 1 --- [ main] o.s.nativex.NativeListener : This application is bootstrapped with code generated with Spring AOT . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.4.4) 2021-04-15 19:12:58.920 INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting PetClinicApplication using Java 1.8.0_282 on bb364a89b932 with PID 1 (/workspace/org.springframework.samples.petclinic.PetClinicApplication started by cnb in /workspace) 2021-04-15 19:12:58.920 INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : No active profile set, falling back to default profiles: default 2021-04-15 19:12:58.987 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) Apr 15, 2021 7:12:58 PM org.apache.coyote.AbstractProtocol init INFO: Initializing ProtocolHandler ["http-nio-8080"] Apr 15, 2021 7:12:58 PM org.apache.catalina.core.StandardService startInternal INFO: Starting service [Tomcat] Apr 15, 2021 7:12:58 PM org.apache.catalina.core.StandardEngine startInternal INFO: Starting Servlet engine: [Apache Tomcat/9.0.44] 2021-04-15 19:12:58.989 INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 68 ms Apr 15, 2021 7:12:58 PM org.apache.catalina.core.ApplicationContext log INFO: Initializing Spring embedded WebApplicationContext 2021-04-15 19:12:59.022 INFO 1 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2021-04-15 19:12:59.034 WARN 1 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration) Apr 15, 2021 7:12:59 PM org.apache.coyote.AbstractProtocol start INFO: Starting ProtocolHandler ["http-nio-8080"] 2021-04-15 19:12:59.044 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2021-04-15 19:12:59.048 INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Started PetClinicApplication in 0.139 seconds (JVM running for 0.143) |
We can see the resource footprint using the “docker stats” command which shows 57.17 MB of memory with 0.04% CPU ..
1 2 3 |
$ docker stats CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 37b55e79208a spring-native 0.04% 57.17MiB / 14.47GiB 0.39% 13.9kB / 606kB 2.85MB / 0B 18 |
You can also follow along by watching the YouTube tutorial below
Comparing Spring-Native vs Spring-JVM | Pet Clinic
So how do these numbers compare to the Spring JVM based pet clinic application that you would normally run? I will clone the following git project and perform a full build into a container image as done before.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
$ git clone https://github.com/spring-projects/spring-petclinic.git $ mvn clean package spring-boot:build-image [INFO] Scanning for projects... [INFO] [INFO] ------------< org.springframework.samples:spring-petclinic >------------ [INFO] Building petclinic 2.4.2 [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ spring-petclinic --- [INFO] Deleting /home/andy/IdeaProjects/spring-petclinic/target ... omitted for brevity [INFO] [creator] Saving docker.io/library/spring-petclinic:2.4.2... [INFO] [creator] *** Images (9febd6ff3b0c): [INFO] [creator] docker.io/library/spring-petclinic:2.4.2 [INFO] [INFO] Successfully built image 'docker.io/library/spring-petclinic:2.4.2' [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 01:03 min [INFO] Finished at: 2021-04-15T19:03:08Z [INFO] ------------------------------------------------------------------------ |
The above builds, runs about 40 tests and packages the jar file in a container image which takes 01:03 min as seen above. If you instead ran a “mvn package” then it would take less time, realize that there is overhead in building the container image. Obviously, the Spring-JVM based project has a big advantage at build time over the Spring-native version.
The next question is, how fast does it take the Spring-JVM pet clinic application to run? We can see in the console output above that the spring-boot maven plugin creates the “spring-petclinic:2.4.2“ docker image. Let’s run it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
$ docker run --name spring-jvm -p 8080:8080 --rm spring-petclinic:2.4.2 Setting Active Processor Count to 4 Calculating JVM memory based on 10313200K available memory Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx9681983K -XX:MaxMetaspaceSize=119216K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 10313200K, Thread Count: 250, Loaded Class Count: 18634, Headroom: 0%) Adding 129 container CA certificates to JVM truststore Spring Cloud Bindings Enabled Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -agentpath:/layers/paketo-buildpacks_bellsoft-liberica/jvmkill/jvmkill-1.16.0-RELEASE.so=printHeapHistogram=1 -XX:ActiveProcessorCount=4 -XX:MaxDirectMemorySize=10M -Xmx9681983K -XX:MaxMetaspaceSize=119216K -XX:ReservedCodeCacheSize=240M -Xss1M -Dorg.springframework.cloud.bindings.boot.enable=true |\ _,,,--,,_ /,`.-'`' ._ \-;;,_ _______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______ | | '---''(_/._)-'(_\_) | | | | | | | | | | _ | ___|_ _| | | | | |_| | | | __ _ _ | |_| | |___ | | | | | | | | | | \ \ \ \ | ___| ___| | | | _| |___| | _ | | _| \ \ \ \ | | | |___ | | | |_| | | | | | | |_ ) ) ) ) |___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / / ==================================================================/_/_/_/ :: Built with Spring Boot :: 2.4.2 2021-04-15 19:12:32.367 INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting PetClinicApplication v2.4.2 using Java 1.8.0_282 on db37a7ce2b9e with PID 1 (/workspace/BOOT-INF/classes started by cnb in /workspace) 2021-04-15 19:12:32.373 INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : No active profile set, falling back to default profiles: default 2021-04-15 19:12:33.786 INFO 1 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2021-04-15 19:12:33.869 INFO 1 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 70 ms. Found 4 JPA repository interfaces. 2021-04-15 19:12:34.661 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2021-04-15 19:12:34.678 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2021-04-15 19:12:34.678 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.41] 2021-04-15 19:12:34.762 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2021-04-15 19:12:34.762 INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2303 ms 2021-04-15 19:12:35.370 INFO 1 --- [ main] org.ehcache.core.EhcacheManager : Cache 'vets' created in EhcacheManager. 2021-04-15 19:12:35.383 INFO 1 --- [ main] org.ehcache.jsr107.Eh107CacheManager : Registering Ehcache MBean javax.cache:type=CacheStatistics,CacheManager=urn.X-ehcache.jsr107-default-config,Cache=vets 2021-04-15 19:12:35.391 INFO 1 --- [ main] org.ehcache.jsr107.Eh107CacheManager : Registering Ehcache MBean javax.cache:type=CacheStatistics,CacheManager=urn.X-ehcache.jsr107-default-config,Cache=vets 2021-04-15 19:12:35.468 INFO 1 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2021-04-15 19:12:35.693 INFO 1 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2021-04-15 19:12:35.891 INFO 1 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2021-04-15 19:12:35.954 INFO 1 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.4.27.Final 2021-04-15 19:12:36.122 INFO 1 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final} 2021-04-15 19:12:36.279 INFO 1 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect 2021-04-15 19:12:37.274 INFO 1 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] 2021-04-15 19:12:37.284 INFO 1 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2021-04-15 19:12:38.354 INFO 1 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2021-04-15 19:12:39.692 INFO 1 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 13 endpoint(s) beneath base path '/actuator' 2021-04-15 19:12:39.781 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2021-04-15 19:12:39.813 INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Started PetClinicApplication in 7.898 seconds (JVM running for 8.397) ^C2021-04-15 19:12:52.336 INFO 1 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor' 2021-04-15 19:12:52.341 INFO 1 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' 2021-04-15 19:12:52.347 INFO 1 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2021-04-15 19:12:52.378 INFO 1 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. 2021-04-15 19:12:52.383 INFO 1 --- [extShutdownHook] org.ehcache.core.EhcacheManager : Cache 'vets' removed from EhcacheManager. |
Running the JVM based application, took 7.898 seconds to deploy. This is much slower than the Spring-native based application, but we expected this.
Spring JVM vs Spring Native | Summary of Results
We can see the following Spring-Native and Spring-JVM based container images listed out below via “docker images”. There is a big difference in their image size. Spring-Native clearly takes up less disk space (only 143MB instead of 261MB) which will translate to smaller runtime containers.
When both containers are run side by side, we can see via “docker stats” that Spring-Native is a again a clear leader in resource consumption. The memory usage is practically an order of magnitude less (57.17 MiB instead of 508.8MiB). The cpu utilization is also reduced but not enough in this sample to make a big deal about it.
1 2 3 4 5 6 7 8 |
$ docker images | grep petclinic spring-petclinic 2.4.2 8846bf593485 41 years ago 261MB petclinic-jdbc 0.0.1-SNAPSHOT e302b08389f0 41 years ago 143MB $ docker stats CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS b73dcb33498e spring-jvm 0.21% 508.8MiB / 14.47GiB 3.43% 8.66kB / 7.51kB 1.29MB / 315kB 33 37b55e79208a spring-native 0.04% 57.17MiB / 14.47GiB 0.39% 13.9kB / 606kB 2.85MB / 0B 18 |
Spring Native Drawbacks and Trade-offs
It is not all roses! At this point, we can see that all this comes at the cost of much longer build times however, I already mentioned how to handle this previously.
Static analysis of the entire application is performed at build time in order to gather configuration information on things like reflection, resources, and dynamic proxies. This means that there are times that you have to provide additional configuration in the form of “hints” in your code (like an annotation or property) in order to help GraalVM port code from the JVM to binary.
Honestly, it’s a little messy now and improvements will be made in this area by the Spring team. Here’s a simple example which highlights the fact that GraalVM only supports JDK proxies and not CGLIB proxies hence the need for ..
1 |
@SpringBootapplication (proxyBeanMethods = false) |
Of course, I would just create a new annotation called @SpringBootNativeApplication in order to abstract any other properties we might want to set. I don’t want to show case examples of these “hints” becacuse I expect this to change quite a bit in the future.
At this time (beta release), not all Spring services are candidates for Spring Native which means that not all Spring Boot starters are supported right now. That will obviously change but for now, here is the list of Spring Boot started that are supported.
Spring Native | Summary
Spring Native is only in Beta right now but it already posts very impressive numbers. Looks like we will eventually have to choose between a Spring JVM and Spring Native application in the Spring’s next major release.
Spring Native is an experimental feature for now but it’s here to stay and will improve by leaps and bounds. Remember how Docker just started out as a developer tool for Ubuntu? You can’t go anywhere today without talking about containers. It’s going to be the same with Spring Native.
Make no mistake, this is a game changer. Through Spring Native, Spring has a real chance in becoming a prime candidate for small, quick and optimized running applications in this ever demanding containerized world.