GraalVM 原生映象簡介
GraalVM 原生映象提供了一種部署和執行 Java 應用的新方式。與 Java 虛擬機器相比,原生映象佔用的記憶體更少,啟動速度更快。
它們非常適合使用容器映象部署的應用,並且與“函式即服務”(FaaS)平臺結合使用時尤為有趣。
與為 JVM 編寫的傳統應用不同,GraalVM 原生映象應用需要提前處理才能建立可執行檔案。這種提前處理包括從主入口點對你的應用程式碼進行靜態分析。
GraalVM 原生映象是一個完整的、特定於平臺的可執行檔案。執行原生映象不需要攜帶 Java 虛擬機器。
如果你只想快速入門並嘗試 GraalVM,可以跳到“開發你的第一個 GraalVM 原生應用”部分,稍後返回此部分。 |
與 JVM 部署的主要區別
GraalVM 原生映象提前生成意味著原生應用和基於 JVM 的應用之間存在一些關鍵區別。主要區別包括
-
從主入口點在構建時對你的應用進行靜態分析。
-
在建立原生映象時無法到達的程式碼將被移除,並且不會成為可執行檔案的一部分。
-
GraalVM 不能直接感知程式碼中的動態元素,必須告知它有關反射、資源、序列化和動態代理的資訊。
-
應用類路徑在構建時固定,不能更改。
-
沒有延遲類載入,可執行檔案中包含的所有內容將在啟動時載入到記憶體中。
-
Java 應用的某些方面存在一些侷限性,並非完全支援。
除了這些區別之外,Spring 還使用一種稱為Spring 提前(AOT)處理的機制,這會施加進一步的限制。請務必閱讀下一部分的開頭,以瞭解這些內容。
GraalVM 參考文件的“原生映象相容性指南”部分提供了有關 GraalVM 限制的更多詳細資訊。 |
理解 Spring 提前(AOT)處理
典型的 Spring Boot 應用非常動態,配置在執行時執行。事實上,Spring Boot 自動配置的概念很大程度上依賴於對執行時狀態的響應來正確配置各項內容。
雖然可以將應用的這些動態方面告知 GraalVM,但這樣做會抵消大部分靜態分析的好處。因此,在使用 Spring Boot 建立原生映象時,假定為封閉世界(closed-world),並限制應用的動態方面。
封閉世界的假設意味著,除了GraalVM 本身造成的限制之外,還存在以下限制
-
你的應用中定義的 Bean 不能在執行時更改,這意味著
-
如果建立 Bean 時更改的屬性不受支援(例如,
@ConditionalOnProperty
和.enabled
屬性)。
當這些限制到位後,Spring 就有可能在構建時執行提前(AOT)處理,並生成 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 定義性質而異。 |
你可以看到,生成的程式碼建立的 Bean 定義與@Configuration
類等價,但以 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 找到它們。 |