×
Community Blog Towards Native: Examples and Principles of Spring and Dubbo AOT Technology

Towards Native: Examples and Principles of Spring and Dubbo AOT Technology

This article discusses Java application challenges in the cloud era, GraalVM Native Image solutions, and the principles of GraalVM.

By Jun Liu

In the era of cloud computing, Java applications face challenges such as slow "cold start," high memory usage, and long warm-up time, which impede their adaptability to cloud deployment modes like Serverless. GraalVM addresses these issues by leveraging static compilation, packaging, and other technologies. Additionally, popular frameworks like Spring and Dubbo offer compatible Ahead-of-Time (AOT) solutions to accommodate GraalVM's usage limitations.

This article provides a comprehensive analysis of the challenges Java applications encounter in the cloud era, the solutions offered by GraalVM Native Image, and the fundamental concepts and working principles of GraalVM. Furthermore, it demonstrates the process of statically packaging a typical microservices application using the Spring 6 and Dubbo 3 frameworks.

This article is divided into the following four parts:

  1. First, we explore the features that cloud applications should possess and the challenges faced by Java applications in the cloud environment, considering the rapid development of cloud computing.
  2. Next, we introduce GraalVM and Native Image, explaining how to utilize GraalVM to statically compile Java applications into executable binary programs.
  3. We know that GraalVM imposes certain usage restrictions, such as the lack of support for dynamic features like Java reflection. To overcome these limitations, we delve into the necessary Metadata configurations, highlighting the implementation of AOT processing in frameworks like Spring 6 and Dubbo 3.
  4. Lastly, we demonstrate the static packaging of a Java application by providing an example of a Spring 6 and Dubbo 3 microservices application.

Challenges for Java Applications in the Cloud Era

First, let's take a look at the application characteristics in the cloud computing era and the challenges that Java faces in the cloud era. According to various statistical agencies, Java remains one of the most popular programming languages for developers today, second only to certain scripting languages. Java enables efficient development of business applications, thanks to its rich ecosystem, which enhances productivity and operational efficiency. Consequently, numerous applications have been developed using the Java language.

1

However, in the era of cloud computing, Java applications encounter several deployment and operational challenges. Let's consider Serverless as an example. Serverless is an increasingly prevalent deployment model in the cloud that allows developers to focus on business logic and address resource issues through rapid elasticity. However, Java's representation in the Serverless runtime across various cloud computing vendors is relatively small, significantly lower than its prevalence in traditional application development.

2

The main reason for the situation is that Java applications cannot meet the key requirements of Serverless scenarios.

First, Java has a relatively long cold start time. This is a major challenge for Serverless scenarios where fast bounce is required because the pull-up time of Java applications may be seconds or tens of seconds.

Second, Java applications require warm-up time to achieve optimal performance. It is inappropriate to allocate a large amount of traffic to applications that have just been pulled up because there will be problems such as request timeout and excessive resource occupation, further extending the effective startup time of Java applications.

Third, Java applications have high demands for the running environment. It often requires a lot of memory and computing resources. However, instead of being allocated to the business needs, most of them are consumed by the JVM runtime, which contradicts the goal of cost reduction and efficiency improvement in the cloud.

• Finally, Java applications produce large packages or images, affecting overall storage and retrieval efficiency.

Next, let's take a specific look at how GraalVM, a packaging and runtime technology, solves these problems faced by Java applications.

Introduction to GraaIVM

GraalVM compiles your Java applications ahead of time into standalone binaries that start instantly, provide peak performance with no warmup, and use fewer resources.

According to the official introduction, GraalVM provides AOT compilation and binary packaging capabilities for Java applications. Binary packages based on GraalVM have the following advantages: fast startup, ultra-high performance, no warm-up time, and little resource consumption. The AOT mentioned here is a technical abbreviation that occurs during compilation, namely Ahead-of-time, and I will talk about it later. In general, GraalVM can be divided into two parts.

• First, GraalVM is a complete JDK release version, which is equivalent to OpenJDK and can run applications developed in any JVM language.

