1. ภาพรวม
ใน Codelab นี้ เราจะมาเรียนรู้เกี่ยวกับโปรเจ็กต์ Spring Native สร้างแอปที่ใช้โปรเจ็กต์นี้ และติดตั้งใช้งานบน Google Cloud
เราจะพูดถึงคอมโพเนนต์ ประวัติล่าสุดของโปรเจ็กต์ กรณีการใช้งานบางอย่าง และแน่นอนว่าขั้นตอนที่จำเป็นเพื่อให้คุณใช้ในโปรเจ็กต์ได้
ขณะนี้โปรเจ็กต์ Spring Native อยู่ในขั้นทดลอง จึงต้องมีการกำหนดค่าเฉพาะบางอย่างเพื่อเริ่มต้นใช้งาน อย่างไรก็ตาม ตามที่ได้ประกาศไว้ใน SpringOne 2021 เราจะผสานรวม Spring Native เข้ากับ Spring Framework 6.0 และ Spring Boot 3.0 โดยมีการสนับสนุนระดับเฟิร์สคลาส ดังนั้นนี่จึงเป็นเวลาที่เหมาะเจาะที่จะมาดูโปรเจ็กต์นี้อย่างใกล้ชิดก่อนที่จะเปิดตัวในอีกไม่กี่เดือน
แม้ว่าการคอมไพล์แบบทันทีจะได้รับการเพิ่มประสิทธิภาพอย่างดีสำหรับสิ่งต่างๆ เช่น กระบวนการที่ทำงานเป็นเวลานาน แต่ก็มี Use Case บางอย่างที่แอปพลิเคชันที่คอมไพล์ล่วงหน้าทำงานได้ดียิ่งขึ้น ซึ่งเราจะพูดถึงในระหว่างการทำ Codelab
คุณจะได้เรียนรู้วิธีต่อไปนี้
- ใช้ Cloud Shell
- เปิดใช้ Cloud Run API
- สร้างและทำให้แอปที่มาพร้อมเครื่อง Spring ใช้งานได้
- ทำให้แอปดังกล่าวใช้งานได้ใน Cloud Run
สิ่งที่คุณต้องมี
- โปรเจ็กต์ Google Cloud Platform ที่มีบัญชีสำหรับการเรียกเก็บเงินของ GCP ที่ใช้งานอยู่
- ติดตั้ง gcloud CLI หรือมีสิทธิ์เข้าถึง Cloud Shell
- ทักษะพื้นฐานด้าน Java + XML
- มีความรู้พื้นฐานเกี่ยวกับคำสั่ง Linux ทั่วไป
แบบสำรวจ
คุณจะใช้บทแนะนำนี้อย่างไร
คุณจะให้คะแนนประสบการณ์การใช้งาน Java เท่าใด
คุณจะให้คะแนนประสบการณ์การใช้บริการ Google Cloud เท่าใด
2. ฉากหลัง
โปรเจ็กต์ Spring Native ใช้เทคโนโลยีหลายอย่างเพื่อมอบประสิทธิภาพของแอปพลิเคชันแบบเนทีฟแก่นักพัฒนาแอป
หากต้องการทำความเข้าใจ Spring Native อย่างถ่องแท้ คุณควรทำความเข้าใจเทคโนโลยีคอมโพเนนต์บางอย่างเหล่านี้ สิ่งที่เทคโนโลยีเหล่านี้ช่วยให้เราทำได้ และวิธีการทำงานร่วมกัน
การคอมไพล์ AOT
เมื่อนักพัฒนาซอฟต์แวร์เรียกใช้ javac ตามปกติในเวลาคอมไพล์ ซอร์สโค้ด .java ของเราจะคอมไพล์เป็นไฟล์ .class ซึ่งเขียนด้วยไบต์โค้ด ไบต์โค้ดนี้มีไว้เพื่อให้เครื่องเสมือน Java เข้าใจเท่านั้น ดังนั้น JVM จะต้องตีความโค้ดนี้ในเครื่องอื่นๆ เพื่อให้เราเรียกใช้โค้ดได้
กระบวนการนี้ช่วยให้ Java มีความสามารถในการพกพาที่เป็นเอกลักษณ์ ซึ่งช่วยให้เรา "เขียนครั้งเดียวและเรียกใช้ได้ทุกที่" แต่ก็มีค่าใช้จ่ายสูงเมื่อเทียบกับการเรียกใช้โค้ดแบบเนทีฟ
โชคดีที่การใช้งาน JVM ส่วนใหญ่ใช้การคอมไพล์แบบทันทีเพื่อลดต้นทุนการตีความนี้ โดยจะนับการเรียกใช้ฟังก์ชัน และหากมีการเรียกใช้บ่อยพอที่จะผ่านเกณฑ์ ( 10,000 โดยค่าเริ่มต้น) ระบบจะคอมไพล์เป็นโค้ดแบบเนทีฟที่รันไทม์เพื่อป้องกันการตีความที่มีค่าใช้จ่ายสูงเพิ่มเติม
การคอมไพล์ล่วงหน้าจะใช้วิธีการตรงกันข้าม โดยการคอมไพล์โค้ดที่เข้าถึงได้ทั้งหมดเป็นไฟล์ที่เรียกใช้งานได้แบบเนทีฟในเวลาคอมไพล์ ซึ่งจะแลกความสามารถในการพกพาเพื่อประสิทธิภาพด้านหน่วยความจำและประสิทธิภาพอื่นๆ ที่เพิ่มขึ้นในเวลาเรียกใช้

