Средства поддержки AOT-оптимизации в Spring предназначены для просмотра ApplicationContext
во время сборки, а также применения решений и логики обнаружения, которые обычно имеют место во время выполнения. Это позволяет создать более простую схему запуска приложения, ориентированную на фиксированный набор функций, основанных в основном на classpath и Environment
.
Применение такой оптимизации на ранних стадиях подразумевает следующие ограничения:
-
Classpath фиксирован и полностью определен во время сборки.
-
Бины, определенные в приложении, нельзя изменять во время выполнения, а это значит, что:
-
@Profile
, в частности, конфигурацию, характерная для конкретного профиля, необходимо выбрать во время сборки. -
Свойства окружения, влияющие на наличие бина (аннотация
@Conditional
), учитываются только во время сборки.
-
Если эти ограничения были введены, становится возможным выполнять обработку перед выполнением во время сборки и генерировать дополнительные ресурсы. Обрабатываемое перед выполнением приложение Spring обычно генерирует:
-
• Исходный Java-код
-
• Байт-код (обычно для динамических прокси)
-
RuntimeHints
для использования отражения, загрузки ресурсов, сериализации и прокси JDK.
Обзор AOT-движка
Точкой входа AOT-движка для обработки компоновки ApplicationContext
является ApplicationContextAotGenerator
. Он выполняет следующие шаги, основываясь на GenericApplicationContext
, который представляет оптимизируемое приложение, и GenerationContext
:
-
Обновление
ApplicationContext
для AOT-обработки. В отличие от традиционного обновления, эта версия создает только определения бинов, а не их экземпляры. -
Вызов доступных реализаций
BeanFactoryInitializationAotProcessor
и применение результатов их работы кGenerationContext
. Например, основная реализация осуществляет перебор всех определений бинов-кандидатов и генерирует необходимый код для восстановления состоянияBeanFactory
.
После завершения этого процесса GenerationContext
будет обновлен сгенерированным кодом, ресурсами и классами, необходимыми для запуска приложения. Экземпляр RuntimeHints
также можно использовать для генерации соответствующих конфигурационных файлов нативного образа GraalVM.
ApplicationContextAotGenerator#processAheadOfTime
возвращает имя класса точки входа ApplicationContextInitializer, которая позволяет запустить контекст с AOT-оптимизацией.
Более подробно эти шаги описаны в следующих разделах.
Обновление для обработки AOT
Обновление для обработки AOT поддерживается во всех реализациях GenericApplicationContext
. Контекст приложения создается с помощью любого количества точек входа, обычно в виде классов с добавлением аннотации @Configuration
.
Давайте рассмотрим базовый пример:
@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {
}
Запуск этого приложения с помощью обычной среды выполнения предусматривает ряд этапов, среди которых в том числе сканирование classpath, парсинг конфигурационного класса, создание экземпляров бинов и обработка обратных вызовов жизненного цикла. В процессе обновления при AOT-обработке применяется только часть всех процессов, которые имеют место при обычном обновление
. AOT-обработку можно запустить следующим образом:
RuntimeHints hints = new RuntimeHints();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing(hints);
В этом режиме реализации BeanFactoryPostProcessor
вызываются в обычном порядке. Этот процесс предусматривает парсинг конфигурационных классов, использование селекторов импорта, сканирование classpath и т.д. Такие шаги позволяют убедиться, что BeanRegistry содержит соответствующие определения бинов для приложения. Если определения бинов защищены условиями (например, аннотацией @Profile
), они на данном этапе игнорируются.
Поскольку в этом режиме экземпляры бинов фактически не создаются, реализации BeanPostProcessor
не будут вызываться, за исключением специфических вариантов, имеющих отношение к AOT-обработке. К ним относятся:
-
Реализации
MergedBeanDefinitionPostProcessor
, которые осуществляют постобработку определений бинов для извлечения дополнительных параметров, таких как методыinit
иdestroy.
-
Реализации
SmartInstantiationAwareBeanPostProcessor
, которые при необходимости определяют более точный тип бина. Это позволяет создавать все необходимые во время выполнения прокси.
После завершения этой части BeanFactory
будет содержать определения бинов, которые необходимы для работы приложения. Эта фабрика бинов не запускает создание экземпляров бинов, но позволяет AOT-движку просматривать бины, которые будут созданы во время выполнения.
Инициализация фабрики бинов для AOT-вкладов
Компоненты, которые будут принимать участие в работе на этом этапе, могут реализовать интерфейс BeanFactoryInitializationAotProcessor
. Каждая реализация может возвращать AOT-вклад на основании состояния фабрики бинов.
АOT-вклад – это компонент, который вносит сгенерированный код, воспроизводящий определенную логику работы. Он также может вносить RuntimeHints
, чтобы обозначить необходимость в отражении, загрузке ресурсов, сериализации или создании прокси JDK.
Реализация BeanFactoryInitializationAotProcessor
может быть зарегистрирована в META-INF/spring/aot.factories
с ключом, эквивалентным полному имени интерфейса.
BeanFactoryInitializationAotProcessor
также может быть реализован непосредственно самим бином. В этом режиме бин обеспечивает AOT-вклад, эквивалентный тому, который он обеспечивает в обычной среде выполнения. Следовательно, такой бин автоматически исключается из оптимизируемого по принципу AOT контекста.
Если бин реализует интерфейс BeanFactoryInitializationAotProcessor
, то бин и все его зависимости будут инициализированы во время AOT-обработки. Обычно мы рекомендуем, чтобы этот интерфейс реализовывался только инфраструктурными бинами, такими как BeanFactoryPostProcessor
, которые имеют ограниченные зависимости и уже инициализированы на ранних стадиях жизненного цикла фабрики бинов. Если такой бин регистрируется с помощью фабричного метода с аннотацией @Bean
, убедитесь, что этот метод является статичным, чтобы не пришлось инициализировать его вложенный класс с аннотацией @Configuration
.
Регистрация бинов для АОТ-вкладов
Основная реализация BeanFactoryInitializationAotProcessor
отвечает за сбор необходимых вкладов для каждого кандидата BeanDefinition
. Для этого используется специализированный BeanRegistrationAotProcessor
.
Этот интерфейс используется следующим образом:
-
Реализуется бином
BeanPostProcessor
, чтобы заменить его логику работы во время выполнения. Например,AutowiredAnnotationBeanPostProcessor
реализует этот интерфейс для генерации кода, который внедряет члены, помеченные аннотацией@Autowired
. -
Реализуется типом, зарегистрированным в
META-INF/spring/aot.factories
с ключом, эквивалентным полному имени интерфейса. Обычно используется, если определение бина необходимо подстроить под характерные особенности основного фреймворка.
Если бин реализует интерфейс BeanRegistrationAotProcessor
, то бин и все его зависимости будут инициализированы во время AOT-обработки. Обычно мы рекомендуем, чтобы этот интерфейс реализовывался только инфраструктурными бинами, такими как BeanFactoryPostProcessor
, которые имеют ограниченные зависимости и уже инициализированы на ранних стадиях жизненного цикла фабрики бинов. Если такой бин регистрируется с помощью фабричного метода с аннотацией @Bean
, убедитесь, что этот метод является статичным, чтобы не пришлось инициализировать его вложенный класс с аннотацией @Configuration
.
Если ни один BeanRegistrationAotProcessor
не обрабатывает конкретный зарегистрированный бин, его обрабатывает реализация по умолчанию. Это является логикой работы по умолчанию, поскольку настройка сгенерированного кода для определения бина должна быть ограничена тупиковыми ситуациями.
Взяв наш предыдущий пример, предположим, что DataSourceConfiguration
выглядит следующим образом:
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {
@Bean
public SimpleDataSource dataSource() {
return new SimpleDataSource();
}
}
Поскольку к этому классу не применяется никаких особых условий, dataSourceConfiguration
и dataSource
определяются как кандидаты. AOT-движок преобразует приведенный выше конфигурационный класс в код, подобный следующему:
/**
* Определения бинов для {@link DataSourceConfiguration}
*/
public class DataSourceConfiguration__BeanDefinitions {
/**
* Получаем определение бина для 'dataSourceConfiguration'.
*/
public static BeanDefinition getDataSourceConfigurationBeanDefinition() {
Class<?> beanType = DataSourceConfiguration.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
beanDefinition.setInstanceSupplier(DataSourceConfiguration::new);
return beanDefinition;
}
/**
* Получаем экземпляр бина поставщика для "dataSource".
*/
private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() {
return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource")
.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource());
}
/**
* Получаем определение бина для "dataSource".
*/
public static BeanDefinition getDataSourceBeanDefinition() {
Class<?> beanType = SimpleDataSource.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier());
return beanDefinition;
}
}
Сгенерированный выше код создает определения бинов, эквивалентные классу с аннотацией @Configuration
, но прямым способом и без использования отражения, если это вообще возможно. Будет существовать определение бина для dataSourceConfiguration
и одно для dataSourceBean
. Если потребуется экземпляр datasource
, будет вызван BeanInstanceSupplier
. Этот поставщик вызывает метод dataSource()
в бинах dataSourceConfiguration
.
Подсказки во время выполнения
Запуск приложения в виде нативного образа требует дополнительной информации в сравнении с обычным выполнением на JVM. Например, GraalVM необходимо заранее знать, использует ли компонент отражение. Аналогично, ресурсы classpath не поставляются в нативном образе, если это не задано явным образом. Следовательно, если приложению необходимо загрузить ресурс, на него нужно сослаться из соответствующего конфигурационного файла нативного образа GraalVM.
RuntimeHints
API собирает информацию о необходимости отражения, загрузки ресурсов, сериализации и создания прокси JDK во время выполнения. В следующем примере показано, что config/app.properties
гарантированно будет загружен из classpath во время выполнения в рамках нативного образа:
runtimeHints.resources().registerPattern("config/app.properties");
Ряд контрактов обрабатывается автоматически во время AOT-обработки. Например, проверяется возвращаемый тип метода с аннотацией @Controller
, а также добавляются соответствующие подсказки отражения, если Spring обнаруживает, что тип необходимо сериализовать (обычно в JSON).
В случаях, если основной контейнер не может выполнить вывод, можно зарегистрировать такие подсказки программно. Также предусмотрен ряд вспомогательных аннотаций для распространенных сценариев использования.
Аннотация @ImportRuntimeHints
Реализации RuntimeHintsRegistrar
позволяют получить обратный вызов экземпляра RuntimeHints, управляемого AOT-движком. Реализации этого интерфейса могут быть зарегистрированы через снабжение аннотацией @ImportRuntimeHints
любого бина Spring или фабрики бинов, помеченной аннотацией @Bean. Реализации RuntimeHintsRegistrar
обнаруживаются и вызываются во время сборки.
@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
следует использовать как можно ближе к компоненту, которому требуются подсказки. Таким образом, если компонент не будет внесен в BeanFactory
, подсказки также не будут внесены.
Также можно зарегистрировать реализацию статически, добавив запись в META-INF/spring/aot.factories
с ключом, эквивалентным полному имени интерфейса RuntimeHintsRegistrar
.
Аннотация @Reflective
Аннотация @Reflective
позволяет идиоматическим способом помечать необходимость отражения для аннотированного элемента. Например, аннотация @EventListener
мета-аннотируется аннотацией @Reflective
, поскольку базовая реализация вызывает аннотированный метод через отражение.
По умолчанию учитываются только бины Spring, а для аннотированного элемента регистрируется подсказка вызова. Эту логику работы можно тонко настроить, задав кастомную реализацию ReflectiveProcessor
через аннотацию @Reflective
.
Авторы библиотек могут повторно использовать эту аннотацию в своих целях. Если необходимо обработать компоненты, отличные от бинов Spring, BeanFactoryInitializationAotProcessor
может определить соответствующие типы и использовать ReflectiveRuntimeHintsRegistrar
для их обработки.
Аннотация @RegisterReflectionForBinding
Аннотация @RegisterReflectionForBinding
– это конкретизация аннотации @Reflective
, которая регистрирует необходимость сериализации произвольных типов. Типичным случаем является использование DTO, которые контейнер не может вывести, например, при использовании веб-клиента в теле метода.
Аннотацию @RegisterReflectionForBinding
можно применить к любому бину Spring на уровне класса, но также её можно применить и непосредственно к методу, полю или конструктору, чтобы лучше обозначить, где именно будут необходимы подсказки. В следующем примере регистрируется Account
для сериализации.
@Component
public class OrderService {
@RegisterReflectionForBinding(Account.class)
public void process(Order order) {
// ...
}
}
Подсказки для тестирования во время выполнения
В составе Spring Core также поставляется RuntimeHintsPredicates
, утилита для проверки соответствия существующих подсказок определенному сценарию использования. Её можно использовать в собственных тестах, чтобы валидировать, что RuntimeHintsRegistrar
содержит ожидаемые результаты. Мы можем написать тест для нашего SpellCheckService
и убедиться, что сможем загрузить словарь во время выполнения:
@Test
void shouldRegisterResourceHints() {
RuntimeHints hints = new RuntimeHints();
new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
.accepts(hints);
}
При помощи RuntimeHintsPredicates
можно проверять наличие подсказок, сигнализирующих о необходимости отражения, заргрузки ресурсов, сериализации или генерации прокси. Этот подход отлично подходит для модульных тестов, но подразумевает, что логика работы компонента во время выполнения должна быть хорошо изучена.
Можно узнать больше о глобальной логике работы приложения во время выполнения, запустив его тестовый комплект (или само приложение) с использованием агента трассировки GraalVM. Этот агент будет регистрировать все соответствующие вызовы, требующие вывода подсказок из GraalVM во время выполнения, и записывать их в виде конфигурационных файлов в формате JSON.
Для более целенаправленного обнаружения и тестирования в составе Spring Framework поставляется специальный модуль, содержащий основные утилиты для AOT-тестирования, "org.springframework:spring-core-test"
. Этот модуль содержит RuntimeHints Agent, Java-агент, который регистрирует все вызовы методов, связанные с подсказками среды выполнения, и помогает добавить утверждение о том, что данный экземпляр RuntimeHints
охватывает все зарегистрированные вызовы. Рассмотрим часть инфраструктуры, для которой мы хотели бы протестировать подсказки, вносимые на этапе 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 сообщает о том, что аннотированный тестовый класс или тестовый
// метод должны активироваться только в том случае, если RuntimeHintsAgent был загружен в текущей JVM.
// Эта аннотация также помечает тесты JUnit-тегом "RuntimeHints".
@EnabledIfRuntimeHintsAgent
class SampleReflectionRuntimeHintsTests {
@Test
void shouldRegisterReflectionHints() {
RuntimeHints runtimeHints = new RuntimeHints();
// Вызываем RuntimeHintsRegistrar, который вносит подсказки типа:
runtimeHints.reflection().registerType(SpringVersion.class, typeHint ->
typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE));
// Обращаемся к соответствующему фрагменту кода, который необходимо протестировать, в рамках записи лямбда-выражения
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
SampleReflection sample = new SampleReflection();
sample.performReflection();
});
// добавляем утверждение, что на зарегистрированные вызовы распространяются внесенные подсказки
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
Существуют различные способы конфигурирования этого Java-агента в вашей сборке, поэтому, пожалуйста, обратитесь к документации для вашего средства сборки и плагина выполнения тестов. Сам агент можно сконфигурировать на инструментирование определенных пакетов (по умолчанию инструментирование осуществляется только для org.springframework
). Более подробную информацию вы найдете в README-файле к buildSrc
для Spring Framework .
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