• Secondly, GraalVM provides the Native Image packaging technology, which can package applications into binary packages that can run independently. This package is a self-contained application that can run separately from the JVM.

3

As shown in the above figure, the GraalVM compiler provides two modes: JIT and AOT.

JIT mode: We all know that Java classes will be compiled into files in the .class format, which have become the bytecode recognized by JVM after compilation. When Java applications are running, the JIT compiler compiles some bytecode on hot paths into machine code to achieve faster execution speed.

AOT mode: AOT directly converts bytecode into machine code during compilation, eliminating the dependency of runtime on JVM. Because JVM loading and bytecode runtime warm-up time is saved, the programs compiled and packaged by AOT have high runtime efficiency.

4

In general, the JIT mode enables applications to have higher limit processing capability. It can reduce the key metric, the maximum latency of requests. The AOT mode can further improve the cold start speed of applications, have smaller binary package sizes, and require less memory and other resources in the running state.

What Is Native Image?

We have mentioned the concept of Native Image in GraalVM many times above. Native Image is a technology that compiles Java code and packages it into executable binary programs. The produced package only contains the code required at the runtime, including the application's code, standard dependency package, language runtime, and static code associated with the JDK library. This package no longer relies on the JVM environment for execution. However, it is tied to specific machine environments and requires separate packaging for different machine environments.

Native Image has the following features:

• Contains only a portion of the resources needed to run the JVM, so the running cost is lower
• Starts in milliseconds
• Enters the best state without warming up after starting
• Supports to be packaged as a lighter binary package, making deployment faster and more efficient
• More secure

5

To sum up, Native Image provides faster startup speed, less resource usage, less risk of security vulnerabilities, and a more compact binary package size. It effectively addresses prominent challenges faced by Java applications in cloud computing scenarios like Serverless.

The Basic Principle and Use of GraaIVM Native Image

Next, let's take a look at the basic usage of GraalVM. First, you need to install the relevant basic dependencies required by native-image, which may vary depending on the operating system environment. Next, you can use the GraalVM JDK downloader to download native-image. Once everything is installed, you can proceed to compile and package Java applications using the native-image command. The input can be class files, jar files, Java modules, etc., which will ultimately be packaged into an executable file capable of independent execution, such as the HelloWorld example. Additionally, GraalVM provides Maven and Gradle build tool plugins that facilitate the packaging process.

6

GraalVM, based on a concept called "closed world assumption", requires that all runtime resources and behaviors of a program are completely determined during compilation. The figure shows the specific compilation and packaging process of AOT. The application code, dependencies, JDK, etc., on the left are input. GraalVM uses the main function as the entry to scan all reachable codes and execution paths. Some pre-initialization actions may be involved in processing. Finally, the machine code, initialization resources, and other state data compiled by AOT are packaged as executable Native packages.

Compared to the traditional JVM deployment mode, the GraalVM Native Image mode is very different.

• GraalVM uses the main function as the entry during building and compiling to complete static application code analysis.

• Code that cannot be reached during static analysis is removed and not included in the final binary package.

• GraalVM cannot recognize some dynamic calling behaviors in the code, such as reflection, resource loading, serialization, and dynamic proxy, which are all limited.

• Classpath is solidified during building and cannot be modified.

• Delayed class loading is no longer supported, and all available classes and code are determined at the program startup stage.

• Some other Java application capabilities are limited, such as ahead-of-time class initialization.

GraalVM does not support dynamic features like reflection, which are extensively used in many applications and frameworks. To package these applications as Native Images and make them static, GraalVM provides a metadata configuration entry. By providing configuration files for all dynamic features, the "closed world assumption" mode remains valid, allowing GraalVM to know all expected behaviors at compile time.

There are two examples:

1.  The encoding method, such as the encoding method of reflection here, allows GraalVM to analyze and calculate metadata through code.

7
8

2.  Another example is to provide an additional json configuration file and place it under the specified directory: META-INF/native-image//.

9

AOT Processing