แน่นอนว่านี่เป็นการแลกเปลี่ยนและอาจไม่คุ้มค่าเสมอไป อย่างไรก็ตาม การคอมไพล์ AOT อาจมีประโยชน์ใน Use Case บางอย่าง เช่น
- แอปพลิเคชันที่มีอายุการใช้งานสั้นซึ่งเวลาเริ่มต้นมีความสำคัญ
- สภาพแวดล้อมที่มีข้อจำกัดด้านหน่วยความจำสูงซึ่ง JIT อาจมีค่าใช้จ่ายสูงเกินไป
ข้อเท็จจริงที่น่าสนใจคือการคอมไพล์ AOT ได้รับการเปิดตัวเป็นฟีเจอร์ทดลองใน JDK 9 แม้ว่าการใช้งานนี้จะมีค่าใช้จ่ายในการบำรุงรักษาสูงและไม่ได้รับความนิยมมากนัก จึงถูกนำออกอย่างเงียบๆ ใน Java 17 เพื่อให้ผู้พัฒนาใช้ GraalVM แทน
GraalVM
GraalVM เป็นการกระจาย JDK แบบโอเพนซอร์สที่ได้รับการเพิ่มประสิทธิภาพอย่างสูง ซึ่งมีเวลาเริ่มต้นที่รวดเร็วมาก การคอมไพล์อิมเมจเนทีฟ AOT และความสามารถแบบหลายภาษาที่ช่วยให้นักพัฒนาซอฟต์แวร์สามารถรวมหลายภาษาไว้ในแอปพลิเคชันเดียว
GraalVM อยู่ระหว่างการพัฒนาอย่างต่อเนื่อง โดยจะได้รับความสามารถใหม่ๆ และปรับปรุงความสามารถที่มีอยู่ตลอดเวลา ดังนั้นฉันขอแนะนำให้นักพัฒนาซอฟต์แวร์ติดตามข่าวสาร
เหตุการณ์สำคัญล่าสุดบางส่วนมีดังนี้
- เอาต์พุตการสร้างรูปภาพเนทีฟใหม่ที่ใช้งานง่าย ( 2021-01-18)
- รองรับ Java 17 ( 2022-01-18)
- เปิดใช้การคอมไพล์แบบหลายระดับโดยค่าเริ่มต้นเพื่อปรับปรุงเวลาในการคอมไพล์แบบหลายภาษา ( 2021-04-20)
Spring Native
กล่าวโดยย่อคือ Spring Native ช่วยให้ใช้คอมไพเลอร์เนทีฟอิมเมจของ GraalVM เพื่อเปลี่ยนแอปพลิเคชัน Spring ให้เป็นไฟล์ที่เรียกใช้งานได้แบบเนทีฟ
กระบวนการนี้เกี่ยวข้องกับการวิเคราะห์แบบคงที่ของแอปพลิเคชันในเวลาคอมไพล์เพื่อค้นหาวิธีการทั้งหมดในแอปพลิเคชันที่เข้าถึงได้จากจุดแรกเข้า
ซึ่งโดยพื้นฐานแล้วจะสร้างแนวคิด "โลกปิด" ของแอปพลิเคชัน โดยถือว่าโค้ดทั้งหมดเป็นที่รู้จักในเวลาคอมไพล์ และไม่อนุญาตให้โหลดโค้ดใหม่ในเวลาเรียกใช้
โปรดทราบว่าการสร้างรูปภาพแบบเนทีฟเป็นกระบวนการที่ใช้หน่วยความจำมากและใช้เวลานานกว่าการคอมไพล์แอปพลิเคชันทั่วไป และมีข้อจำกัดในบางแง่มุมของ Java
ในบางกรณี คุณไม่จำเป็นต้องเปลี่ยนแปลงโค้ดเพื่อให้แอปพลิเคชันทำงานร่วมกับ Spring Native ได้ อย่างไรก็ตาม ในบางกรณี คุณจะต้องกำหนดค่าเนทีฟเฉพาะเพื่อให้ทำงานได้อย่างถูกต้อง ในกรณีดังกล่าว Spring Native มักจะให้คำแนะนำสำหรับ Native เพื่อทำให้กระบวนการนี้ง่ายขึ้น
3. การตั้งค่า/การเตรียมการ
ก่อนที่จะเริ่มใช้ Spring Native เราจะต้องสร้างและทำให้ใช้งานได้แอปเพื่อกำหนดเกณฑ์ประสิทธิภาพพื้นฐานที่เราจะใช้เปรียบเทียบกับเวอร์ชันเนทีฟในภายหลัง
1. การสร้างโปรเจ็กต์
เราจะเริ่มต้นด้วยการรับแอปจาก 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
แอปเริ่มต้นนี้ใช้ Spring Boot 2.6.4 ซึ่งเป็นเวอร์ชันล่าสุดที่โปรเจ็กต์ spring-native รองรับในขณะที่เขียน
โปรดทราบว่าตั้งแต่เปิดตัว GraalVM 21.0.3 คุณก็ใช้ Java 17 สำหรับตัวอย่างนี้ได้เช่นกัน เราจะยังคงใช้ Java 11 สำหรับบทแนะนำนี้เพื่อลดการกำหนดค่าที่เกี่ยวข้อง
เมื่อมีไฟล์ ZIP ในบรรทัดคำสั่งแล้ว เราจะสร้างไดเรกทอรีย่อยสำหรับโปรเจ็กต์และแตกไฟล์โฟลเดอร์ในนั้นได้โดยใช้คำสั่งต่อไปนี้
mkdir spring-native cd spring-native unzip ../io-native-starter.zip
2. การเปลี่ยนแปลงโค้ด
เมื่อเปิดโปรเจ็กต์แล้ว เราจะเพิ่มสัญญาณการทำงานอย่างรวดเร็วและแสดงประสิทธิภาพของ Spring Native เมื่อเราดำเนินการ
แก้ไข DemoApplication.java ให้ตรงกับโค้ดต่อไปนี้
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();
}
}
ตอนนี้แอปพื้นฐานของเราพร้อมใช้งานแล้ว คุณสามารถสร้างอิมเมจและดำเนินการในเครื่องเพื่อดูเวลาเริ่มต้นก่อนที่เราจะแปลงเป็นแอปพลิเคชันแบบเนทีฟ
วิธีสร้างอิมเมจ
mvn spring-boot:build-image
นอกจากนี้ คุณยังใช้ docker images demo เพื่อดูขนาดของรูปภาพพื้นฐานได้ด้วย 
วิธีเรียกใช้แอป
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
3. ทำให้แอปพื้นฐานใช้งานได้
ตอนนี้เรามีแอปแล้ว เราจะติดตั้งใช้งานแอปและจดบันทึกเวลา ซึ่งเราจะนำไปเปรียบเทียบกับเวลาเริ่มต้นของแอปที่มาพร้อมเครื่องในภายหลัง
คุณสามารถเลือกใช้บริการโฮสติ้งสิ่งต่างๆ ของคุณได้หลายแบบ ขึ้นอยู่กับประเภทของแอปพลิเคชันที่คุณสร้าง
อย่างไรก็ตาม เนื่องจากตัวอย่างของเราเป็นเว็บแอปพลิเคชันที่เรียบง่ายและตรงไปตรงมามาก เราจึงสามารถทำให้ทุกอย่างเรียบง่ายและใช้ Cloud Run ได้
หากคุณกำลังทำตามในเครื่องของคุณเอง โปรดตรวจสอบว่าได้ติดตั้งและอัปเดตเครื่องมือ gcloud CLI แล้ว
หากคุณใช้ Cloud Shell ระบบจะจัดการให้ทั้งหมด และคุณเพียงแค่เรียกใช้คำสั่งต่อไปนี้ในไดเรกทอรีต้นทาง
gcloud run deploy
4. การกำหนดค่าแอปพลิเคชัน
1. การกำหนดค่าที่เก็บ Maven
เนื่องจากโปรเจ็กต์นี้ยังอยู่ในระยะทดลอง เราจึงต้องกำหนดค่าแอปเพื่อให้ค้นหาสิ่งประดิษฐ์ทดลองได้ ซึ่งไม่มีอยู่ในที่เก็บส่วนกลางของ Maven
ซึ่งจะต้องเพิ่มองค์ประกอบต่อไปนี้ลงใน pom.xml ซึ่งคุณสามารถทำได้ในเครื่องมือแก้ไขที่ต้องการ
เพิ่มส่วนที่เก็บข้อมูลและส่วน pluginRepositories ต่อไปนี้ลงใน pom
<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. การเพิ่มทรัพยากร Dependency
จากนั้นเพิ่มทรัพยากร Dependency ของ Spring Native ซึ่งจำเป็นต่อการเรียกใช้แอปพลิเคชัน Spring เป็นอิมเมจเนทีฟ หมายเหตุ: คุณไม่จำเป็นต้องทำขั้นตอนนี้หากใช้ Gradle
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
3. การเพิ่ม/เปิดใช้ปลั๊กอิน
ตอนนี้ให้เพิ่มปลั๊กอิน AOT เพื่อปรับปรุงความเข้ากันได้และร่องรอยของรูปภาพดั้งเดิม ( อ่านเพิ่มเติม)
<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>
ตอนนี้เราจะอัปเดตปลั๊กอิน Maven ของ Spring Boot เพื่อเปิดใช้การรองรับอิมเมจแบบเนทีฟและใช้เครื่องมือสร้าง Paketo เพื่อสร้างอิมเมจแบบเนทีฟ
<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>
โปรดทราบว่ารูปภาพช่างตัวน้อยเป็นเพียงหนึ่งในหลายๆ ตัวเลือก ซึ่งเป็นตัวเลือกที่ดีสำหรับกรณีการใช้งานของเราเนื่องจากมีไลบรารีและยูทิลิตีเพิ่มเติมเพียงเล็กน้อย ซึ่งช่วยลดพื้นที่ในการโจมตีของเรา
เช่น หากคุณกำลังสร้างแอปที่ต้องเข้าถึงไลบรารี C ทั่วไปบางรายการ หรือยังไม่แน่ใจเกี่ยวกับข้อกำหนดของแอป full-builder อาจเป็นตัวเลือกที่เหมาะสมกว่า
5. สร้างและเรียกใช้แอปที่มาพร้อมเครื่อง
เมื่อทุกอย่างพร้อมแล้ว เราก็ควรจะสร้างอิมเมจและเรียกใช้แอปเนทีฟที่คอมไพล์แล้วได้
ก่อนเรียกใช้บิลด์ โปรดคำนึงถึงสิ่งต่อไปนี้
- การดำเนินการนี้จะใช้เวลานานกว่าการสร้างปกติ (2-3 นาที)

