GraalVM 原生映像檔簡介 (GraalVM 原生映像檔簡介)

GraalVM 原生映像檔提供部署和執行 Java 應用程式的新方法。與 Java 虛擬機器相比,原生映像檔可以用更少的記憶體空間和更快的啟動時間執行。

它們非常適合使用容器映像檔部署的應用程式,並且在與「功能即服務」(FaaS) 平台結合使用時尤其引人注目。

與為 JVM 編寫的傳統應用程式不同,GraalVM 原生映像檔應用程式需要事先處理才能建立可執行檔。此事前處理涉及從主要進入點對應用程式程式碼進行靜態分析。

GraalVM 原生映像檔是一個完整的、特定於平台的可執行檔。您不需要為了執行原生映像檔而發佈 Java 虛擬機器。

如果您只想開始並體驗 GraalVM,您可以跳到「開發您的第一個 GraalVM 原生應用程式」章節,稍後再回到此章節。

與 JVM 部署的主要差異

GraalVM 原生映像檔是事先產生的,這意味著原生應用程式和基於 JVM 的應用程式之間存在一些主要差異。主要差異如下:

  • 在建構時從 main 進入點執行應用程式的靜態分析。

  • 建立原生映像檔時無法到達的程式碼將被移除,並且不會成為可執行檔的一部分。

  • GraalVM 不直接感知程式碼的動態元素,必須告知反射、資源、序列化和動態代理。

  • 應用程式類別路徑在建構時是固定的,無法更改。

  • 沒有延遲類別載入,可執行檔中發佈的所有內容都會在啟動時載入到記憶體中。

  • Java 應用程式的某些方面存在一些限制,並未完全支援。

除了這些差異之外,Spring 還使用一個稱為 Spring 事前處理 的流程,這會施加進一步的限制。請務必至少閱讀下一節的開頭,以了解這些限制。

關於 GraalVM 限制的更多細節,請參閱 GraalVM 參考文件中原生映像相容性指南的部分。

理解 Spring 的提前編譯處理 (AOT)

典型的 Spring Boot 應用程式相當動態,其組態設定是在執行時期完成的。事實上,Spring Boot 自動組態的概念很大程度上依賴於對執行時期狀態的反應,才能正確地設定。

雖然可以告知 GraalVM 應用程式的這些動態面向,但這樣做會抵消大部分靜態分析的優點。因此,當使用 Spring Boot 建立原生映像時,會假設一個封閉世界,並限制應用程式的動態面向。

除了GraalVM 本身造成的限制之外,封閉世界的假設還意味著以下限制:

  • 應用程式中定義的 Bean 無法在執行時期更改,這表示:

    • Spring 的 @Profile 標註和特定設定檔的組態有一些限制

    • 不支援在建立 Bean 時會變更的屬性(例如,@ConditionalOnProperty.enable 屬性)。

當這些限制到位時,Spring 就可以在建置時期執行提前編譯處理,並產生 GraalVM 可以使用的額外資源。經過 Spring AOT 處理的應用程式通常會產生:

  • Java 原始碼

  • 位元組碼(用於動態代理等)

  • GraalVM JSON 提示檔案

    • 資源提示 (resource-config.json)

    • 反射提示 (reflect-config.json)

    • 序列化提示 (serialization-config.json)

    • Java 代理提示 (proxy-config.json)

    • JNI 提示 (jni-config.json)

原始碼產生

Spring 應用程式由 Spring Bean 組成。在內部,Spring Framework 使用兩個不同的概念來管理 Bean。一個是 Bean 實例,它是已建立的實際實例,可以注入到其他 Bean 中。另一個是 Bean 定義,用於定義 Bean 的屬性以及如何建立其實例。

以一個典型的 @Configuration 類別為例:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MyConfiguration {

	@Bean
	public MyBean myBean() {
		return new MyBean();
	}

}

Bean 定義是透過解析 @Configuration 類別並找到 @Bean 方法來建立的。在上面的範例中,我們為名為 myBean 的單例 Bean 定義了一個 BeanDefinition。我們也為 MyConfiguration 類別本身建立了一個 BeanDefinition

當需要 myBean 實例時,Spring 知道它必須呼叫 myBean() 方法並使用結果。在 JVM 上執行時,@Configuration 類別的解析發生在應用程式啟動時,並且使用反射呼叫 @Bean 方法。

