The problem with Spring
As we mentioned in the previous article, Java-based microservices suffer from two primary disadvantages; slow startup times and large memory footprints. Oracle introduced the GraalVM with Native Image support, which has helped mitigate these concerns. As GraalVM native image support has matured, many Java microservice frameworks have embraced the native image technology. Noticeably absent from this list has been the leading Java microservice framework: Spring Boot. For many, Spring Boot is synonymous with Java Microservices. It has long been the leading microservices framework in the Java world. However, its dominance has started to decline as other Java frameworks mature. In response to this, Spring spent a year and a half working to integrate GraalVM native image support. On March 11, 2021, Spring Native Beta was released.Introducing Spring native
The Spring Framework has been around since 2002. It leverages nearly all Java's language features to provide a comprehensive collection of subprojects for build applications. Unfortunately, each of those subprojects needs to be revisited, and neccesary provision made to enable support for the GraalVM, including Spring Boot, Spring Security, Spring Cloud, Spring Data. It is important to remember, however, that Spring Native is currently a beta release. Fortunately, in the beta release, Spring native provides support for a subset of the framework, including Spring Boot, Spring Cloud, Spring Kafka, Mysql JDBC driver, PostgreSQL JDBC driver, GRPC, Lombok, and the H2 database. Additionally, the Spring Initializr was modified to simplify the process of generating the Spring Native application structure.Spring Boot support
Spring Boot provides a rich set of starters that automagically include Spring subproject dependencies into your application. With Spring Native, we get a substantial subset to work with include:- spring-boot-starter-actuator
- spring-boot-starter-data-elasticsearch
- spring-boot-starter-data-jdbc
- spring-boot-starter-data-jpa
- spring-boot-starter-data-mongodb
- spring-boot-starter-data-neo4j
- spring-boot-starter-data-r2dbc
- spring-boot-starter-data-redis
- spring-boot-starter-jdbc
- spring-boot-starter-logging
- spring-boot-starter-mail
- spring-boot-starter-thymeleaf
- spring-boot-starter-rsocket
- spring-boot-starter-validation
- spring-boot-starter-security
- spring-boot-starter-oauth2-resource-server
- spring-boot-starter-oauth2-client
- spring-boot-starter-webflux
- spring-boot-starter-web
- spring-boot-starter-websocket
- spring-boot-starter-quartz
Spring Cloud support
Spring Cloud extends the core starters with a collection of common distributed system patterns to provide configuration management, service discovery, circuit breakers, routing, proxy, control bus, and more. With Spring Native we get access to :- spring-cloud-starter-bootstrap
- spring-cloud-starter-config
- spring-cloud-config-client
- spring-cloud-config-server
- spring-cloud-starter-netflix-eureka-client
- spring-cloud-starter-task
- spring-cloud-function-web
- spring-cloud-function-adapter-aws
- spring-cloud-starter-function-webflux
- spring-cloud-starter-sleuth
Spring AOT
Spring Native provides Ahead-Of-Time (AOT) compilation of the application to a platform-specific binary executable using the GraalVM tools. Luckily, the team has simplified the process by introducing both Maven and Gradle plugins to streamline the process.Spring AOT Maven and gradle plugins
Spring makes liberal use of reflection, lazy loading, proxies, etc. Since the GraalVM native image executables handle these features differently,the Spring Native team created plugins for both Maven and Gradle. These plugins analyzes the application and generate the GraalVM configuration files needed to support native image execution.Inference Engine
The AOT plugin contains an inference engine which understands the Spring programming model and its infrastructure. The plugin evaluates the application code and generates entries in each of the supplemental configuration files. This inference engine performs much of the work needed to handle GraalVM native image configuration.,Native Hint Annotations
Unfortunately, all of the configuration information can't always be inferred by the inference engine. To address this shortcoming, the Spring Native team introduced Native Hint Annotations. The Native Hint Annotations leverage Java's Annotations to support configuration using a typesafe mechanism rather than the GraalVM supplemental configuration files. In the following example, we can use the @NativeHint annotation to configure a MySQL driver.
@NativeHint(
trigger = Driver.class,
options = "--enable-all-security-services",
types = @TypeHint(types = {
FailoverConnectionUrl.class,
FailoverDnsSrvConnectionUrl.class,
// ...
}), resources = {
@ResourceHint(patterns = "com/mysql/cj/TlsSettings.properties"),
@ResourceHint(patterns = "com.mysql.cj.LocalizedErrorMessages",
isBundle = true)
})
public class MySqlHints implements NativeConfiguration {}
Spring will evaluate this annotation during the build and add entries to the appropriate configuration files (e.g., reflect-config.json, resource-config.json, and native-image.properties).
GraalVM Tracing Agent
While Spring Native is able to automatically identify and generate a large portion of the native image configuration, to identify the elements it missed manually. To do this, we can employ the GraalVM native image tracing agent. One of the best methods for exercising the application when using the tracing agent is to execute the application's test suite, provided the suite has sufficient code coverage. The obvious alternative would be to exercise the app manually. However, this approach is tedious and may suffer from a lack of repeatability.The tracing agent is implemented as a Java agent and is invoked like other agents, using the -agentlib JVM flag.
$JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ ...
During it execution, the tracing agent will intercept all calls that lookup classes, methods, fields, resources or request proxy accesses. After it intercepts the call, it will add appropriate entries to supplemental JSON configuration files in which ever output directory was supplied.
Building A Simple Spring Native Service
There are two approaches for setting up a Spring Native: Cloud Native Buildpacks and GraalVM Native Image Maven plugin.Cloud Native Build packs
Spring created Cloud Native Buildpacks to enable the creation of Docker compatible container images using either the Maven or Gradle build tools. This approach simplifies the build process by providing everything necessary to produce a containerized native application. We need only to install our build tool of choice ( Maven or Gradle) and Docker.GraalVM native image Maven plugin support
An alternative approach is to use the GraalVM Native Image Maven or Gradle plugin. This setup is slightly more complex, requiring the manual installation of the GraalVM and its native image support (gu install native-image).For this article, we will be using the Cloud Native Build Pack approach to make the build as portable as possible.
Using the Spring Initializr
If you have used Spring for a while, you are probably already familiar with the Spring Initializr. It is a web application that generates Spring Boot project skeletons (both Maven and Gradle). Fill out the project form fields, select your project dependencies, click the Generate button, and a zip file with the skeleton project will be created and downloaded.To use the Spring Initializer, you can open a browser and navigate to https://start.spring.io/.
In this example, we opted to create a Maven project and have added two dependencies:
- Spring Native [Experimental] - provides all the GraalVM Native Image goodness.
- Spring Web - provides REST support using Spring MVC.
Here we see the Maven POM file. If we click on the src directory, we will see the application's package structure use the Package Name we supplied in the corresponding Spring Initializr field. We can also see that the initializer has created an entry point class DemoApplication.
The DemoApplication class contains the main method required to launch the application.
Once we are happy with the configuration of our project, we can simply click the Generate button, and the initializer will zip up the project contents and download it. Once downloaded, we can start the development process with a configured skeleton project.
Zoned Date Time Controller
In our example, we will build a simple REST endpoint that returns the current date and time as a ZonedDateTimeDetail instance based on a given timezone. It will consist of two methods: getZoneIds and getZonedDateTime.Zoned Date Time Detail class
The ZonedDateTimeDetail class is a simple data transfer object that is returned from the getZonedDateTime call. It contains three fields:- ZoneId - which was supplied by the caller.
- zonedDateTime - the current time with calculated offset.
- error - if not null (no error), contains any error message generated during the creation of the getZonedDateTime method.
public class ZonedDateTimeDetail{
private String zoneId;
private ZonedDateTime zonedDateTime;
private String error;
ZonedDateTimeDetail(String zoneId) {
this.zoneId=zoneId;
try{
this.zonedDateTime = ZonedDateTime.now(ZoneId.of(zoneId));
}catch(DateTimeException ex){
this.error = ex.getMessage();
}
}
public String getError(){ return this.error;}
public String getZoneId(){ return this.zoneId;}
public ZonedDateTime getDateTime(){ return this.zonedDateTime;}
}
The constructor wraps the ZonedDateTime.now(...) method, trapping any exceptions and setting the appropriate member variables.
get Zone Ids
The getZoneIds method wraps the ZoneId.getAvailableZoneIds() method from the java.time package. The controller exposes the method with the @GetMapping("/zones") mapping.
@GetMapping("/zones")
public Set
getZoneIds(){
return ZoneId.getAvailableZoneIds();
}
get Zoned Date Time
The getZonedDateTime method captures the caller-supplied zone value from the zone @RequestParam and passes it into the ZonedDateTimeDetail constructor. The controller method is exposed with the @GetMapping("/zonedDateTime") mapping.
@GetMapping("/zonedDateTime")
public ZonedDateTimeDetail getZonedDateTime(@RequestParam(value = "zone", defaultValue = "US/Eastern") String zoneId) {
return new ZonedDateTimeDetail(zoneId);
}
Zoned Date Time Controller Class
Here is the controller in its entirety.
package com.thinkmicroservices.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.util.Set;
import java.time.DateTimeException;
@RestController
public class ZonedDateTimeController {
@GetMapping("/zones")
public Set
getZoneIds(){
return ZoneId.getAvailableZoneIds();
}
@GetMapping("/zonedDateTime")
public ZonedDateTimeDetail getZonedDateTime(@RequestParam(value = "zone", defaultValue = "US/Eastern") String zoneId) {
return new ZonedDateTimeDetail(zoneId);
}
public class ZonedDateTimeDetail{
private String zoneId;
private ZonedDateTime zonedDateTime;
private String error;
ZonedDateTimeDetail(String zoneId) {
this.zoneId=zoneId;
try{
this.zonedDateTime = ZonedDateTime.now(ZoneId.of(zoneId));
}catch(DateTimeException ex){
this.error = ex.getMessage();
}
}
public String getError(){ return this.error; }
public String getZoneId(){ return this.zoneId; }
public ZonedDateTime getDateTime(){ return this.zonedDateTime; }
}
}
As you can see, this is a pretty generic
Spring controller. We have made no provisions in the code for
GraalVM native image support.
Building The Native Image Container
Now that we have created our application, we can use the Spring Native build command:
mvn spring-boot:build-image
Now find some way to entertain yourself for the next 10-15 minutes while the native image builds.
Once the build completes, we can look at its artifacts.
Jar Image
After the dust has settled, we can look at the Maven target directory and see that two jar files were created. The first one is the small demo-0.0.1-SNAPSHOT.jar.original at 43.3KB. It contains only the compiled source code from the project. A second and considerably larger demo-0.0.1-SNAPSHOT.jar file ( 17.5MB) file was also created. The larger file is the standard Executable Jar. If we crack it open, we find two directories and two files. The classes directory contains the class code used to bootstrap the application along with its empty application.properties file. The library directory contains a collection of jars that are required by the application. The classpath.idx file contains the listing of all the dependent library jars:
- "BOOT-INF/lib/spring-boot-2.5.1.jar"
- "BOOT-INF/lib/spring-boot-autoconfigure-2.5.1.jar"
- "BOOT-INF/lib/logback-classic-1.2.3.jar"
- "BOOT-INF/lib/logback-core-1.2.3.jar"
- "BOOT-INF/lib/log4j-to-slf4j-2.14.1.jar"
- "BOOT-INF/lib/log4j-api-2.14.1.jar"
- "BOOT-INF/lib/jul-to-slf4j-1.7.30.jar"
- "BOOT-INF/lib/jakarta.annotation-api-1.3.5.jar"
- "BOOT-INF/lib/snakeyaml-1.28.jar"
- "BOOT-INF/lib/jackson-databind-2.12.3.jar"
- "BOOT-INF/lib/jackson-annotations-2.12.3.jar"
- "BOOT-INF/lib/jackson-core-2.12.3.jar"
- "BOOT-INF/lib/jackson-datatype-jdk8-2.12.3.jar"
- "BOOT-INF/lib/jackson-datatype-jsr310-2.12.3.jar"
- "BOOT-INF/lib/jackson-module-parameter-names-2.12.3.jar"
- "BOOT-INF/lib/tomcat-embed-core-9.0.46.jar"
- "BOOT-INF/lib/tomcat-embed-el-9.0.46.jar"
- "BOOT-INF/lib/tomcat-embed-websocket-9.0.46.jar"
- "BOOT-INF/lib/spring-web-5.3.8.jar"
- "BOOT-INF/lib/spring-beans-5.3.8.jar"
- "BOOT-INF/lib/spring-webmvc-5.3.8.jar"
- "BOOT-INF/lib/spring-aop-5.3.8.jar"
- "BOOT-INF/lib/spring-context-5.3.8.jar"
- "BOOT-INF/lib/spring-expression-5.3.8.jar"
- "BOOT-INF/lib/spring-native-0.10.0.jar"
- "BOOT-INF/lib/slf4j-api-1.7.30.jar"
- "BOOT-INF/lib/spring-core-5.3.8.jar"
- "BOOT-INF/lib/spring-jcl-5.3.8.jar"
- "BOOT-INF/lib/spring-boot-jarmode-layertools-2.5.1.jar"
The
layers.idx file contains a list of layers and the parts of the jar that should be contained within them. The layers are listed in the order that they should be added to the Docker/OCI image.
"dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
Application Container Image
In addition to building our application, the mvn spring-boot:build-image also builds our native-image and container image. However, if you are looking for the application's container image, you'll need to use Docker to find it. Use the following command:
docker images demo
You should see a listing like:
REPOSITORY TAG IMAGE ID CREATED SIZE demo 0.0.1-SNAPSHOT 85f30d415672 41 years ago 105MB
You may be scratching your head wondering how the mvn spring-boot:build-image command was able to travel back in time 41 years to build the container image? This temporal anomaly has a reasonable explanation. Under the covers, Spring Native buildpacks employ Packeto, which in turn uses the Pack tool. To ensure that we have Reproducible Builds, Pack chose January 1, 1980, a fixed build date. This date won't change between invocations. Having a consistent date is important since the date is part of the container image's metadata. This metadata is used to calculate the image's sha/digest, also known as the Image Id.
The date isn't arbitrary. It was deliberately chosen to extend support for MS-Dos and zip files whose epoch date is January 1,1980. As explained by one of Pack's developers:
The motivation … is therefore to allow verification that no vulnerabilities or backdoors have been introduced during this compilation process. By promising identical results are always generated from a given source, this allows multiple third parties to come to a consensus on a “correct” result, highlighting any deviations as suspect and worthy of scrutiny.
After that lengthy digression, we can see that the entire container image is 105 MB.
You might be wondering, "How big is the native image executable?". Unfortunately, we must look into our container image to get this information. To do this, we will use Dive, to peer into our container. Dive is a tool for exploring a docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.", With Dive, we can inspect the actual filesystem of our container image.
Dive installation instructions can be found here.
Once installed, we can launch Dive with:
dive demo:0.0.1-SNAPSHOT
You should see the Dive user interface looking similar to this:
The left side of the interface displays the container layers, and the right side shows the current layer contents. We can use the Tab key to switch between the Layers and the Current Layer Contents pane. The Arrow Up, Arrow Down, Page Down, Page Down keys allow us to traverse the container layers and the file system directories.
In the Layers window, we navigate down to the 4th layer from the top. We can then Tab to the Current Layer Contents and Page Down to the last directory- Workspace.
We can expand the directory using the spacebar to see its contents.
In this directory, we find our native image executable: com.thinkmicroservices.demo.DemoApplication. We can now see that its file size is 82MB. While this is several times larger than our jar file, it contains the entire application, all its dependencies, the supporting SubstrateVM, and the pre-initialized heap. Of course, file size is misleading. What we really want to see is how much memory is used by the service when it is running.
Running The application Jar
If we want to compare the startup-time performance of the native image startup, we must first run the application using the JVM to get a baseline metric. We can do this with:
java -jar ./demo-0.0.1-SNAPSHOT.jar
Your output should be similar to the following:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.1)
2021-03-17 12:07:45.666 INFO 15385 --- [ main] c.t.demo.DemoApplication : Starting DemoApplication v0.0.1-SNAPSHOT using Java 16.0.1 on ubuntu-vm with PID 15385 (/home/cwoodward/native-image/demo/target/demo-0.0.1-SNAPSHOT.jar started by cwoodward in /home/cwoodward/native-image/demo/target)
2021-03-17 12:07:45.669 INFO 15385 --- [ main] c.t.demo.DemoApplication : No active profile set, falling back to default profiles: default
2021-03-17 12:07:49.444 INFO 15385 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-03-17 12:07:49.469 INFO 15385 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2021-03-17 12:07:49.470 INFO 15385 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.46]
2021-03-17 12:07:49.620 INFO 15385 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2021-03-17 12:07:49.620 INFO 15385 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 3721 ms
2021-03-17 12:07:51.363 INFO 15385 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-03-17 12:07:51.432 INFO 15385 --- [ main] c.t.demo.DemoApplication : Started DemoApplication in 9.321 seconds (JVM running for 12.779)
Startup Time
So we see that this fairly simple service took 12.779 seconds to startup. The JVM took 3.458 seconds and an additional 9.321 seconds for the application.Memory
We can get the memory usage using a combination of ps, awk, and grep to find our process, convert its memory usage to MB, and filter by command name:ps -eo size,pid,user,command --sort -size | \ awk '{ hr=$1/1024 ; printf("%13.2f Mb ",hr) } { for ( x=4 ; x<=NF ; x++ ) { printf("%s ",$x) } print "" }' | \ grep -i "java -jar ./demo-0.0.1-SNAPSHOT.jar"
You should see output similar to this:
0.1 0.0 580.65 Mb java -jar ./demo-0.0.1-SNAPSHOT.jarHere we see the the JVM version is consuming 580.65MB
Exercising the service
Now that we have a working service, let's call its methods. We will start with /zones.
curl http://localhost:8080/zones
This response, unsurprisingly, contains an array of time zones.
["Asia/Aden","America/Cuiaba","Etc/GMT+9","Etc/GMT+8","Africa/Nairobi","America/Marigot", "Asia/Aqtau","Pacific/Kwajalein","America/El_Salvador","Asia/Pontianak","Africa/Cairo","Pacific/Pago_Pago","Africa/Mbabane","Asia/Kuching","Pacific/Honolulu","Pacific/Rarotonga","America/Guatemala","Australia/Hobart","Europe/London","America/Belize","America/Panama","Asia/Chungking","America/Managua","America/Indiana/Petersburg","Asia/Yerevan","Europe/Brussels","GMT","Europe/Warsaw","America/Chicago","Asia/Kashgar","Chile/Continental","Pacific/Yap","CET","Etc/GMT-1","Etc/GMT-0","Europe/Jersey","America/Tegucigalpa","Etc/GMT-5","Europe/Istanbul","America/Eirunepe","Etc/GMT-4","America/Miquelon","Etc/GMT-3","Europe/Luxembourg","Etc/GMT-2","Etc/GMT-9","America/Argentina/Catamarca","Etc/GMT-8","Etc/GMT-7","Etc/GMT-6","Europe/Zaporozhye","Canada/Yukon","Canada/Atlantic","Atlantic/St_Helena","Australia/Tasmania","Libya","Europe/Guernsey","America/Grand_Turk","Asia/Samarkand","America/Argentina/Cordoba","Asia/Phnom_Penh","Africa/Kigali","Asia/Almaty","US/Alaska","Asia/Dubai","Europe/Isle_of_Man","America/Araguaina","Cuba","Asia/Novosibirsk","America/Argentina/Salta","Etc/GMT+3","Africa/Tunis","Etc/GMT+2","Etc/GMT+1","Pacific/Fakaofo","Africa/Tripoli","Etc/GMT+0","Israel","Africa/Banjul","Etc/GMT+7","Indian/Comoro","Etc/GMT+6","Etc/GMT+5","Etc/GMT+4","Pacific/Port_Moresby","US/Arizona","Antarctica/Syowa","Indian/Reunion","Pacific/Palau","Europe/Kaliningrad","America/Montevideo","Africa/Windhoek","Asia/Karachi","Africa/Mogadishu","Australia/Perth","Brazil/East","Etc/GMT","Asia/Chita","Pacific/Easter","Antarctica/Davis","Antarctica/McMurdo","Asia/Macao","America/Manaus","Africa/Freetown","Europe/Bucharest","Asia/Tomsk","America/Argentina/Mendoza","Asia/Macau","Europe/Malta","Mexico/BajaSur","Pacific/Tahiti","Africa/Asmera","Europe/Busingen","America/Argentina/Rio_Gallegos","Africa/Malabo","Europe/Skopje","America/Catamarca","America/Godthab","Europe/Sarajevo","Australia/ACT","GB-Eire","Africa/Lagos","America/Cordoba","Europe/Rome","Asia/Dacca","Indian/Mauritius","Pacific/Samoa","America/Regina","America/Fort_Wayne","America/Dawson_Creek","Africa/Algiers","Europe/Mariehamn","America/St_Johns","America/St_Thomas","Europe/Zurich","America/Anguilla","Asia/Dili","America/Denver","Africa/Bamako","Europe/Saratov","GB","Mexico/General","Pacific/Wallis","Europe/Gibraltar","Africa/Conakry","Africa/Lubumbashi","Asia/Istanbul","America/Havana","NZ-CHAT","Asia/Choibalsan","America/Porto_Acre","Asia/Omsk","Europe/Vaduz","US/Michigan","Asia/Dhaka","America/Barbados","Europe/Tiraspol","Atlantic/Cape_Verde","Asia/Yekaterinburg","America/Louisville","Pacific/Johnston","Pacific/Chatham","Europe/Ljubljana","America/Sao_Paulo","Asia/Jayapura","America/Curacao","Asia/Dushanbe","America/Guyana","America/Guayaquil","America/Martinique","Portugal","Europe/Berlin","Europe/Moscow","Europe/Chisinau","America/Puerto_Rico","America/Rankin_Inlet","Pacific/Ponape","Europe/Stockholm","Europe/Budapest","America/Argentina/Jujuy","Australia/Eucla","Asia/Shanghai","Universal","Europe/Zagreb","America/Port_of_Spain","Europe/Helsinki","Asia/Beirut","Asia/Tel_Aviv","Pacific/Bougainville","US/Central","Africa/Sao_Tome","Indian/Chagos","America/Cayenne","Asia/Yakutsk","Pacific/Galapagos","Australia/North","Europe/Paris","Africa/Ndjamena","Pacific/Fiji","America/Rainy_River","Indian/Maldives","Australia/Yancowinna","SystemV/AST4","Asia/Oral","America/Yellowknife","Pacific/Enderbury","America/Juneau","Australia/Victoria","America/Indiana/Vevay","Asia/Tashkent","Asia/Jakarta","Africa/Ceuta","Asia/Barnaul","America/Recife","America/Buenos_Aires","America/Noronha","America/Swift_Current","Australia/Adelaide","America/Metlakatla","Africa/Djibouti","America/Paramaribo","Asia/Qostanay","Europe/Simferopol","Europe/Sofia","Africa/Nouakchott","Europe/Prague","America/Indiana/Vincennes","Antarctica/Mawson","America/Kralendijk","Antarctica/Troll","Europe/Samara","Indian/Christmas","America/Antigua","Pacific/Gambier","America/Indianapolis","America/Inuvik","America/Iqaluit","Pacific/Funafuti","UTC","Antarctica/Macquarie","Canada/Pacific","America/Moncton","Africa/Gaborone","Pacific/Chuuk","Asia/Pyongyang","America/St_Vincent","Asia/Gaza","Etc/Universal","PST8PDT","Atlantic/Faeroe","Asia/Qyzylorda","Canada/Newfoundland","America/Kentucky/Louisville","America/Yakutat","Asia/Ho_Chi_Minh","Antarctica/Casey","Europe/Copenhagen","Africa/Asmara","Atlantic/Azores","Europe/Vienna","ROK","Pacific/Pitcairn","America/Mazatlan","Australia/Queensland","Pacific/Nauru","Europe/Tirane","Asia/Kolkata","SystemV/MST7","Australia/Canberra","MET","Australia/Broken_Hill","Europe/Riga","America/Dominica","Africa/Abidjan","America/Mendoza","America/Santarem","Kwajalein","America/Asuncion","Asia/Ulan_Bator","NZ","America/Boise","Australia/Currie","EST5EDT","Pacific/Guam","Pacific/Wake","Atlantic/Bermuda","America/Costa_Rica","America/Dawson","Asia/Chongqing","Eire","Europe/Amsterdam","America/Indiana/Knox","America/North_Dakota/Beulah","Africa/Accra","Atlantic/Faroe","Mexico/BajaNorte","America/Maceio","Etc/UCT","Pacific/Apia","GMT0","America/Atka","Pacific/Niue","Australia/Lord_Howe","Europe/Dublin","Pacific/Truk","MST7MDT","America/Monterrey","America/Nassau","America/Jamaica","Asia/Bishkek","America/Atikokan","Atlantic/Stanley","Australia/NSW","US/Hawaii","SystemV/CST6","Indian/Mahe","Asia/Aqtobe","America/Sitka","Asia/Vladivostok","Africa/Libreville","Africa/Maputo","Zulu","America/Kentucky/Monticello","Africa/El_Aaiun","Africa/Ouagadougou","America/Coral_Harbour","Pacific/Marquesas","Brazil/West","America/Aruba","America/North_Dakota/Center","America/Cayman","Asia/Ulaanbaatar","Asia/Baghdad","Europe/San_Marino","America/Indiana/Tell_City","America/Tijuana","Pacific/Saipan","SystemV/YST9","Africa/Douala","America/Chihuahua","America/Ojinaga","Asia/Hovd","America/Anchorage","Chile/EasterIsland","America/Halifax","Antarctica/Rothera","America/Indiana/Indianapolis","US/Mountain","Asia/Damascus","America/Argentina/San_Luis","America/Santiago","Asia/Baku","America/Argentina/Ushuaia","Atlantic/Reykjavik","Africa/Brazzaville","Africa/Porto-Novo","America/La_Paz","Antarctica/DumontDUrville","Asia/Taipei","Antarctica/South_Pole","Asia/Manila","Asia/Bangkok","Africa/Dar_es_Salaam","Poland","Atlantic/Madeira","Antarctica/Palmer","America/Thunder_Bay","Africa/Addis_Ababa","Asia/Yangon","Europe/Uzhgorod","Brazil/DeNoronha","Asia/Ashkhabad","Etc/Zulu","America/Indiana/Marengo","America/Creston","America/Punta_Arenas","America/Mexico_City","Antarctica/Vostok","Asia/Jerusalem","Europe/Andorra","US/Samoa","PRC","Asia/Vientiane","Pacific/Kiritimati","America/Matamoros","America/Blanc-Sablon","Asia/Riyadh","Iceland","Pacific/Pohnpei","Asia/Ujung_Pandang","Atlantic/South_Georgia","Europe/Lisbon","Asia/Harbin","Europe/Oslo","Asia/Novokuznetsk","CST6CDT","Atlantic/Canary","America/Knox_IN","Asia/Kuwait","SystemV/HST10","Pacific/Efate","Africa/Lome","America/Bogota","America/Menominee","America/Adak","Pacific/Norfolk","Europe/Kirov","America/Resolute","Pacific/Tarawa","Africa/Kampala","Asia/Krasnoyarsk","Greenwich","SystemV/EST5","America/Edmonton","Europe/Podgorica","Australia/South","Canada/Central","Africa/Bujumbura","America/Santo_Domingo","US/Eastern","Europe/Minsk","Pacific/Auckland","Africa/Casablanca","America/Glace_Bay","Canada/Eastern","Asia/Qatar","Europe/Kiev","Singapore","Asia/Magadan","SystemV/PST8","America/Port-au-Prince","Europe/Belfast","America/St_Barthelemy","Asia/Ashgabat","Africa/Luanda","America/Nipigon","Atlantic/Jan_Mayen","Brazil/Acre","Asia/Muscat","Asia/Bahrain","Europe/Vilnius","America/Fortaleza","Etc/GMT0","US/East-Indiana","America/Hermosillo","America/Cancun","Africa/Maseru","Pacific/Kosrae","Africa/Kinshasa","Asia/Kathmandu","Asia/Seoul","Australia/Sydney","America/Lima","Australia/LHI","America/St_Lucia","Europe/Madrid","America/Bahia_Banderas","America/Montserrat","Asia/Brunei","America/Santa_Isabel","Canada/Mountain","America/Cambridge_Bay","Asia/Colombo","Australia/West","Indian/Antananarivo","Australia/Brisbane","Indian/Mayotte","US/Indiana-Starke","Asia/Urumqi","US/Aleutian","Europe/Volgograd","America/Lower_Princes","America/Vancouver","Africa/Blantyre","America/Rio_Branco","America/Danmarkshavn","America/Detroit","America/Thule","Africa/Lusaka","Asia/Hong_Kong","Iran","America/Argentina/La_Rioja","Africa/Dakar","SystemV/CST6CDT","America/Tortola","America/Porto_Velho","Asia/Sakhalin","Etc/GMT+10","America/Scoresbysund","Asia/Kamchatka","Asia/Thimbu","Africa/Harare","Etc/GMT+12","Etc/GMT+11","Navajo","America/Nome","Europe/Tallinn","Turkey","Africa/Khartoum","Africa/Johannesburg","Africa/Bangui","Europe/Belgrade","Jamaica","Africa/Bissau","Asia/Tehran","WET","Europe/Astrakhan","Africa/Juba","America/Campo_Grande","America/Belem","Etc/Greenwich","Asia/Saigon","America/Ensenada","Pacific/Midway","America/Jujuy","Africa/Timbuktu","America/Bahia","America/Goose_Bay","America/Virgin","America/Pangnirtung","Asia/Katmandu","America/Phoenix","Africa/Niamey","America/Whitehorse","Pacific/Noumea","Asia/Tbilisi","America/Montreal","Asia/Makassar","America/Argentina/San_Juan","Hongkong","UCT","Asia/Nicosia","America/Indiana/Winamac","SystemV/MST7MDT","America/Argentina/ComodRivadavia","America/Boa_Vista","America/Grenada", "Asia/Atyrau","Australia/Darwin","Asia/Khandyga","Asia/Kuala_Lumpur","Asia/Famagusta","Asia/Thimphu","Asia/Rangoon","Europe/Bratislava","Asia/Calcutta","America/Argentina/Tucuman","Asia/Kabul","Indian/Cocos","Japan","Pacific/Tongatapu","America/New_York","Etc/GMT-12","Etc/GMT-11","America/Nuuk","Etc/GMT-10","SystemV/YST9YDT","Europe/Ulyanovsk","Etc/GMT-14","Etc/GMT-13","W-SU","America/Merida","EET","America/Rosario","Canada/Saskatchewan","America/St_Kitts","Arctic/Longyearbyen","America/Fort_Nelson","America/Caracas","America/Guadeloupe","Asia/Hebron","Indian/Kerguelen","SystemV/PST8PDT","Africa/Monrovia","Asia/Ust-Nera","Egypt","Asia/Srednekolymsk","America/North_Dakota/New_Salem","Asia/Anadyr","Australia/Melbourne","Asia/Irkutsk","America/Shiprock","America/Winnipeg","Europe/Vatican","Asia/Amman","Etc/UTC","SystemV/AST4ADT","Asia/Tokyo","America/Toronto","Asia/Singapore","Australia/Lindeman","America/Los_Angeles","SystemV/EST5EDT","Pacific/Majuro","America/Argentina/Buenos_Aires","Europe/Nicosia","Pacific/Guadalcanal","Europe/Athens","US/Pacific","Europe/Monaco"]
Who knew there were so many time zones?. Now that we have a list of valid time zones, let's check the time in Prague using the "Europe/Prague" zone.
curl localhost:8080/zonedDateTime?zone=Europe/Prague
The response should contain a JSON object containing the date:
{
"zoneId":"Europe/Prague",
"dateTime":"2021-03-23T16:54:40.088989914+02:00",
"error":null
}
Now let's call the method with an invalid zone:
curl localhost:8080/zonedDateTime?zone=Neptune
Here we see the response now contains an error message and the dateTime value is null.
{
"zoneId":"Neptune",
"dateTime":null,
"error":"Unknown time-zone ID: Neptune"
}
So now that we have verified the service is working in the JVM, we can get to the interesting part.
Running The Native Image Container
Now let's run the native image in our container:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
We now see the application starts up nearly instantly.
2021-03-17 02:27:04.367 INFO 1 --- [ main] o.s.nativex.NativeListener : This application is bootstrapped with code generated with Spring AOT
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.1)
2021-03-17 02:27:04.368 INFO 1 --- [ main] c.t.demo.DemoApplication : Starting DemoApplication using Java 11.0.11 on 41c6cd47d5cd with PID 1 (/workspace/com.thinkmicroservices.demo.DemoApplication started by cnb in /workspace)
2021-03-17 02:27:04.369 INFO 1 --- [ main] c.t.demo.DemoApplication : No active
profile set, falling back to default profiles: default
2021-03-17 02:27:04.633 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-03-17 02:27:04.636 INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 266 ms
March 17, 2021 2:27:04 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
March 17, 2021 2:27:04 AM org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
March 17, 2021 2:27:04 AM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/9.0.46]
March 17, 2021 2:27:04 AM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring embedded WebApplicationContext
March 17, 2021 2:27:04 AM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
2021-03-17 02:27:04.677 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '';
2021-03-17 02:27:04.678 INFO 1 --- [ main] c.t.demo.DemoApplication : Started DemoApplication in 0.135 seconds (JVM running for 0.137)
Startup time
We can see from the output that the application started in 0.137 seconds. If we compare that to the startup time for the non-native image version (12.779 seconds), we see that our native image executable starts roughly 93 times faster.Memory
To obtain the memory in our container will require we jump through a couple of hoops. The container generated by Spring Native is stripped down to make it as lightweight as is practica. Unfortunately, one of the casualties of this strip-down was the bash shell. Without the shell, we are unable to connect to the container to interrogate the application memory size. Fortunately, through the magic of Docker layer composition and multi-stage builds, we can create a new image that has both the native image executable and the bash shell. Our new Dockerfile looks like this:
# multi-stage build composite image
# containing the GraalVM
# native image executable
# AND the 'bash' shell.
# Spring-Native generated image
FROM demo:0.0.1-SNAPSHOT AS spring-native
# generic ubuntu bionic image
FROM ubuntu:bionic
# make sure we are up to date
RUN apt-get update
# Install BASH.
RUN apt-get install bash
# create a workspace directory
RUN mkdir /workspace
# copy the workspace folder from
COPY --from=spring-native /workspace /workspace
# start with the bash shell
CMD bin/bash
Let's create a new image with this
Dockerfile.
docker build -t bash-demo .
Now we can run our new image:
docker run -p 8080:8080 -it bash-demo:latest
You should now have a bash shell prompt looking similar to this:
root@64497eb774d7:/#
Navigate to the /workspace directory.
root@64497eb774d7:/# cd /workspace
Now check the contents.
root@64497eb774d7:/workspace ls -o
You should see the native image executable there.
root@64497eb774d7:/workspace ls -o
Now that we know our native image executable is there, we can run it.
root@64497eb774d7:/workspace# ./com.thinkmicroservices.demo.DemoApplication &
You should see the application launch:
2021-03-18 00:53:43.660 INFO 18 --- [ main] o.s.nativex.NativeListener : This application is bootstrapped with code generated with Spring AOT . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.1) 2021-03-18 00:53:43.661 INFO 18 --- [ main] c.t.demo.DemoApplication : Starting DemoApplication using Java 11.0.11 on 64497eb774d7 with PID 18 (/workspace/com.thinkmicroservices.demo.DemoApplication started by root in /workspace) 2021-03-18 00:53:43.661 INFO 18 --- [ main] c.t.demo.DemoApplication : No active profile set, falling back to default profiles: default 2021-03-18 00:53:43.747 INFO 18 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2021-03-18 12:53:43 AM org.apache.coyote.AbstractProtocol init INFO: Initializing ProtocolHandler ["http-nio-8080"] March 18, 2021 12:53:43 AM org.apache.catalina.core.StandardService startInternal INFO: Starting service [Tomcat] March 18, 2021 12:53:43 AM org.apache.catalina.core.StandardEngine startInternal INFO: Starting Servlet engine: [Apache Tomcat/9.0.46] March 18, 2021 12:53:43 AM org.apache.catalina.core.ApplicationContext log INFO: Initializing Spring embedded WebApplicationContext 2021-03-18 00:53:43.750 INFO 18 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 88 ms March 18, 2021 12:53:43 AM org.apache.coyote.AbstractProtocol start INFO: Starting ProtocolHandler ["http-nio-8080"] 2021-03-23 00:53:43.791 INFO 18 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2021-03-23 00:53:43.792 INFO 18 --- [ main] c.t.demo.DemoApplication : Started DemoApplication in 0.148 seconds (JVM running for 0.15)
Now that our app is running we can check the application size.
ps -eo size,pid,user,command --sort -size | \ awk '{ hr=$1/1024 ; printf("%13.2f Mb ",hr) } { for ( x=4 ; x<=NF ; x++ ) { printf("%s ",$x) } print "" }' | \ grep -i "com.thinkmicroservices.demo.DemoApplication"
Your results should be similar to this:
207.91 Mb ./workspace/com.thinkmicroservices.demo.DemoApplication
Here we see that our native image is 207.91MB, less than half the size of the JVM's 580.65 MB.
Twitter
Facebook
Reddit
LinkedIn
Email