- กระบวนการบิลด์นี้อาจใช้หน่วยความจำจำนวนมาก (2-3 กิกะไบต์)

- กระบวนการบิลด์นี้ต้องเข้าถึง Docker Daemon ได้
- แม้ว่าในตัวอย่างนี้เราจะดำเนินการด้วยตนเอง แต่คุณก็กำหนดค่าระยะการสร้างให้ทริกเกอร์โปรไฟล์การสร้างเนทีฟโดยอัตโนมัติได้เช่นกัน
วิธีสร้างอิมเมจ
mvn spring-boot:build-image
เมื่อสร้างเสร็จแล้ว เราก็พร้อมที่จะดูการทำงานของแอปที่มาพร้อมเครื่องแล้ว
วิธีเรียกใช้แอป
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
ตอนนี้เราอยู่ในจุดที่ได้เห็นทั้ง 2 ด้านของสมการแอปพลิเคชันแบบเนทีฟ
เราเสียเวลาไปเล็กน้อยและใช้การใช้งานหน่วยความจำเพิ่มเติมในเวลาคอมไพล์ แต่ในทางกลับกัน เราก็ได้แอปพลิเคชันที่เริ่มทำงานได้เร็วกว่ามากและใช้หน่วยความจำน้อยลงอย่างเห็นได้ชัด (ขึ้นอยู่กับปริมาณงาน)
หากเราเรียกใช้ docker images demo เพื่อเปรียบเทียบขนาดของรูปภาพเนทีฟกับรูปภาพต้นฉบับ เราจะเห็นว่าขนาดลดลงอย่างมาก