建立原生映像時,Spring 的運作方式不同。它不是在執行時期解析 @Configuration 類別並產生 Bean 定義,而是在建置時期執行此操作。找到 Bean 定義後,會將其處理並轉換為 GraalVM 編譯器可以分析的原始碼。

Spring AOT 程序會將上述組態類別轉換為如下程式碼:

import org.springframework.beans.factory.aot.BeanInstanceSupplier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;

/**
 * Bean definitions for {@link MyConfiguration}.
 */
public class MyConfiguration__BeanDefinitions {

	/**
	 * Get the bean definition for 'myConfiguration'.
	 */
	public static BeanDefinition getMyConfigurationBeanDefinition() {
		Class<?> beanType = MyConfiguration.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(MyConfiguration::new);
		return beanDefinition;
	}

	/**
	 * Get the bean instance supplier for 'myBean'.
	 */
	private static BeanInstanceSupplier<MyBean> getMyBeanInstanceSupplier() {
		return BeanInstanceSupplier.<MyBean>forFactoryMethod(MyConfiguration.class, "myBean")
			.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(MyConfiguration.class).myBean());
	}

	/**
	 * Get the bean definition for 'myBean'.
	 */
	public static BeanDefinition getMyBeanBeanDefinition() {
		Class<?> beanType = MyBean.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(getMyBeanInstanceSupplier());
		return beanDefinition;
	}

}
產生的確切程式碼可能會根據 Bean 定義的性質而有所不同。

您可以看到上面產生的程式碼建立了與 @Configuration 類別等效的 Bean 定義,但採用 GraalVM 可以理解的直接方式。

有一個針對 myConfiguration Bean 的 Bean 定義,以及一個針對 myBean 的 Bean 定義。當需要 myBean 實例時,會呼叫 BeanInstanceSupplier。此供應器將在 myConfiguration Bean 上呼叫 myBean() 方法。

在 Spring AOT 處理期間,您的應用程式會啟動到 Bean 定義可用的程度。在 AOT 處理階段不會建立 Bean 實例。

Spring AOT 會為所有 Bean 定義產生程式碼。當需要 Bean 後處理時(例如,呼叫 @Autowired 方法),它也會產生程式碼。同時也會產生一個 ApplicationContextInitializer,Spring Boot 會在 AOT 處理過的應用程式實際運行時使用它來初始化 ApplicationContext

雖然 AOT 產生的原始程式碼可能很冗長,但它具有相當的可讀性,並且在除錯應用程式時很有幫助。使用 Maven 時,產生的原始碼檔案可以在 target/spring-aot/main/sources 中找到;使用 Gradle 時,則可以在 build/generated/aotSources 中找到。

提示檔案產生

除了產生原始碼檔案之外,Spring AOT 引擎還會產生 GraalVM 使用的提示檔案。提示檔案包含 JSON 資料,描述 GraalVM 應如何處理它無法透過直接檢查程式碼來理解的部分。

例如,您可能在私有方法上使用了 Spring 註釋。即使在 GraalVM 上,Spring 也需要使用反射來呼叫私有方法。當出現這種情況時,Spring 可以寫入反射提示,以便 GraalVM 知道即使沒有直接呼叫私有方法,它仍然需要在原生映像檔中可用。

提示檔案會產生在 META-INF/native-image 下,GraalVM 會自動讀取它們。

使用 Maven 時,產生的提示檔案可以在 target/spring-aot/main/resources 中找到;使用 Gradle 時,則可以在 build/generated/aotResources 中找到。

代理類別產生

Spring 有時需要產生代理類別,以使用額外功能增強您編寫的程式碼。為此,它使用直接產生位元組碼的 cglib 函式庫。

當應用程式在 JVM 上運行時,代理類別會在應用程式運行時動態產生。建立原生映像檔時,需要在建置時建立這些代理,以便 GraalVM 可以包含它們。

與原始程式碼產生不同,產生的位元組碼在除錯應用程式時並不是特別有用。但是,如果您需要使用 javap 等工具檢查 .class 檔案的內容,則可以在 Maven 的 target/spring-aot/main/classes 和 Gradle 的 build/generated/aotClasses 中找到它們。