Spring Native

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
This collection provides developers with a comprehensive collection of libraries for building native image applications.

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
Even though it is a beta release, we still get fast startup (typically < 100ms) and lower memory consumption. There is, of course, no free lunch. The reduction in startup time and memory footprint comes at the cost of a more complex configuration and longer build process.

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/.

Spring Initializer

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.
We can explore the structure of the application prior to generation by clicking the EXPLORE button.

Spring Initializer Explorer

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.

Spring Initializer Entry Point Class

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.

    via GIPHY

    Our build gets longer since the AOT compiler must shoulder the burden traditionally handled by the JVM at runtime. In addition to the longer build times, the AOT compiler is resource hungry. Expect it to consume a lot of CPU and memory resources while it builds the application. Since the native-image build times will always be longer, you need to consider building them only when necessary (e.g., performance testing, production releases, etc.). Unless you are willing to absorb the extended build times, it is not recommended to generate a native-image as part of a continuous integration process where a build is triggered for each code check-in.

    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:

    Dive Home

    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.

    Dive workspace

    We can expand the directory using the spacebar to see its contents.

    Dive workspace expanded

    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.jar 
    
    
    Here 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.

    Conclusions

    We can see from this example that Spring Native gives us a significant reduction in bot service startup time and memory footprint. While Spring Native is still only a Beta release and doesn't currently support all of Spring's various subprojects, its integration of the GraalVM's native image generation has had a significant impact.

    Resources

    Coming up

    In our next article, we will take a look at NATS, an open-source messaging system that is part of the CNCF project portfolio.