นอกจากนี้ เราควรทราบว่าใน Use Case ที่ซับซ้อนมากขึ้น คุณจะต้องทำการแก้ไขเพิ่มเติมเพื่อแจ้งให้คอมไพเลอร์ AOT ทราบว่าแอปจะทำอะไรในรันไทม์ ด้วยเหตุนี้ ภาระงานที่คาดการณ์ได้บางอย่าง (เช่น งานแบบกลุ่ม) อาจเหมาะกับสิ่งนี้เป็นอย่างยิ่ง ในขณะที่ภาระงานอื่นๆ อาจต้องมีการปรับเปลี่ยนมากกว่า
6. การทำให้แอปที่มาพร้อมเครื่องของเราใช้งานได้
หากต้องการทำให้แอปใช้งานได้ใน Cloud Run เราจะต้องนำ Native Image ไปไว้ในเครื่องมือจัดการแพ็กเกจ เช่น Artifact Registry
1. กำลังเตรียมที่เก็บ Docker
เราสามารถเริ่มกระบวนการนี้ได้โดยการสร้างที่เก็บ ดังนี้
gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"
จากนั้นเราจะตรวจสอบว่าเราได้รับการตรวจสอบสิทธิ์เพื่อพุชไปยังรีจิสทรีใหม่
gcloud CLI ช่วยให้กระบวนการดังกล่าวง่ายขึ้นได้มาก
gcloud auth configure-docker us-central1-docker.pkg.dev
2. การพุชอิมเมจไปยัง Artifact Registry
จากนั้นเราจะติดแท็กรูปภาพโดยทำดังนี้
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
จากนั้นเราจะใช้ docker push เพื่อส่งไปยัง Artifact Registry ได้
docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
3. การติดตั้งใช้งานกับ Cloud Run
ตอนนี้เราพร้อมที่จะทำให้ใช้งานได้อิมเมจที่เราจัดเก็บไว้ใน Artifact Registry ไปยัง Cloud Run แล้ว:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
เนื่องจากเราสร้างและติดตั้งใช้งานแอปเป็นอิมเมจเนทีฟ จึงมั่นใจได้ว่าแอปพลิเคชันของเราใช้ต้นทุนโครงสร้างพื้นฐานได้อย่างคุ้มค่าขณะทำงาน
คุณสามารถเปรียบเทียบเวลาเริ่มต้นของแอปพื้นฐานกับแอปใหม่นี้ได้ด้วยตนเอง

