1. Tổng quan
Trong lớp học lập trình này, chúng ta sẽ tìm hiểu về dự án Spring Native, tạo một ứng dụng sử dụng dự án này và triển khai ứng dụng đó trên Google Cloud.
Chúng ta sẽ xem xét các thành phần, lịch sử gần đây của dự án, một số trường hợp sử dụng và tất nhiên là các bước cần thiết để bạn sử dụng dự án này trong các dự án của mình.
Dự án Spring Native hiện đang trong giai đoạn thử nghiệm, vì vậy, bạn cần định cấu hình một số thông tin cụ thể để bắt đầu. Tuy nhiên, như đã công bố tại SpringOne 2021, Spring Native sẽ được tích hợp vào Spring Framework 6.0 và Spring Boot 3.0 với sự hỗ trợ hàng đầu. Vì vậy, đây là thời điểm hoàn hảo để xem xét kỹ hơn dự án này vài tháng trước khi phát hành.
Mặc dù tính năng biên dịch tức thì đã được tối ưu hoá rất tốt cho những việc như các quy trình chạy dài, nhưng có một số trường hợp sử dụng mà các ứng dụng được biên dịch trước thời gian chạy hoạt động tốt hơn nữa. Chúng ta sẽ thảo luận về vấn đề này trong lớp học lập trình.
Bạn sẽ tìm hiểu cách
- Sử dụng Cloud Shell
- Bật Cloud Run API
- Tạo và triển khai ứng dụng gốc Spring
- Triển khai ứng dụng như vậy lên Cloud Run
Bạn cần có
- Một dự án trên Google Cloud Platform có tài khoản thanh toán GCP đang hoạt động
- gcloud CLI đã cài đặt hoặc quyền truy cập vào Cloud Shell
- Các kỹ năng cơ bản về Java + XML
- Kiến thức thực tế về các lệnh Linux thông thường
Khảo sát
Bạn sẽ sử dụng hướng dẫn này như thế nào?
Bạn đánh giá thế nào về kinh nghiệm của mình với Java?
Bạn đánh giá thế nào về kinh nghiệm của mình khi sử dụng các dịch vụ của Google Cloud?
2. Thông tin khái quát
Dự án Spring Native sử dụng một số công nghệ để mang lại hiệu suất ứng dụng gốc cho nhà phát triển.
Để hiểu đầy đủ về Spring Native, bạn nên nắm được một số công nghệ thành phần này, những gì chúng cho phép chúng ta thực hiện và cách chúng phối hợp với nhau tại đây.
Biên dịch AOT
Khi nhà phát triển chạy javac bình thường trong thời gian biên dịch, mã nguồn .java của chúng ta sẽ được biên dịch thành các tệp .class được viết bằng mã byte. Mã byte này chỉ dành cho Máy ảo Java (JVM). Vì vậy, JVM sẽ phải diễn giải mã này trên các máy khác để chúng ta có thể chạy mã của mình.
Quá trình này mang lại cho chúng ta khả năng di động đặc trưng của Java – cho phép chúng ta "viết một lần và chạy ở mọi nơi", nhưng tốn kém hơn so với việc chạy mã gốc.
May mắn là hầu hết các cách triển khai JVM đều sử dụng tính năng biên dịch tức thì để giảm thiểu chi phí diễn giải này. Điều này đạt được bằng cách đếm số lần gọi một hàm. Nếu hàm đó được gọi đủ thường xuyên để vượt qua một ngưỡng ( theo mặc định là 10.000), thì hàm đó sẽ được biên dịch thành mã gốc trong thời gian chạy để ngăn chặn việc diễn giải tốn kém hơn nữa.
Tính năng biên dịch trước thời gian chạy áp dụng phương pháp ngược lại bằng cách biên dịch tất cả mã có thể truy cập thành một tệp thực thi gốc trong thời gian biên dịch. Điều này đánh đổi khả năng di động để lấy hiệu quả sử dụng bộ nhớ và các lợi ích khác về hiệu suất trong thời gian chạy.

