GraalVM 原生映象簡介

GraalVM Native Images 提供了一種部署和執行 Java 應用程式的新方式。與 Java 虛擬機器相比,原生映象可以以更小的記憶體佔用和更快的啟動時間執行。

它們非常適合使用容器映象部署的應用程式,並且與“函式即服務”(FaaS)平臺結合使用時特別有意義。

與為 JVM 編寫的傳統應用程式不同,GraalVM Native Image 應用程式需要提前處理才能建立可執行檔案。這種提前處理涉及到從其主要入口點對應用程式程式碼進行靜態分析。

GraalVM Native Image 是一個完整的、平臺特定的可執行檔案。您無需附帶 Java 虛擬機器即可執行原生映象。

如果您只想開始嘗試 GraalVM,可以直接跳轉到開發您的第一個 GraalVM Native 應用程式部分,稍後再返回此部分。

與 JVM 部署的主要區別

GraalVM Native Images 是提前生成的,這意味著原生應用程式和基於 JVM 的應用程式之間存在一些關鍵區別。主要區別在於:

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

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

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

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

  • 沒有惰性類載入,可執行檔案中包含的所有內容都會在啟動時載入到記憶體中。

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

除了這些差異之外,Spring 還使用了一個名為Spring 提前處理的過程,這施加了進一步的限制。請務必閱讀下一節的開頭部分以瞭解這些限制。

GraalVM 參考文件的Native Image 相容性指南部分提供了關於 GraalVM 限制的更多詳細資訊。

理解 Spring 提前處理

典型的 Spring Boot 應用程式是相當動態的,配置在執行時執行。事實上,Spring Boot 自動配置的概念嚴重依賴於對執行時狀態的響應以正確配置事物。

儘管可以將這些應用程式的動態方面告知 GraalVM,但這樣做會抵消靜態分析的大部分好處。因此,在使用 Spring Boot 建立原生映象時,假設一個封閉世界,並且限制了應用程式的動態方面。

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

  • 應用程式中定義的 bean 不能在執行時更改,這意味著:

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

  • Java 原始碼

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

  • 位於 META-INF/native-image/{groupId}/{artifactId}/ 中的 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();
	}

}

透過解析 @Configuration 類並查詢 @Bean 方法來建立 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 註解。Spring 將需要使用反射來呼叫私有方法,即使是在 GraalVM 上。當出現這種情況時,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 中找到它們。
© . This site is unofficial and not affiliated with VMware.