Using dynamic features such as reflection in Java applications or frameworks is an obstacle to using GraalVM. However, a large number of frameworks have this limitation. It will be a very challenging task if applications or developers are required to provide metadata configuration. Therefore, frameworks such as Spring and Dubbo introduce AOT Processing before AOT compilation. AOT Processing is used to collect metadata automatically and provide the metadata to the AOT compiler.

10

The AOT compilation mechanism is common to all Java applications. However, compared to AOT compilation, the process of collecting metadata by AOT Processing is different for each framework because each framework has its own usage for reflection and dynamic proxies.

Let's take a Spring + Dubbo microservices application as an example. To achieve static packaging for this application, it involves the metadata processing of Spring, Dubbo, and third-party dependencies.

• Spring - Spring AOT processing
• Dubbo - Dubbo AOT processing
• Third-party libraries - Reachability Metadata

For Spring, the Spring AOT mechanism was introduced in Spring 6 to support static preprocessing of Spring applications. Similarly, Dubbo released the Dubbo AOT mechanism in version 3.2, enabling automatic preprocessing of Dubbo-related components. Besides these two frameworks closely related to business development, an application often relies on numerous third-party dependencies, and their metadata is crucial for static processing. If these third-party libraries involve behaviors such as reflection and class loading, metadata configuration needs to be provided. Currently, there are two ways to provide metadata configuration for these third-party libraries. One option is to use the shared space provided by GraalVM, which offers a significant portion of metadata configurations for dependencies. The other approach is to require official component releases to include metadata configuration. GraalVM can automatically read metadata in both cases.

Metadata configuration: https://github.com/oracle/graalvm-reachability-metadata

Spring AOT

Next, let's look at what Spring AOT has done before compilation. Spring framework has many dynamic features, such as automatic configuration and conditional bean. Spring AOT preprocesses these dynamic features during building to generate a series of metadata inputs that can be used by GraalVM. The output is as follows:

• Pregenerated code related to the Spring Bean definition, as shown in the following figure.
• Dynamic proxy-related code generated during building.
• JSON metadata file used for reflection.

11

Dubbo AOT

What Dubbo AOT does is similar to Spring AOT, except that Dubbo AOT is used to preprocess the specific usage of the Dubbo framework, including:

• Generate SPI extension-related source code.
• Generate JSON configuration file used for some reflection.
• Generate RPC proxy class code.

12
13

Spring 6 + Dubbo 3 Demonstration

Next, I'll use a sample microservices application of Spring6 + Dubbo3 to demonstrate how to use Spring AOT and Dubbo AOT to package the Native Image of an application.

The complete code sample can be downloaded here: https://github.com/apache/dubbo-samples/tree/master/1-basic/dubbo-samples-native-image

Step 1: Install GraalVM

  1. Select the corresponding Graalvm version according to your system on the Graalvm official website: https://www.graalvm.org/downloads/
  2. Install native-image according to the official documentation: https://www.graalvm.org/latest/reference-manual/native-image/#install-native-image

Step 2: Create a Project

This sample application is a common microservice application. We use Spring Boot 3 for application configuration development and Dubbo3 to define and publish RPC services. The application building tool is Maven.

14
15

Step 3: Configure the Maven Plug-in

The focus is to add three plug-in configurations: spring-boot-maven-plugin, native-maven-plugin, and dubbo-maven-plugin. Open the AOT processing, and modify the mainClass in the dubbo-maven-plugin as the full path of the required startup class. (You do not need to add spring-boot-maven-plugin dependency for API usage.)