Đây tất nhiên là một sự đánh đổi và không phải lúc nào cũng đáng thực hiện. Tuy nhiên, tính năng biên dịch AOT có thể phát huy tác dụng trong một số trường hợp sử dụng như:
- Các ứng dụng tồn tại trong thời gian ngắn mà thời gian khởi động là quan trọng
- Các môi trường bị hạn chế về bộ nhớ, trong đó JIT có thể quá tốn kém
Một điều thú vị là tính năng biên dịch AOT được giới thiệu dưới dạng tính năng thử nghiệm trong JDK 9, mặc dù cách triển khai này tốn kém để duy trì và không bao giờ thực sự được sử dụng rộng rãi. Vì vậy, tính năng này đã bị loại bỏ một cách lặng lẽ trong Java 17 để nhà phát triển chỉ sử dụng GraalVM.
GraalVM
GraalVM là một bản phân phối JDK nguồn mở được tối ưu hoá cao, có thời gian khởi động cực nhanh, tính năng biên dịch hình ảnh gốc AOT và khả năng đa ngôn ngữ cho phép nhà phát triển kết hợp nhiều ngôn ngữ vào một ứng dụng.
GraalVM đang trong quá trình phát triển tích cực, liên tục có thêm các tính năng mới và cải thiện các tính năng hiện có. Vì vậy, tôi khuyến khích nhà phát triển nên theo dõi.
Một số cột mốc gần đây là:
- Đầu ra bản dựng hình ảnh gốc mới, thân thiện với người dùng ( 18/1/2021)
- Hỗ trợ Java 17 ( 18/1/2022)
- Bật tính năng biên dịch nhiều tầng theo mặc định để cải thiện thời gian biên dịch đa ngôn ngữ ( 20/4/2021)
Spring Native
Nói một cách đơn giản, Spring Native cho phép sử dụng trình biên dịch native-image của GraalVM để biến các ứng dụng Spring thành các tệp thực thi gốc.
Quá trình này bao gồm việc thực hiện phân tích tĩnh ứng dụng của bạn trong thời gian biên dịch để tìm tất cả các phương thức trong ứng dụng có thể truy cập từ điểm truy cập.
Về cơ bản, điều này tạo ra một khái niệm "thế giới khép kín" về ứng dụng của bạn, trong đó tất cả mã được giả định là đã biết trong thời gian biên dịch và không có mã mới nào được phép tải trong thời gian chạy.
Điều quan trọng cần lưu ý là việc tạo hình ảnh gốc là một quá trình tốn nhiều bộ nhớ, mất nhiều thời gian hơn so với việc biên dịch một ứng dụng thông thường và áp đặt các giới hạn đối với một số khía cạnh của Java.
Trong một số trường hợp, bạn không cần thay đổi mã để ứng dụng hoạt động với Spring Native. Tuy nhiên, một số trường hợp yêu cầu cấu hình gốc cụ thể để hoạt động đúng cách. Trong những trường hợp đó, Spring Native thường cung cấp Gợi ý gốc để đơn giản hoá quy trình này.
3. Thiết lập/Chuẩn bị
Trước khi bắt đầu triển khai Spring Native, chúng ta cần tạo và triển khai ứng dụng để thiết lập một đường cơ sở hiệu suất mà chúng ta có thể so sánh với phiên bản gốc sau này.
1. Tạo dự án
Chúng ta sẽ bắt đầu bằng cách lấy ứng dụng từ start.spring.io:
curl https://start.spring.io/starter.zip -d dependencies=web \
-d javaVersion=11 \
-d bootVersion=2.6.4 -o io-native-starter.zip
Ứng dụng khởi đầu này sử dụng Spring Boot 2.6.4, đây là phiên bản mới nhất mà dự án spring-native hỗ trợ tại thời điểm viết.
Xin lưu ý rằng kể từ khi phát hành GraalVM 21.0.3, bạn cũng có thể sử dụng Java 17 cho mẫu này. Chúng ta vẫn sẽ sử dụng Java 11 cho hướng dẫn này để giảm thiểu cấu hình liên quan.
Sau khi có tệp zip trên dòng lệnh, chúng ta có thể tạo một thư mục con cho dự án và giải nén thư mục đó:
mkdir spring-native cd spring-native unzip ../io-native-starter.zip
2. Thay đổi mã
Sau khi mở dự án, chúng ta sẽ nhanh chóng thêm dấu hiệu hoạt động và giới thiệu hiệu suất của Spring Native sau khi chạy.
Chỉnh sửa DemoApplication.java để khớp với nội dung này:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.Instant;
@RestController
@SpringBootApplication
public class DemoApplication {
private static Instant startTime;
private static Instant readyTime;
public static void main(String[] args) {
startTime = Instant.now();
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/")
public String index() {
return "Time between start and ApplicationReadyEvent: "
+ Duration.between(startTime, readyTime).toMillis()
+ "ms";
}
@EventListener(ApplicationReadyEvent.class)
public void ready() {
readyTime = Instant.now();
}
}
Tại thời điểm này, ứng dụng cơ sở của chúng ta đã sẵn sàng hoạt động. Vì vậy, bạn có thể tạo hình ảnh và kích hoạt hình ảnh đó cục bộ để nắm được thời gian khởi động trước khi chuyển đổi thành ứng dụng gốc.
Cách tạo hình ảnh:
mvn spring-boot:build-image
Bạn cũng có thể sử dụng docker images demo để nắm được kích thước của hình ảnh cơ sở: 
Cách chạy ứng dụng:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
3. Triển khai ứng dụng cơ sở
Bây giờ, chúng ta sẽ triển khai ứng dụng và ghi lại thời gian. Chúng ta sẽ so sánh thời gian này với thời gian khởi động ứng dụng gốc sau này.
Tuỳ thuộc vào loại ứng dụng mà bạn đang tạo, có một số cách lưu trữ nội dung của bạn.
Tuy nhiên, vì ví dụ của chúng ta là một ứng dụng web rất đơn giản, dễ hiểu, nên chúng ta có thể giữ mọi thứ đơn giản và dựa vào Cloud Run.
Nếu bạn đang làm theo trên máy của riêng mình, hãy nhớ cài đặt và cập nhật công cụ gcloud CLI.
Nếu bạn đang sử dụng Cloud Shell, thì tất cả những việc đó sẽ được thực hiện và bạn chỉ cần chạy lệnh sau trong thư mục nguồn:
gcloud run deploy
4. Cấu hình ứng dụng
1. Định cấu hình kho lưu trữ Maven
Vì dự án này vẫn đang trong giai đoạn thử nghiệm, nên chúng ta phải định cấu hình ứng dụng để có thể tìm thấy các cấu phần phần mềm thử nghiệm không có trong kho lưu trữ trung tâm của Maven.
Điều này sẽ liên quan đến việc thêm các thành phần sau vào pom.xml. Bạn có thể thực hiện việc này trong trình chỉnh sửa mà bạn chọn.
Thêm các phần sau vào pom của chúng ta: repositories và pluginRepositories:
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
2. Thêm phần phụ thuộc
Tiếp theo, hãy thêm phần phụ thuộc spring-native. Phần phụ thuộc này là bắt buộc để chạy ứng dụng Spring dưới dạng hình ảnh gốc. Lưu ý: bạn không cần thực hiện bước này nếu đang sử dụng Gradle
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
3. Thêm/bật trình bổ trợ
Bây giờ, hãy thêm trình bổ trợ AOT để cải thiện khả năng tương thích và kích thước hình ảnh gốc ( Đọc thêm):
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.11.2</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Bây giờ, chúng ta sẽ cập nhật spring-boot-maven-plugin để bật tính năng hỗ trợ hình ảnh gốc và sử dụng trình tạo paketo để tạo hình ảnh gốc:
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
</plugins>
Xin lưu ý rằng hình ảnh trình tạo nhỏ chỉ là một trong số nhiều lựa chọn. Đây là lựa chọn phù hợp cho trường hợp sử dụng của chúng ta vì có rất ít thư viện và tiện ích bổ sung, giúp giảm thiểu bề mặt tấn công.
Ví dụ: nếu bạn đang tạo một ứng dụng cần quyền truy cập vào một số thư viện C phổ biến hoặc bạn chưa chắc chắn về các yêu cầu của ứng dụng, thì trình tạo đầy đủ có thể phù hợp hơn.
5. Tạo và chạy ứng dụng gốc
Sau khi hoàn tất, chúng ta sẽ có thể tạo hình ảnh và chạy ứng dụng gốc đã biên dịch.
Trước khi chạy bản dựng, bạn cần lưu ý một số điều sau:
- Quá trình này sẽ mất nhiều thời gian hơn so với bản dựng thông thường (vài phút)

- Quy trình xây dựng này có thể tốn nhiều bộ nhớ (vài gigabyte)

- Quy trình xây dựng này yêu cầu có thể truy cập vào trình nền Docker
- Mặc dù trong ví dụ này, chúng ta đang thực hiện quy trình theo cách thủ công, nhưng bạn cũng có thể định cấu hình các giai đoạn tạo bản dựng để tự động kích hoạt hồ sơ bản dựng gốc.
Cách tạo hình ảnh:
mvn spring-boot:build-image
Sau khi tạo xong, chúng ta đã sẵn sàng xem ứng dụng gốc hoạt động!
Cách chạy ứng dụng:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
Tại thời điểm này, chúng ta đang ở vị trí tuyệt vời để xem cả hai mặt của phương trình ứng dụng gốc.
Chúng ta đã mất một chút thời gian và sử dụng thêm mức sử dụng bộ nhớ trong thời gian biên dịch, nhưng đổi lại, chúng ta có một ứng dụng có thể khởi động nhanh hơn nhiều và tiêu thụ ít bộ nhớ hơn đáng kể (tuỳ thuộc vào khối lượng công việc).
Nếu chạy docker images demo để so sánh kích thước của hình ảnh gốc với hình ảnh gốc, chúng ta có thể thấy mức giảm đáng kể:

Chúng ta cũng nên lưu ý rằng trong các trường hợp sử dụng phức tạp hơn, cần có thêm các sửa đổi để thông báo cho trình biên dịch AOT về những gì ứng dụng của bạn sẽ thực hiện trong thời gian chạy. Vì lý do đó, một số khối lượng công việc có thể dự đoán được (chẳng hạn như công việc hàng loạt) có thể rất phù hợp với điều này, trong khi những khối lượng công việc khác có thể khó khăn hơn.
6. Triển khai ứng dụng gốc
Để triển khai ứng dụng lên Cloud Run, chúng ta cần đưa hình ảnh gốc vào một trình quản lý gói như Artifact Registry.
1. Chuẩn bị kho lưu trữ Docker
Chúng ta có thể bắt đầu quá trình này bằng cách tạo một kho lưu trữ:
gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"
Tiếp theo, chúng ta cần đảm bảo rằng mình đã xác thực để đẩy vào sổ đăng ký mới.
gcloud CLI có thể đơn giản hoá quy trình đó đôi chút:
gcloud auth configure-docker us-central1-docker.pkg.dev
2. Đẩy hình ảnh lên Artifact Registry
Tiếp theo, chúng ta sẽ gắn thẻ hình ảnh:
export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
docker tag demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Sau đó, chúng ta có thể sử dụng docker push để gửi hình ảnh đó đến Artifact Registry:
docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
3. Triển khai lên Cloud Run
Bây giờ, chúng ta đã sẵn sàng triển khai hình ảnh mà chúng ta đã lưu trữ trong Artifact Registry lên Cloud Run:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Vì đã tạo và triển khai ứng dụng dưới dạng hình ảnh gốc, nên chúng ta có thể yên tâm rằng ứng dụng của mình đang tận dụng tối đa chi phí cơ sở hạ tầng khi chạy.
Bạn có thể tự so sánh thời gian khởi động của ứng dụng cơ sở với ứng dụng gốc mới này!

7. Tóm tắt/Dọn dẹp
Xin chúc mừng bạn đã tạo và triển khai ứng dụng gốc Spring trên Google Cloud!
Hy vọng hướng dẫn này sẽ khuyến khích bạn làm quen hơn với dự án Spring Native và ghi nhớ dự án này nếu dự án đó đáp ứng nhu cầu của bạn trong tương lai.
Không bắt buộc: Dọn dẹp và/hoặc tắt dịch vụ
Dù bạn đã tạo một dự án trên đám mây của Google Cloud cho lớp học lập trình này hay đang sử dụng lại một dự án hiện có, hãy cẩn thận để tránh các khoản phí không cần thiết từ các tài nguyên mà chúng ta đã sử dụng.
Bạn có thể xoá hoặc tắt các dịch vụ Cloud Run mà chúng ta đã tạo, xoá hình ảnh mà chúng ta đã lưu trữ hoặc tắt toàn bộ dự án.
8. Tài nguyên khác
Mặc dù dự án Spring Native hiện là một dự án mới và thử nghiệm, nhưng đã có rất nhiều tài nguyên hữu ích để giúp những người dùng sớm khắc phục sự cố và tham gia:
Tài nguyên khác
Dưới đây là các tài nguyên trực tuyến có thể liên quan đến hướng dẫn này:
- Tìm hiểu thêm về Gợi ý gốc
- Tìm hiểu thêm về GraalVM
- Cách tham gia
- Lỗi hết bộ nhớ khi tạo hình ảnh gốc
- Lỗi ứng dụng không khởi động được
Giấy phép
Tác phẩm này được cấp phép theo Giấy phép Ghi công theo Creative Commons 2.0 Chung.