Spring AOT介紹

瘋狂小兵發表於2023-02-26

Spring對AOT最佳化的支援意味著將哪些通常在執行時才發生的事情提前到編譯期做,包括在構建時檢查ApplicationContext,支援決策和發現執行邏輯。這樣做可以構建一個更直接的應用程式啟動安排,並主要基於類路徑和環境來關注一組固定的特性。

支援這樣的最佳化意味著需要對原Spring應用做如下的限制:

  1. classpath是固定的,並在在構建時就已經全部指定了。
  2. bean的定義在執行時不能改變。

    1. @Profile,特別是需要在構建時選擇特定於配置檔案的配置
    2. 影響bean存在的環境屬性配置@Conditional僅能在構建時考慮
  3. 帶有Supplier(包括lambda和方法引用)的Bean的定義不能被AOT轉換。
  4. @Bean註解的方法的返回型別得是具體的類,而不能是介面了,以便允許正確的提示推斷。

當以上的限制都避免了,就可以在構建時執行AOT的處理並生成額外的資產。

經過Spring AOT處理過的應用,透過會生成如下資產:

  1. Java原始碼
  2. 位元組碼
  3. RuntimeHints,用於反射,資源定位,序列化和Java反射

在當前情況下,Spring AOT專注於使用GraalVM將Spring的應用部署為原生的映象,後續可能會支援更多的JVM。

AOT引擎介紹

用於處理ApplicationContext排列的AOT引擎的入口點是ApplicationContextAotGenerator.它負責以下步驟,其基於的引數GenericApplicationContext表示要被最佳化的應用,和一個通用的上下文引數GenerationContext.

  1. 重新整理用於AOT處理的ApplicationContext。與傳統的重新整理不同,此版本只建立bean定義,而不是bean例項
  2. 呼叫可用的BeanFactoryInitializationAotProcessor的具體實現,並對GenerationContext使用。例如,核心實現 迭代所有候選bean definition,並生成必要的程式碼以恢復BeanFactory的狀態。

一旦該處理完成,GenerationContext將被那些應用執行所必須的已生成程式碼、資源和類更新。RuntimeHints例項可以用於生成與GraalVM相關的原生映象配置檔案。

ApplicationContextAotGenerator#processAheadOfTime返回ApplicationContextInitializer入口點的類名,該入口點允許使用AOT最佳化啟動上下文。

重新整理AOT的處理

所有GenericApplicationContext的實現都支援AOT處理的重新整理。應用程式上下文由任意數量的入口點建立,通常以@Configuration註解類的形式。

通常的實現如下:

@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {
}

使用常規執行時啟動此應用程式涉及許多步驟,包括類路徑掃描、配置類解析、bean例項化和生命週期回撥處理。AOT處理的重新整理僅應用常規重新整理的子集。AOT處理可按如下方式觸發:

RuntimeHints hints = new RuntimeHints();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing(hints);
// ...
context.close();

在AOT模式下,BeanFactoryPostProcessor擴充套件點的實現和平時一樣呼叫。包括configuration類的解析、import selector和類掃描等。這些步驟確保BeanRegistry包含應用程式的相關bean定義.如果Bean definition收到conditions(如 @Profile)的保護,則在該階段會被拋棄。因為此模式實際上不建立Bean的例項,除了與AOT相關的變體實現之外,BeanPostProcessor將不會被呼叫。變體實現包括:

  1. MergedBeanDefinitionPostProcessor的實現,後處理bean定義以提取其他設定,如initdestroy方法
  2. SmartInstantiationAwareBeanPostProcessor的實現,如果需要,確定更精確的bean型別,這確保建立執行時需要的任何代理類。

一旦該步驟完成,BeanFactory就包含了應用執行所必須的bean definition 集合。它不觸發bean例項化,但允許AOT引擎檢查將在執行時建立的bean。

Bean工廠初始化AOT貢獻

希望參與此步驟的元件可以實現BeanFactoryInitializationAotProcessor介面。每個實現都可以根據bean工廠的狀態返回AOT貢獻。

AOT貢獻是貢獻生成的程式碼可以再現特定行為的元件。它還可以提供RuntimeHints來指示反射、資源載入、序列化或JDK代理的需要.

BeanFactoryInitializationAotProcessor的實現可以註冊在META-INF/spring/aot.factories中,key為該介面的全限定名。

BeanFactoryInitializationAotProcessor也可以直接被一個bean實現。在這種模式下,bean提供的AOT貢獻與它在常規執行時提供的特性相當。因此,這樣的bean會自動從AOT最佳化上下文中排除。