<profiles>
        <profile>
            <id>native</id>
            <build>
                <plugins>
                    <plugin>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <configuration>
                            <release>17</release>
                            <fork>true</fork>
                            <verbose>true</verbose>
                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>process-aot</id>
                                <goals>
                                    <goal>process-aot</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.graalvm.buildtools</groupId>
                        <artifactId>native-maven-plugin</artifactId>
                        <version>0.9.20</version>
                        <configuration>
                            <classesDirectory>${project.build.outputDirectory}</classesDirectory>
                            <metadataRepository>
                                <enabled>true</enabled>
                            </metadataRepository>
                            <requiredVersion>22.3</requiredVersion>
                        </configuration>
                        <executions>
                            <execution>
                                <id>add-reachability-metadata</id>
                                <goals>
                                    <goal>add-reachability-metadata</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.dubbo</groupId>
                        <artifactId>dubbo-maven-plugin</artifactId>
                        <version>${dubbo.version}</version>
                        <configuration>
                            <mainClass>com.example.nativedemo.NativeDemoApplication</mainClass>
                        </configuration>
                        <executions>
                            <execution>
                                <phase>process-sources</phase>
                                <goals>
                                    <goal>dubbo-process-aot</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

Step 4: Add Native-related Dependencies to the Pom Dependency

In addition, for Dubbo, because some Native mechanisms depend on versions such as JDK17, Dubbo does not package some packages into the release version by default, so two additional dependencies need to be added to dubbo-spring6 adaptation and dubbo-native components.

<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-config-spring6</artifactId>
    <version>${dubbo.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-native</artifactId>
    <version>${dubbo.version}</version>
</dependency>

Step 5: Adjust the compiler, proxy, serialization, and logger

At the same time, this example supports limited third-party components and mainly supports the Reachability Metadata of third-party components. For example, the currently supported network communication or coding components are Netty and Fastjson2, the supported logging component is Logback, and the supported microservice components are Nacos and Zookeeper.

• The serialization method supported well is Fastjson2.
• The compiler and proxy can only choose JDK.
• Logger needs to be configured with slf4j. Currently, only logback is supported.

Example:

dubbo:
  application:
    name: ${spring.application.name}
    logger: slf4j
    compiler: jdk
  protocol:
    name: dubbo
    port: -1
    serialization: fastjson2
  registry:
    id: zk-registry
    address: zookeeper://127.0.0.1:2181
  config-center:
    address: zookeeper://127.0.0.1:2181
  metadata-report:
    address: zookeeper://127.0.0.1:2181
  provider:
    proxy: jdk
    serialization: fastjson2
  consumer:
    proxy: jdk
    serialization: fastjson2

Step 6: Compile

Execute the following compilation command in the root path of the project:

• Direct execution through API

mvn clean install -P native -Dmaven.test.skip=true

• Annotation and XML modes (Integrated mode of Springboot3)

mvn clean install -P native native:compile -Dmaven.test.skip=true

Step 7: Execute Binary Files

Binary files are in the target/ directory. Generally, the project name is the binary package name, such as target/native-demo.

Summary

GraalVM technology has brought new changes to Java applications in the era of cloud computing. It has effectively addressed common issues faced by Java applications, such as slow startup times and excessive resource consumption. However, it's important to acknowledge the limitations associated with using GraalVM. In response to these limitations, Spring 6, Spring Boot 3, and Dubbo 3 have introduced their respective Native solutions. Additionally, Spring Cloud Alibaba is actively promoting static packaging solutions. Going forward, we will focus on conducting comprehensive Native static verification, with a particular emphasis on the surrounding ecosystem components of these two frameworks, including nacos, sentinel, and seata.

Reference

[1] Apache Dubbo blog
https://cn.dubbo.apache.org/en/blog/

0 1 0
Share on

You may also like

Comments

Related Products

  • Cloud-Native Applications Management Solution

    Accelerate and secure the development, deployment, and management of containerized applications cost-effectively.

    Learn More
  • Function Compute

    Alibaba Cloud Function Compute is a fully-managed event-driven compute service. It allows you to focus on writing and uploading code without the need to manage infrastructure such as servers.

    Learn More
  • Lindorm

    Lindorm is an elastic cloud-native database service that supports multiple data models. It is capable of processing various types of data and is compatible with multiple database engine, such as Apache HBase®, Apache Cassandra®, and OpenTSDB.

    Learn More
  • PolarDB for MySQL

    Alibaba Cloud PolarDB for MySQL is a cloud-native relational database service 100% compatible with MySQL.

    Learn More