7. สรุป/ล้างข้อมูล
ขอแสดงความยินดีที่คุณสร้างและทําให้แอปพลิเคชัน Spring แบบเนทีฟใช้งานได้ใน Google Cloud
เราหวังว่าบทแนะนำนี้จะช่วยให้คุณคุ้นเคยกับโปรเจ็กต์ Spring Native มากขึ้น และเก็บไว้ในใจเผื่อว่าโปรเจ็กต์นี้จะตอบโจทย์ความต้องการของคุณในอนาคต
ไม่บังคับ: ล้างข้อมูลและ/หรือปิดใช้บริการ
ไม่ว่าคุณจะสร้างโปรเจ็กต์ที่อยู่ในระบบคลาวด์ Google Cloud สำหรับ Codelab นี้หรือใช้โปรเจ็กต์ที่มีอยู่แล้ว โปรดระมัดระวังเพื่อหลีกเลี่ยงค่าใช้จ่ายที่ไม่จำเป็นจากทรัพยากรที่เราใช้
คุณสามารถลบหรือปิดใช้บริการ Cloud Run ที่เราสร้าง ลบรูปภาพที่เราโฮสต์ หรือปิดทั้งโปรเจ็กต์ได้
8. แหล่งข้อมูลเพิ่มเติม
แม้ว่าโปรเจ็กต์ Spring Native จะเป็นโปรเจ็กต์ใหม่และอยู่ในขั้นทดลอง แต่ก็มีแหล่งข้อมูลดีๆ มากมายที่พร้อมช่วยให้ผู้ใช้กลุ่มแรกแก้ปัญหาและมีส่วนร่วม
แหล่งข้อมูลเพิ่มเติม
แหล่งข้อมูลออนไลน์ที่อาจเกี่ยวข้องกับบทแนะนำนี้มีดังนี้
- ดูข้อมูลเพิ่มเติมเกี่ยวกับคำแนะนำเนทีฟ
- ดูข้อมูลเพิ่มเติมเกี่ยวกับ GraalVM
- วิธีเข้าร่วม
- ข้อผิดพลาดเนื่องจากไม่มีหน่วยความจำเมื่อสร้างรูปภาพเนทีฟ
- ข้อผิดพลาดในการเริ่มแอปพลิเคชันไม่สำเร็จ
ใบอนุญาต
ผลงานนี้ได้รับอนุญาตภายใต้สัญญาอนุญาตครีเอทีฟคอมมอนส์สำหรับยอมรับสิทธิของผู้สร้าง (Creative Commons Attribution License) 2.0 แบบทั่วไป