Using Java Native Image

Page last updated:

This topic describes how to build Java apps with native image support and deploy those apps to Pivotal Application Service (PAS).

Overview

A growing number of Java users are building Java apps using support for native images. For more information, see Native Image in the GraalVM documentation.

Java users deploying to PAS can deploy apps compiled using native image support. However, the PAS Java buildpack does not provide support to build, compile, and turn apps into a native image. You must perform those steps before deploying to PAS.

To build your app in a way that is suitable for running on PAS, you can do one of the following:

Build Using Cloud Native Buildpacks

Cloud Native Buildpacks include support for building native image apps. You can pass in your source code or a compiled JAR and Cloud Native Buildpacks installs the required tools and builds a compatible image.

Example Build Using Cloud Native Buildpacks

  1. Clone the example repository from GitHub:

    git clone https://github.com/paketo-buildpacks/samples
    cd samples/java/native-image/java-native-image-sample
    
  2. Build the example image:

    ./mvnw package
    pack build apps/native-image -p target/demo-0.0.1-SNAPSHOT.jar -e BP_NATIVE_IMAGE=true -B paketobuildpacks/builder:tiny
    

For more information about building with Cloud Native Buildpacks, see Getting Started in the Spring Native documentation.

Deploy Using Cloud Native Buildpacks

This section describes how to deploy a Java app with native image support using Cloud Native Buildpacks.

To deploy an app compiled using the steps from the previous section to PAS, you can deploy the image directly using PAS’s Docker support. If Docker support is disabled, you can extract the native image binary from the container image and deploy the app using the binary buildpack.

Deploy from Image

To deploy the app from an image:

  1. Publish your image. You can do this in a number of ways. For example, you can use the --publish flag to pack build with docker tag and docker push, or through any other means of publishing a container image to a registry.

  2. Run cf push -o registry.example.com/apps/native-image native-image-app. This option requires that your foundation support deploying Docker images. To validate this, run the following command and confirm that diego_docker is set to enabled. If it is set to disabled, you cannot use this deployment option.

    > cf feature-flags
    Retrieving status of all flagged features as user@example.com...
    
    features                                      state
    user_org_creation                             disabled
    private_domain_creation                       enabled
    app_bits_upload                               enabled
    app_scaling                                   enabled
    route_creation                                enabled
    service_instance_creation                     enabled
    diego_docker                                  enabled
    set_roles_by_username                         enabled
    unset_roles_by_username                       enabled
    task_creation                                 enabled
    env_var_visibility                            enabled
    space_scoped_private_broker_creation          enabled
    space_developer_env_var_visibility            enabled
    service_instance_sharing                      enabled
    hide_marketplace_from_unauthenticated_users   disabled
    resource_matching                             enabled
    

Extract the Image and Deploy the Binary

To extract and deploy your app using the binary buildpack:

  1. Create a script to extract the files:

    #!/bin/bash
    
    if [ -z "$1" ] || [ -z "$2" ]; then
        echo "USAGE: extract.sh image-name full-main-class"
        echo
        exit 1
    fi
    
    CONTAINER_ID=$(docker create "$1")
    mkdir -p ./out
    docker cp "$CONTAINER_ID:/workspace/$2" "./out/$2"
    docker rm "$CONTAINER_ID" > /dev/null
    

    This script is optional, but illustrates the process of extracting the binary file. It runs docker create and docker cp to extract the file from the image, followed by docker rm to remove the containerd. You can do this manually, or you can use any other tools for interacting with a container image.

  2. Run the extract script:

    $ ./extract.sh apps/native-image io.paketo.demo.Demoapp
    $ file ./out/io.paketo.demo.Demoapp
    io.paketo.demo.Demoapp: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=05cb81992f3859c9653a9ef6a691e798a9c48b9b, with debug_info, not stripped
    

    The extract script places the binary in a directory called out under the current working directory. You can run file to examine it and confirm that it is a 64-bit Linux executable.

  3. Run cf push -b binary_buildpack -m 256M -p ./out -c ./io.paketo.demo.Demoapp native-image. This pushes the compiled binary and executes it. You can adjust other properties or use a manifest.yml file to deploy as well.

Build Using Native Build Tools

This section describes how to deploy a Java app with native image support using Native Build Tools. If you do not want to build using Cloud Native Buildpacks, you can build and compile directly using the Native Build Tools.

To build a Java app using Native Build Tools:

  1. Obtain an Ubuntu Bionic computer, VM, or container. Ubuntu Bionic is recommended for best compatibility with the PAS cflinuxfs3 root filesystem.

  2. Install GraalVM. See Get Started with GraalVM.

  3. Install the Java Native Image tools. See Install Native Image.

  4. To add the Native Build Tools to your Maven or Gradle project, follow the instructions in Add the native build tools plugin in the Spring documentation. This only needs to be done once.

  5. Clone the example repository from GitHub:

    git clone https://github.com/paketo-buildpacks/samples
    cd samples/java/native-image/java-native-image-sample
    
  6. Build the example image:

    mvn -Pnative -DskipTests package
    

For more information about building with Native Build Tools, see Getting started with Native Build Tools in the Spring documentation.

Deploy with Direct Build

This section describes how to deploy a Java app with native image support using a direct build.

To deploy a Java app with native image support using the binary buildpack:

  1. Zip the executable created by your build tool, either Maven or Gradle. For example, with Maven, zip demo.zip target/demo. Alternatively, you can copy the executable into a directory by itself. For example, with Maven, mkdir -p ./out && cp target/demo ./out/. This is the root for cf push.
  2. Run cf push -b binary_buildpack -p demo.zip -c ./target/demo native-image. This pushes the root zip or directory you created in the previous step. If you use a directory instead of a zip archive, adjust the -p argument. This is important so that it only uploads the compiled binary, not your entire project. You can adjust other properties or use a manifest.yml file to deploy as well.

Spring Boot and Spring Native

Regardless of how you build your app, with Spring Boot v2.5 and Spring Native v0.10.1, there is a bug that causes cf push to fail. The problem is caused by conditional behavior in Spring Boot Actuator when run on PAS. It is fixed in Spring Native v0.10.2.

For more information, see the spring-projects-experimental repository on GitHub.

You can also work around this issue by adding the following hints above your @SpringBootapp annotation:

For example:

  @NativeHint(trigger = ReactivePASActuatorAutoConfiguration.class, types = {
          @TypeHint(types = EndpointPASExtension.class, access = AccessBits.ANNOTATION),
          @TypeHint(typeNames = "org.springframework.boot.actuate.autoconfigure.PAS.PASEndpointFilter"),
          @TypeHint(typeNames = "org.springframework.boot.actuate.autoconfigure.PAS.reactive.PASWebFluxEndpointHandlerMapping$PASLinksHandler", access = AccessBits.LOAD_AND_CONSTRUCT
                  | AccessBits.DECLARED_METHODS) })
  @NativeHint(trigger = PASActuatorAutoConfiguration.class, types = {
          @TypeHint(types = EndpointPASExtension.class, access = AccessBits.ANNOTATION),
          @TypeHint(typeNames = "org.springframework.boot.actuate.autoconfigure.PAS.PASEndpointFilter"),
          @TypeHint(typeNames = "org.springframework.boot.actuate.autoconfigure.PAS.servlet.PASWebEndpointServletHandlerMapping$PASLinksHandler", access = AccessBits.LOAD_AND_CONSTRUCT
                  | AccessBits.DECLARED_METHODS) })
  @SpringBootapp
  public class Demoapp {

      public static void main(String[] args) {
          Springapp.run(Demoapp.class, args);
      }

  }