Kami mengkompilasi aplikasi Boot Spring menjadi asli menggunakan GraalVM

Terjemahan artikel disiapkan sebelum dimulainya kursus "Pengembang Kerangka Kerja Musim Semi" .



Halo pecinta Spring! Selamat datang di rilis selanjutnya dari Tips Musim Semi. Hari ini kita akan berbicara tentang dukungan yang baru diterapkan untuk mengkompilasi aplikasi Boot Musim Semi di GraalVM. Kita sudah bicara tentang GraalVM dan aplikasi asli dalam rilis Spring Tips lain tentang topik Spring Fu.



Mari kita ingat apa itu GraalVM. GraalVM adalah pengganti untuk kompiler C1 standar di OpenJDK. Anda dapat membaca lebih lanjut tentang menggunakan GraalVM di podcast Bootiful Podcast saya dengan Chris Thalinge, seorang kontributor GraalVM dan insinyur Twitter. Dalam kondisi tertentu, GraalVM memungkinkan Anda untuk menjalankan aplikasi Musim Semi biasa lebih cepat dan, setidaknya untuk alasan ini, itu pantas mendapat perhatian.

Tetapi kita tidak akan membicarakannya. Kami akan melihat komponen GraalVM lainnya: pembangun gambar asli dan SubstrateVM. SubstrateVM memungkinkan Anda untuk membuat executable asli untuk aplikasi Java Anda. Ngomong-ngomong, tentang ini dan kegunaan GraalVM lainnya ada podcast dengan Oleg Shelaev dari Oracle Labs. Pembangun gambar asli adalah ujian untuk kompromi. Jika Anda memberikan GraalVM informasi yang cukup tentang perilaku aplikasi Anda dalam runtime (pustaka yang terhubung secara dinamis, refleksi, proksi, dll.), Itu akan dapat mengubah aplikasi Java Anda menjadi biner yang terhubung secara statis, seperti aplikasi di C atau Golang. Sejujurnya, proses ini bisa sangat menyakitkan. Tetapi jika Anda melakukannya, Anda dapat menghasilkan kode asli yang akan sangat cepat. Akibatnya, aplikasi akan mengambil RAM jauh lebih sedikit dan berjalan dalam waktu kurang dari satu detik. Kurang dari sedetik. Cukup menggoda, bukan? Tentu saja!

Namun, harus diingat bahwa beberapa poin harus diperhitungkan. Biner GraalVM yang dihasilkan bukan aplikasi Java. Mereka bahkan tidak menjalankan JVM biasa. GraalVM sedang dikembangkan oleh Oracle Labs dan ada semacam interaksi antara tim Java dan GraalVM, tapi saya tidak akan menyebutnya Java. Biner yang dihasilkan tidak akan cross-platform. Aplikasi tidak menggunakan JVM. Ini berjalan di runtime lain yang disebut SubstrateVM.

Jadi ada banyak pertukaran di sini, namun demikian, saya pikir GraalVM memiliki potensi besar, terutama untuk aplikasi berbasis cloud di mana penskalaan dan efisiensi adalah yang terpenting.

Ayo mulai. Instal GraalVM. Anda dapat mengunduhnya di sini , atau menginstal menggunakan SDKManager. Untuk menginstal distribusi Java, saya suka menggunakan SDKManager. GraalVM sedikit di belakang versi terbaru Java dan saat ini mendukung Java 8 dan 11. Dukungan untuk Java 14 atau 15 atau lebih baru (versi mana yang akan ada ketika Anda membaca ini) tidak ada.

Untuk menginstal GraalVM untuk Java 8, jalankan:

sdk install java 20.0.0.r8-grl

Saya sarankan menggunakan Java 8 daripada Java 11, karena ada beberapa kesalahan yang tidak jelas di Java 11 yang belum saya temukan.

Setelah itu, Anda perlu menginstal komponen pembangun gambar asli. Jalankan:

gu install native-image
gu- ini adalah utilitas dari GraalVM.

Terakhir, periksa JAVA_HOMEpoin apa yang mengarah ke GraalVM. Di komputer saya (Macintosh dengan SDKMAN) saya JAVA_HOMEterlihat seperti ini:

export JAVA_HOME=$HOME/.sdkman/candidates/java/current/