注意: 如果bean實現了BeanFactoryInitializationAotProcessor介面,那麼在AOT處理期間將初始化bean及其所有依賴項。我們通常建議此介面僅由基礎結構bean(如BeanFactoryPostProcessor)實現,這些bean具有有限的依賴性,並且在bean工廠生命週期的早期就已經初始化。如果這樣的bean是使用@bean工廠方法註冊的,請確保該方法是靜態的,以便其封閉的@Configuration類不必初始化。

Bean註冊AOT貢獻

BeanFactoryInitializationAotProcessor實現的核心功能是負責為每個候選BeanDefinition收集必要的貢獻。它使用專用的BeanRegistryAotProcessor來實現。

該介面的使用方式如下:

  1. BeanPostProcessorbean實現,以替換其執行時行為。例如,AutowiredAnnotationBeanPostProcessor實現了這個介面,以生成注入用@Autowired註釋的成員的程式碼。
  2. META-INF/spring/aot.factors中註冊的型別實現,其key等於介面的完全限定名稱。通常在需要針對核心框架的特定特性進行調整的bean定義時使用。

注意: 如果一個bean實現了BeanRegistryAotProcessor介面,那麼在AOT處理期間將初始化該bean及其所有依賴項。我們通常建議此介面僅由基礎結構bean(如BeanFactoryPostProcessor)實現,這些bean具有有限的依賴性,並且在bean工廠生命週期的早期就已經初始化。如果這樣的bean是使用@bean工廠方法註冊的,請確保該方法是靜態的,以便其封閉的@Configuration類不必初始化。

如果沒有BeanRegisterationAotProcessor處理特定註冊的bean,則預設實現會處理它。這是預設行為,因為為bean definition 調整生成的程式碼應該僅限於比較冷門的使用案例。

以前面的示例為例,我們假設DataSourceConfiguration如下:

@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

    @Bean
    public SimpleDataSource dataSource() {
        return new SimpleDataSource();
    }

}

由於該類上沒有任何特定條件,因此dataSourceConfigurationdataSource被標識為候選項。AOT引擎會將上面的配置類轉換為與以下類似的程式碼:

/**
 * Bean definitions for {@link DataSourceConfiguration}
 */
public class DataSourceConfiguration__BeanDefinitions {
    /**
     * Get the bean definition for 'dataSourceConfiguration'
     */
    public static BeanDefinition getDataSourceConfigurationBeanDefinition() {
        Class<?> beanType = DataSourceConfiguration.class;
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
        beanDefinition.setInstanceSupplier(DataSourceConfiguration::new);
        return beanDefinition;
    }

    /**
     * Get the bean instance supplier for 'dataSource'.
     */
    private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() {
        return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource")
                .withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource());
    }

    /**
     * Get the bean definition for 'dataSource'
     */
    public static BeanDefinition getDataSourceBeanDefinition() {
        Class<?> beanType = SimpleDataSource.class;
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
        beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier());
        return beanDefinition;
    }
}

根據bean定義的確切性質,生成的確切程式碼可能有所不同。

上面生成的程式碼建立了與@Configuration類等效的bean定義,但以直接的方式,如果可能的話,不使用反射。dataSourceConfiguration有一個bean定義,dataSourceBean有一個。當需要資料來源例項時,將呼叫BeanInstance Supplier。此Supplier呼叫dataSourceConfiguration bean上的dataSource()方法。

執行時提示(Runtime Hints)

與常規JVM執行時相比,將應用程式作為native image 執行需要額外的資訊。例如,GraalVM需要提前知道元件是否使用反射。類似地,除非明確指定,否則類路徑資源不會在native image中提供。因此,如果應用程式需要載入資源,則必須從相應的GraalVM native image 配置檔案中引用該資源。

RuntimeHints的API收集執行時對反射、資源載入、序列化和JDK代理的需求。以下示例確保config/app.properties可以在執行時從本機映像中的類路徑載入。

runtimeHints.resources().registerPattern("config/app.properties");

在AOT處理過程中,會自動處理許多合同。例如:檢查@Controller方法的返回型別,如果Spring檢測到型別應該序列化(通常為JSON),則新增相關的反射提示。

對於核心容器無法推斷的情況,可以以程式設計方式註冊此類提示。還為常見用例提供了許多方便的註釋。

@ImportRuntimeHints

RuntimeHintsRegister實現允許您獲取對AOT引擎管理的RuntimeHints例項的回撥。可以在任何Spring的bean例項或@bean工廠方法上使用@ImportRuntimeHints註冊此介面的實現。在構建時檢測並呼叫RuntimeHintsRegister實現。

@Component
@ImportRuntimeHints(SpellCheckService.SpellCheckServiceRuntimeHints.class)
public class SpellCheckService {

    public void loadDictionary(Locale locale) {
        ClassPathResource resource = new ClassPathResource("dicts/" + locale.getLanguage() + ".txt");
        //...
    }

