نقوم بتجميع تطبيق Spring Boot إلى اللغة الأم باستخدام GraalVM

تم إعداد ترجمة للمقال قبل بدء الدورة التدريبية "Developer on the Spring Framework" .



مرحبا عشاق الربيع! مرحبًا بك في الإصدار التالي من Spring Tips. سنتحدث اليوم عن الدعم الذي تم تنفيذه مؤخرًا لتجميع تطبيقات Spring Boot في GraalVM. لقد تحدثنا بالفعل عن GraalVM والتطبيقات الأصلية في إصدار Spring Tips آخر حول موضوع Spring Fu.



دعونا نتذكر ما هو GraalVM. GraalVM هو بديل لمترجم C1 القياسي في OpenJDK. يمكنك قراءة المزيد حول استخدام GraalVM في بودكاست Bootiful Podcast مع Chris Thalinge ، مساهم GraalVM ومهندس Twitter. في ظروف معينة ، يسمح لك GraalVM بتشغيل تطبيقات Spring العادية بشكل أسرع ، ولهذا السبب على الأقل ، فإنه يستحق الاهتمام.

لكننا لن نتحدث عنه. سنلقي نظرة على مكونات GraalVM الأخرى: منشئ الصور الأصلية و SubstrateVM. يسمح لك SubstrateVM بإنشاء ملفات تنفيذية أصلية لتطبيق Java الخاص بك. بالمناسبة ، حول هذا الاستخدامات الأخرى لـ GraalVM كان هناك بودكاست مع Oleg Shelaev من Oracle Labs. منشئ الصورة الأصلية هو اختبار للتسوية. إذا قمت بتزويد GraalVM بمعلومات كافية حول سلوك التطبيق الخاص بك في وقت التشغيل (المكتبات المرتبطة ديناميكيًا ، والتفكير ، والوكلاء ، وما إلى ذلك) ، فستتمكن من تحويل تطبيق Java الخاص بك إلى ثنائي مرتبط بشكل ثابت ، مثل تطبيق في C أو Golang. بصراحة ، يمكن أن تكون هذه العملية مؤلمة للغاية. ولكن إذا قمت بذلك ، يمكنك إنشاء كود أصلي سيكون سريعًا بشكل لا يصدق. ونتيجة لذلك ، سيستهلك التطبيق ذاكرة وصول عشوائي أقل بكثير ويعمل في أقل من ثانية. أقل من ثانية. مغرية جدا ، أليس كذلك؟ بالطبع بكل تأكيد!

ومع ذلك ، يجب أن نتذكر أنه يجب مراعاة بعض النقاط. الثنائيات GraalVM الناتجة ليست تطبيقات Java. حتى أنها لا تعمل على JVM عادي. يتم تطوير GraalVM بواسطة Oracle Labs وهناك نوع من التفاعل بين فريقي Java و GraalVM ، لكنني لن أسميها Java. لن يكون الثنائي الناتج عبر النظام الأساسي. التطبيق لا يستخدم JVM. يتم تشغيله في وقت تشغيل آخر يسمى SubstrateVM.

لذلك هناك الكثير من المقايضات هنا ، ولكن مع ذلك ، أعتقد أن GraalVM لديه إمكانات كبيرة ، خاصة بالنسبة للتطبيقات المستندة إلى السحابة حيث يكون التوسع والكفاءة أمرًا بالغ الأهمية.

لنبدأ. قم بتثبيت GraalVM. يمكنك تنزيله هنا ، أو التثبيت باستخدام SDKManager. لتثبيت توزيعات Java ، أحب استخدام SDKManager. GraalVM متأخر قليلاً عن أحدث إصدارات Java ويدعم حاليًا Java 8 و 11. دعم Java 14 أو 15 أو أحدث (أي إصدار سيكون موجودًا عند قراءة هذا) مفقود.

لتثبيت GraalVM لـ Java 8 ، قم بتشغيل:

sdk install java 20.0.0.r8-grl

أوصي باستخدام Java 8 بدلاً من Java 11 ، نظرًا لوجود بعض الأخطاء الغامضة في Java 11 التي لم أحسبها بعد.

بعد ذلك ، تحتاج إلى تثبيت مكون منشئ الصور الأصلي. تشغيل:

gu install native-image
gu- هذه أداة مساعدة من GraalVM.

أخيرًا ، تحقق مما JAVA_HOMEيشير إلى GraalVM. على الجهاز الخاص بي (Macintosh مع SDKMAN) JAVA_HOMEيبدو كما يلي:

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

الآن بعد أن قمت بإعداد كل شيء ، دعنا نلقي نظرة على تطبيقنا. انتقل إلى Spring Initializr وقم بإنشاء مشروع جديد باستخدام Lombok و R2DBC و PostgreSQL و Reactive Web.

لقد رأيت رمزًا مشابهًا مليون مرة ، لذلك لن أقوم بفكه ، ولكن أعطيه هنا.

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;
}

يمكنك رؤية الكود الكامل هنا .