Sekarang setelah Anda mengatur semuanya, mari kita lihat aplikasi kami. Pergi ke Spring Initializr dan hasilkan proyek baru menggunakan Lombok, R2DBC, PostgreSQL, dan Web Reaktif.

Anda melihat kode serupa jutaan kali, jadi saya tidak akan membukanya, tetapi berikan saja di sini.

package com.example.reactive;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.stream.Stream;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

@Log4j2
@SpringBootApplication(proxyBeanMethods = false)
public class ReactiveApplication {

    @Bean
    RouterFunction<ServerResponse> routes(ReservationRepository rr) {
        return route()
            .GET("/reservations", r -> ok().body(rr.findAll(), Reservation.class))
            .build();
    }

    @Bean
    ApplicationRunner runner(DatabaseClient databaseClient, ReservationRepository reservationRepository) {
        return args -> {

            Flux<Reservation> names = Flux
                .just("Andy", "Sebastien")
                .map(name -> new Reservation(null, name))
                .flatMap(reservationRepository::save);

            databaseClient
                .execute("create table reservation ( id   serial primary key, name varchar(255) not null )")
                .fetch()
                .rowsUpdated()
                .thenMany(names)
                .thenMany(reservationRepository.findAll())
                .subscribe(log::info);
        };
    }


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

interface ReservationRepository extends ReactiveCrudRepository<Reservation, Integer> {
}


@Data
@AllArgsConstructor
@NoArgsConstructor
class Reservation {

    @Id
    private Integer id;
    private String name;
}

Anda dapat melihat kode lengkapnya di sini .

Satu-satunya fitur dari aplikasi ini adalah kami menggunakan atribut Spring Boot proxyBeanMethodsuntuk memastikan bahwa aplikasi tersebut tidak akan menggunakan cglib atau proxy lain selain JDK. GraalVM tidak mendukung proxy non-JDK. Meskipun dengan proxy JDK, Anda harus mengotak-atiknya sehingga GraalVM belajar tentang mereka. Atribut ini, baru untuk Spring Framework 5.2, sebagian dimaksudkan untuk mendukung GraalVM.

Jadi, mari kita lanjutkan. Saya sebutkan sebelumnya bahwa kami harus memberi tahu GraalVM tentang beberapa poin yang mungkin ada dalam aplikasi kami saat runtime dan mungkin tidak mengerti ketika menjalankan kode asli. Ini adalah hal-hal seperti refleksi, proksi, dll. Ada beberapa cara untuk melakukan ini. Anda dapat menggambarkan konfigurasi secara manual dan memasukkannya ke dalam perakitan. GraalVM akan secara otomatis mengambilnya. Cara lain adalah menjalankan program dengan agen Java yang memantau apa yang sedang dilakukan aplikasi dan, setelah aplikasi selesai, menulis semuanya ke file konfigurasi, yang kemudian dapat ditransfer ke kompiler GraalVM.

Anda juga dapat menggunakan fitur GraalVM. (Catatan Penerjemah: "fitur" - istilah GraalVM menunjukkan plug-in untuk kompilasi asli, membuat biner asli dari file kelas ) . Fitur GraalVM mirip dengan agen Java. Ia dapat melakukan semacam analisis dan meneruskan informasi ke kompiler GraalVM. Fitur mengetahui dan memahami cara kerja aplikasi Spring. Dia tahu kapan kacang Spring adalah proksi. Dia tahu cara membuat kelas secara dinamis di runtime. Dia tahu bagaimana Spring bekerja, dan dia tahu apa yang diinginkan GraalVM, setidaknya sebagian besar waktu (setelah semua, ini adalah rilis awal!)

Anda juga perlu mengkonfigurasi build. Ini milik sayapom.xml.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.M4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>reactive</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <start-class>
            com.example.reactive.ReactiveApplication
        </start-class>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-graal-native</artifactId>
            <version>0.6.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-indexer</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>io.r2dbc</groupId>
            <artifactId>r2dbc-h2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <finalName>
            ${project.artifactId}
        </finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </pluginRepository>
    </pluginRepositories>