    static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.resources().registerPattern("dicts/*");
        }
    }

}

如果可能,@ImportRuntimeHints應儘可能靠近需要提示的元件使用。這樣,如果元件沒有被貢獻給BeanFactoryhints也不會被貢獻。

@Reflective

@Reflective提供了一種慣用的方法來標記對帶註解元素的反射的需要。例如,@EventListener使用@Reflective進行元註釋,因為底層實現使用反射呼叫註釋方法.

預設情況下,只考慮Spring的bean,併為帶註解的元素註冊呼叫提示。這可以透過@Reflective註解指定自定義ReflectiveProcessor實現來調整。

庫作者可以出於自己的目的重用此註釋。如果需要處理Spring bean以外的元件,BeanFactoryInitializationAotProcessor可以檢測相關型別並使用ReflectiveRuntimeHintsRegister來處理它們。

@RegisterReflectionForBinding

@RegisterReflectionForBinding@Reflective的特例,它註冊了序列化任意型別的需要。典型的用例是容器無法推斷的DTO的使用,例如在方法體中使用web客戶端。

@RegisterReflectionForBinding可以應用於類級別的任何Spring bean,但也可以直接應用於方法、欄位或建構函式,以更好地指示實際需要提示的位置。以下示例 註冊Account以進行序列化。

@Component
public class OrderService {

    @RegisterReflectionForBinding(Account.class)
    public void process(Order order) {
        // ...
    }

}

測試 Runtime Hints

Spring Core還提供RuntimeHintsPredices,這是一個用於檢查現有提示是否匹配特定用例的實用程式。這可以在您自己的測試中使用,以驗證RuntimeHintsRegister是否包含預期結果。我們可以為我們的SpellCheckService編寫測試,並確保我們能夠在執行時載入字典:

@Test
void shouldRegisterResourceHints() {
    RuntimeHints hints = new RuntimeHints();
    new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
    assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
            .accepts(hints);
}

使用RuntimeHintsPredices,我們可以檢查反射、資源、序列化或代理生成提示。這種方法適用於單元測試,但意味著元件的執行時行為是眾所周知的。透過使用GraalVM跟蹤代理執行應用程式的測試套件(或應用程式本身),可以瞭解有關應用程式全域性執行時行為的更多資訊。該代理將在執行時記錄所有需要GraalVM提示的相關呼叫,並將其作為JSON配置檔案寫入。

為了更具針對性的發現和測試,Spring Framework提供了一個帶有核心AOT測試實用程式的專用模組,“org.springframework:Spring-core測試”。此模組包含RuntimeHints Agent,這是一個Java代理,它記錄與執行時提示相關的所有方法呼叫,並幫助您斷言給定的RuntimeHinds例項覆蓋所有記錄的呼叫。讓我們考慮一個基礎設施,我們希望測試在AOT處理階段提供的提示。

public class SampleReflection {

    private final Log logger = LogFactory.getLog(SampleReflection.class);

    public void performReflection() {
        try {
            Class<?> springVersion = ClassUtils.forName("org.springframework.core.SpringVersion", null);
            Method getVersion = ClassUtils.getMethod(springVersion, "getVersion");
            String version = (String) getVersion.invoke(null);
            logger.info("Spring version:" + version);
        }
        catch (Exception exc) {
            logger.error("reflection failed", exc);
        }
    }
}

然後,我們可以編寫一個單元測試(不需要本機編譯),檢查我們提供的提示:

@EnabledIfRuntimeHintsAgent
class SampleReflectionRuntimeHintsTests {

    @Test
    void shouldRegisterReflectionHints() {
        RuntimeHints runtimeHints = new RuntimeHints();
        // Call a RuntimeHintsRegistrar that contributes hints like:
        runtimeHints.reflection().registerType(SpringVersion.class, typeHint ->
                typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE));

        // Invoke the relevant piece of code we want to test within a recording lambda
        RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
            SampleReflection sample = new SampleReflection();
            sample.performReflection();
        });
        // assert that the recorded invocations are covered by the contributed hints
        assertThat(invocations).match(runtimeHints);
    }
}

如果您忘記提供提示,測試將失敗,並提供有關呼叫的一些詳細資訊:

org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection
INFO: Spring version:6.0.0-SNAPSHOT

Missing <"ReflectionHints"> for invocation <java.lang.Class#forName>
with arguments ["org.springframework.core.SpringVersion",
    false,
    jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7].
Stacktrace:
<"org.springframework.util.ClassUtils#forName, Line 284
io.spring.runtimehintstesting.SampleReflection#performReflection, Line 19
io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldRegisterReflectionHints$0, Line 25

該文章的內容來自於Spring官方手冊,原文內容:https://docs.spring.io/spring...

相關文章