الميزة الوحيدة لهذا التطبيق هي أننا نستخدم خاصية Spring Boot proxyBeanMethodsللتأكد من أن التطبيق لن يستخدم cglib أو أي بروكسيات أخرى غير JDK. لا يدعم GraalVM الوكلاء غير JDK. على الرغم من أنه مع وكيل JDK ، يجب عليك العبث به حتى يتعلم GraalVM عنها. تهدف هذه السمة الجديدة في Spring Framework 5.2 جزئيًا إلى دعم GraalVM.

لذا ، دعنا ننتقل. ذكرت سابقًا أنه يجب علينا إخبار GraalVM عن بعض النقاط التي قد تكون موجودة في تطبيقنا في وقت التشغيل والتي قد لا يفهمها عند تنفيذ التعليمات البرمجية الأصلية. هذه أشياء مثل التفكير ، البروكسيات ، إلخ. هناك عدة طرق للقيام بذلك. يمكنك وصف التكوين يدويًا وتضمينه في التجميع. سيقوم GraalVM بالتقاطها تلقائيًا. طريقة أخرى هي تشغيل البرنامج مع وكيل Java الذي يراقب ما يفعله التطبيق ، وبعد انتهاء التطبيق ، يكتب كل شيء إلى ملفات التكوين ، والتي يمكن بعد ذلك نقلها إلى مترجم GraalVM.

يمكنك أيضًا استخدام ميزة GraalVM. (ملحوظة المترجم: "الميزة" - يشير المصطلح GraalVM إلى مكون إضافي للترجمة الأصلية ، وإنشاء ثنائي أصلي من ملف فئة ) . تشبه ميزة GraalVM وكيل Java. يمكنه القيام بنوع من التحليل وتمرير المعلومات إلى مترجم GraalVM. الميزة تعرف وتفهم كيف يعمل تطبيق Spring. إنها تعرف متى تكون حبوب الربيع وكلاء. إنها تعرف كيفية إنشاء فصول بشكل ديناميكي في وقت التشغيل. إنها تعرف كيف يعمل Spring ، وهي تعرف ما تريده GraalVM ، على الأقل معظم الوقت (بعد كل شيء ، هذا إصدار مبكر!)

تحتاج أيضًا إلى تكوين البنية. هنا ليpom.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>

هنا ننتبه إلى البرنامج المساعد native-image-maven-plugin. يأخذ المعلمات من خلال سطر الأوامر ، مما يساعده على فهم ما يجب القيام به. كل هذه المعلمات buildArgsضرورية لبدء التطبيق. (يجب أن أعرب عن امتناني العميق لـ Andy Clement ، مشرف ميزة Spring GraalVM ، لمساعدتي في اكتشاف كل هذه الخيارات!)

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

نريد أن يكون لدى برنامج التحويل البرمجي GraalVM أكبر عدد ممكن من الطرق لتقديم معلومات حول كيفية عمل التطبيق: وكيل جافا ، ميزة GraalVM ، معلمات سطر الأوامر. كل هذا معًا يمنح GraalVM معلومات كافية لتحويل التطبيق بنجاح إلى ثنائي أصلي مترجم بشكل ثابت. هدفنا على المدى الطويل هو مشاريع الربيع. وتوفر ميزة Spring GraalVM كل ما تحتاجه لدعمهم.

الآن بعد أن قمنا بإعداد كل شيء ، فلنقم بتجميع تطبيق:

  • تجميع تطبيق Java بالطريقة المعتادة
  • قم بتشغيل تطبيق Java مع وكيل Java لجمع المعلومات. في هذه المرحلة ، نحتاج إلى التأكد من عمل التطبيق. عليك أن تمر بجميع حالات الاستخدام الممكنة. بالمناسبة ، هذه حالة جيدة جدًا لاستخدام CI والاختبارات! يتحدث الجميع باستمرار عن اختبار التطبيق وتحسين الأداء. الآن ، مع GraalVM ، يمكنك القيام بالأمرين معا!
  • ثم قم بإعادة إنشاء التطبيق ، هذه المرة باستخدام ملف التعريف النشط ، لتجميعه في التطبيق الأصلي ، باستخدام المعلومات التي تم جمعها في البداية الأولى.

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

إذا سار كل شيء دون أخطاء ، فحينئذٍ targetسترى في الدليل تطبيقًا مترجمًا. شغلها.

./target/com.example.reactive.reactiveapplication

يبدأ التطبيق ، كما يمكن رؤيته من إخراج مشابه لهذا.

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)

ليس سيئا؟ تعتبر أداة إنشاء الصور الأصلية GraalVM رائعة للاقتران بمنصة سحابية مثل CloudFoundry أو Kubernetes. يمكنك بسهولة تجميع التطبيق في حاوية وتشغيله في السحابة بأقل الموارد.
كما هو الحال دائمًا ، يسعدنا أن نسمع منك. هل هذه التكنولوجيا مناسبة لك؟ أسئلة؟ تعليقات؟ تويتر (pringcentral) .



تعلم المزيد عن الدورة

All Articles