    <profiles>
        <profile>
            <id>graal</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.nativeimage</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>20.0.0</version>
                        <configuration>
                            <buildArgs>
-Dspring.graal.mode=initialization-only -Dspring.graal.dump-config=/tmp/computed-reflect-config.json -Dspring.graal.verbose=true -Dspring.graal.skip-logback=true --initialize-at-run-time=org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils --initialize-at-build-time=io.r2dbc.spi.IsolationLevel,io.r2dbc.spi --initialize-at-build-time=io.r2dbc.spi.ConstantPool,io.r2dbc.spi.Assert,io.r2dbc.spi.ValidationDepth --initialize-at-build-time=org.springframework.data.r2dbc.connectionfactory -H:+TraceClassInitialization --no-fallback --allow-incomplete-classpath --report-unsupported-elements-at-runtime -H:+ReportExceptionStackTraces --no-server --initialize-at-build-time=org.reactivestreams.Publisher --initialize-at-build-time=com.example.reactive.ReservationRepository --initialize-at-run-time=io.netty.channel.unix.Socket --initialize-at-run-time=io.netty.channel.unix.IovArray --initialize-at-run-time=io.netty.channel.epoll.EpollEventLoop --initialize-at-run-time=io.netty.channel.unix.Errors
                            </buildArgs>
                        </configuration>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

</project>

Di sini kita memperhatikan plugin native-image-maven-plugin. Dia mengambil parameter melalui baris perintah, yang membantunya memahami apa yang perlu dilakukan. Semua parameter ini buildArgsdiperlukan untuk memulai aplikasi. (Saya harus mengucapkan terima kasih yang mendalam kepada Andy Clement, pengelola Fitur GraalVM Musim Semi, karena membantu saya mengetahui semua opsi ini!)

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-graal-native</artifactId>
    <version>0.6.0.RELEASE</version>
</dependency>

Kami ingin kompiler GraalVM memiliki sebanyak mungkin cara untuk memberikan informasi tentang bagaimana aplikasi seharusnya bekerja: agen java, Fitur GraalVM, parameter baris perintah. Semua ini memberi GraalVM informasi yang cukup untuk berhasil mengubah aplikasi menjadi biner asli yang dikompilasi secara statis. Dalam jangka panjang, tujuan kami adalah proyek Musim Semi. Dan fitur Spring GraalVM menyediakan semua yang Anda butuhkan untuk mendukung mereka.

Sekarang kita sudah menyiapkan semuanya, mari kita kumpulkan aplikasi:

  • Kompilasi aplikasi Java dengan cara biasa
  • Luncurkan aplikasi Java dengan agen Java untuk mengumpulkan informasi. Pada tahap ini, kita perlu memastikan bahwa aplikasi berfungsi. Anda harus melewati semua kasus penggunaan yang mungkin. Omong-omong, ini adalah kasus yang sangat baik untuk menggunakan CI dan tes! Setiap orang terus-menerus berbicara tentang pengujian aplikasi dan meningkatkan kinerja. Sekarang, dengan GraalVM, Anda dapat melakukan keduanya!
  • Kemudian buat kembali aplikasi, kali ini dengan profil graal aktif, untuk mengkompilasinya ke dalam aplikasi asli, menggunakan informasi yang dikumpulkan selama peluncuran pertama.

mvn -DskipTests=true clean package
export MI=src/main/resources/META-INF
mkdir -p $MI 
java -agentlib:native-image-agent=config-output-dir=${MI}/native-image -jar target/reactive.jar

## it's at this point that you need to exercise the application: http://localhost:8080/reservations 
## then hit CTRL + C to stop the running application.

tree $MI
mvn -Pgraal clean package

Jika semuanya berjalan tanpa kesalahan, maka di direktori targetAnda akan melihat aplikasi yang dikompilasi. Menjalankannya.

./target/com.example.reactive.reactiveapplication

Aplikasi dimulai, seperti yang dapat dilihat dari output yang mirip dengan ini.

2020-04-15 23:25:08.826  INFO 7692 --- [           main] c.example.reactive.ReactiveApplication   : Started ReactiveApplication in 0.099 seconds (JVM running for 0.103)

Tidak buruk? Pembuat gambar asli GraalVM sangat bagus untuk dipasangkan dengan platform cloud seperti CloudFoundry atau Kubernetes. Anda dapat dengan mudah merakit aplikasi ke dalam wadah dan menjalankannya di cloud dengan sumber daya minimal.
Seperti biasa, kami akan senang mendengar dari Anda. Apakah teknologi ini tepat untuk Anda? Pertanyaan? Komentar? Twitter (@springcentral) .



Pelajari lebih lanjut tentang kursus

All Articles