Ahead of Time Optimizations
This chapter covers Spring’s Ahead of Time (AOT) optimizations.
For AOT support specific to integration tests, see Ahead of Time Support for Tests.
Introduction to Ahead of Time Optimizations
Spring’s support for AOT optimizations is meant to inspect an ApplicationContext
at build time and apply decisions and discovery logic that usually happens at runtime.
Doing so allows building an application startup arrangement that is more straightforward and focused on a fixed set of features based mainly on the classpath and the Environment
.
Applying such optimizations early implies the following restrictions:
-
The classpath is fixed and fully defined at build time.
-
The beans defined in your application cannot change at runtime, meaning:
-
@Profile
, in particular profile-specific configuration needs to be chosen at build time. -
Environment
properties that impact the presence of a bean (@Conditional
) are only considered at build time.
-
-
Bean definitions with instance suppliers (lambdas or method references) cannot be transformed ahead-of-time (see related spring-framework#29555 issue).
-
Make sure that the bean type is as precise as possible.
See also the Best Practices section. |
When these restrictions are in place, it becomes possible to perform ahead-of-time processing at build time and generate additional assets. A Spring AOT processed application typically generates:
-
Java source code
-
Bytecode (usually for dynamic proxies)
-
RuntimeHints
for the use of reflection, resource loading, serialization, and JDK proxies.
At the moment, AOT is focused on allowing Spring applications to be deployed as native images using GraalVM. We intend to support more JVM-based use cases in future generations. |
AOT engine overview
The entry point of the AOT engine for processing an ApplicationContext
arrangement is ApplicationContextAotGenerator
. It takes care of the following steps, based on a GenericApplicationContext
that represents the application to optimize and a GenerationContext
:
-
Refresh an
ApplicationContext
for AOT processing. Contrary to a traditional refresh, this version only creates bean definitions, not bean instances. -
Invoke the available
BeanFactoryInitializationAotProcessor
implementations and apply their contributions against theGenerationContext
. For instance, a core implementation iterates over all candidate bean definitions and generates the necessary code to restore the state of theBeanFactory
.
Once this process completes, the GenerationContext
will have been updated with the generated code, resources, and classes that are necessary for the application to run.
The RuntimeHints
instance can also be used to generate the relevant GraalVM native image configuration files.
ApplicationContextAotGenerator#processAheadOfTime
returns the class name of the ApplicationContextInitializer
entry point that allows the context to be started with AOT optimizations.
Those steps are covered in greater detail in the sections below.
Refresh for AOT Processing
Refresh for AOT processing is supported on all GenericApplicationContext
implementations.
An application context is created with any number of entry points, usually in the form of @Configuration
-annotated classes.
Let’s look at a basic example:
@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {
}
Starting this application with the regular runtime involves a number of steps including classpath scanning, configuration class parsing, bean instantiation, and lifecycle callback handling.
Refresh for AOT processing only applies a subset of what happens with a regular refresh
.
AOT processing can be triggered as follows:
RuntimeHints hints = new RuntimeHints();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing(hints);
// ...
context.close();
In this mode, BeanFactoryPostProcessor
implementations are invoked as usual.
This includes configuration class parsing, import selectors, classpath scanning, etc.
Such steps make sure that the BeanRegistry
contains the relevant bean definitions for the application.
If bean definitions are guarded by conditions (such as @Profile
), these are discarded at this stage.
Because this mode does not actually create bean instances, BeanPostProcessor
implementations are not invoked, except for specific variants that are relevant for AOT processing.
These are:
-
MergedBeanDefinitionPostProcessor
implementations post-process bean definitions to extract additional settings, such asinit
anddestroy
methods. -
SmartInstantiationAwareBeanPostProcessor
implementations determine a more precise bean type if necessary. This makes sure to create any proxy that will be required at runtime.
Once this part completes, the BeanFactory
contains the bean definitions that are necessary for the application to run. It does not trigger bean instantiation but allows the AOT engine to inspect the beans that will be created at runtime.
Bean Factory Initialization AOT Contributions
Components that want to participate in this step can implement the BeanFactoryInitializationAotProcessor
interface.
Each implementation can return an AOT contribution, based on the state of the bean factory.
An AOT contribution is a component that contributes generated code that reproduces a particular behavior.
It can also contribute RuntimeHints
to indicate the need for reflection, resource loading, serialization, or JDK proxies.
A BeanFactoryInitializationAotProcessor
implementation can be registered in META-INF/spring/aot.factories
with a key equal to the fully qualified name of the interface.
A BeanFactoryInitializationAotProcessor
can also be implemented directly by a bean.
In this mode, the bean provides an AOT contribution equivalent to the feature it provides with a regular runtime.
Consequently, such a bean is automatically excluded from the AOT-optimized context.
If a bean implements the |
Bean Registration AOT Contributions
A core BeanFactoryInitializationAotProcessor
implementation is responsible for collecting the necessary contributions for each candidate BeanDefinition
.
It does so using a dedicated BeanRegistrationAotProcessor
.
This interface is used as follows:
-
Implemented by a
BeanPostProcessor
bean, to replace its runtime behavior. For instanceAutowiredAnnotationBeanPostProcessor
implements this interface to generate code that injects members annotated with@Autowired
. -
Implemented by a type registered in
META-INF/spring/aot.factories
with a key equal to the fully qualified name of the interface. Typically used when the bean definition needs to be tuned for specific features of the core framework.
If a bean implements the |
If no BeanRegistrationAotProcessor
handles a particular registered bean, a default implementation processes it.
This is the default behavior, since tuning the generated code for a bean definition should be restricted to corner cases.
Taking our previous example, let’s assume that DataSourceConfiguration
is as follows:
-
Java
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {
@Bean
public SimpleDataSource dataSource() {
return new SimpleDataSource();
}
}
Since there isn’t any particular condition on this class, dataSourceConfiguration
and dataSource
are identified as candidates.
The AOT engine will convert the configuration class above to code similar to the following:
-
Java
/**
* 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;
}
}
The exact code generated may differ depending on the exact nature of your bean definitions. |
The generated code above creates bean definitions equivalent to the @Configuration
class, but in a direct way and without the use of reflection if at all possible.
There is a bean definition for dataSourceConfiguration
and one for dataSourceBean
.
When a datasource
instance is required, a BeanInstanceSupplier
is called.
This supplier invokes the dataSource()
method on the dataSourceConfiguration
bean.
Best Practices
The AOT engine is designed to handle as many use cases as possible, with no code change in applications. However, keep in mind that some optimizations are made at build time based on a static definition of the beans.
This section lists the best practices that make sure your application is ready for AOT.
Expose The Most Precise Bean Type
While your application may interact with an interface that a bean implements, it is still very important to declare the most precise type.
The AOT engine performs additional checks on the bean type, such as detecting the presence of @Autowired
members, or lifecycle callback methods.
For @Configuration
classes, make sure that the return type of the factory @Bean
method is as precise as possible.
Consider the following example:
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public MyInterface myInterface() {
return new MyImplementation();
}
}
In the example above, the declared type for the myInterface
bean is MyInterface
.
None of the usual post-processing will take MyImplementation
into account.
For instance, if there is an annotated handler method on MyImplementation
that the context should register, it won’t be detected upfront.
The example above should be rewritten as follows:
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public MyImplementation myInterface() {
return new MyImplementation();
}
}
If you are registering bean definitions programmatically, consider using RootBeanBefinition
as it allows to specify a ResolvableType
that handles generics.
FactoryBean
FactoryBean
should be used with care as it introduces an intermediate layer in terms of bean type resolution that may not be conceptually necessary.
As a rule of thumb, if the FactoryBean
instance does not hold long-term state and is not needed at a later point in time at runtime, it should be replaced by a regular factory method, possibly with a FactoryBean
adapter layer on top (for declarative configuration purposes).
If your FactoryBean
implementation does not resolve the object type (i.e. T
), extra care is necessary.
Consider the following example:
-
Java
public class ClientFactoryBean<T extends AbstractClient> implements FactoryBean<T> {
}
A concrete client declaration should provide a resolved generic for the client, as shown in the following example:
-
Java
@Configuration(proxyBeanMethods = false)
public class UserConfiguration {
@Bean
public ClientFactoryBean<MyClient> myClient() {
return new ClientFactoryBean<>(...);
}
}
If the FactoryBean
bean definition is registered programmatically, make sure to follow these steps:
-
Use
RootBeanDefinition
. -
Set the
beanClass
to theFactoryBean
class so that AOT knows that it is an intermediate layer. -
Set the
ResolvableType
to a resolved generic, which makes sure the most precise type is exposed.
The following example showcases a basic definition:
-
Java
RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);
JPA
The JPA persistence unit has to be known upfront for certain optimizations to apply. Consider the following basic example:
-
Java
@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setPackagesToScan("com.example.app");
return factoryBean;
}
To make sure the scanning occurs ahead of time, a PersistenceManagedTypes
bean must be declared and used by the
factory bean definition, as shown by the following example:
-
Java
@Bean
PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) {
return new PersistenceManagedTypesScanner(resourceLoader)
.scan("com.example.app");
}
@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource, PersistenceManagedTypes managedTypes) {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setManagedTypes(managedTypes);
return factoryBean;
}
Runtime Hints
Running an application as a native image requires additional information compared to a regular JVM runtime. For instance, GraalVM needs to know ahead of time if a component uses reflection. Similarly, classpath resources are not shipped in a native image unless specified explicitly. Consequently, if the application needs to load a resource, it must be referenced from the corresponding GraalVM native image configuration file.
The RuntimeHints
API collects the need for reflection, resource loading, serialization, and JDK proxies at runtime.
The following example makes sure that config/app.properties
can be loaded from the classpath at runtime within a native image:
-
Java
runtimeHints.resources().registerPattern("config/app.properties");
A number of contracts are handled automatically during AOT processing.
For instance, the return type of a @Controller
method is inspected, and relevant reflection hints are added if Spring detects that the type should be serialized (typically to JSON).
For cases that the core container cannot infer, you can register such hints programmatically. A number of convenient annotations are also provided for common use cases.
@ImportRuntimeHints
RuntimeHintsRegistrar
implementations allow you to get a callback to the RuntimeHints
instance managed by the AOT engine.
Implementations of this interface can be registered using @ImportRuntimeHints
on any Spring bean or @Bean
factory method.
RuntimeHintsRegistrar
implementations are detected and invoked at build time.
import java.util.Locale;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
@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/*");
}
}
}
If at all possible, @ImportRuntimeHints
should be used as close as possible to the component that requires the hints.
This way, if the component is not contributed to the BeanFactory
, the hints won’t be contributed either.
It is also possible to register an implementation statically by adding an entry in META-INF/spring/aot.factories
with a key equal to the fully qualified name of the RuntimeHintsRegistrar
interface.
@Reflective
@Reflective
provides an idiomatic way to flag the need for reflection on an annotated element.
For instance, @EventListener
is meta-annotated with @Reflective
since the underlying implementation invokes the annotated method using reflection.
By default, only Spring beans are considered and an invocation hint is registered for the annotated element.
This can be tuned by specifying a custom ReflectiveProcessor
implementation via the
@Reflective
annotation.
Library authors can reuse this annotation for their own purposes.
If components other than Spring beans need to be processed, a BeanFactoryInitializationAotProcessor
can detect the relevant types and use ReflectiveRuntimeHintsRegistrar
to process them.
@RegisterReflectionForBinding
@RegisterReflectionForBinding
is a specialization of @Reflective
that registers the need for serializing arbitrary types.
A typical use case is the use of DTOs that the container cannot infer, such as using a web client within a method body.
@RegisterReflectionForBinding
can be applied to any Spring bean at the class level, but it can also be applied directly to a method, field, or constructor to better indicate where the hints are actually required.
The following example registers Account
for serialization.
-
Java
@Component
public class OrderService {
@RegisterReflectionForBinding(Account.class)
public void process(Order order) {
// ...
}
}
Testing Runtime Hints
Spring Core also ships RuntimeHintsPredicates
, a utility for checking that existing hints match a particular use case.
This can be used in your own tests to validate that a RuntimeHintsRegistrar
contains the expected results.
We can write a test for our SpellCheckService
and ensure that we will be able to load a dictionary at runtime:
@Test
void shouldRegisterResourceHints() {
RuntimeHints hints = new RuntimeHints();
new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
.accepts(hints);
}
With RuntimeHintsPredicates
, we can check for reflection, resource, serialization, or proxy generation hints.
This approach works well for unit tests but implies that the runtime behavior of a component is well known.
You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the GraalVM tracing agent. This agent will record all relevant calls requiring GraalVM hints at runtime and write them out as JSON configuration files.
For more targeted discovery and testing, Spring Framework ships a dedicated module with core AOT testing utilities, "org.springframework:spring-core-test"
.
This module contains the RuntimeHints Agent, a Java agent that records all method invocations that are related to runtime hints and helps you to assert that a given RuntimeHints
instance covers all recorded invocations.
Let’s consider a piece of infrastructure for which we’d like to test the hints we’re contributing during the AOT processing phase.
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.ClassUtils;
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);
}
}
}
We can then write a unit test (no native compilation required) that checks our contributed hints:
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
import org.springframework.core.SpringVersion;
import static org.assertj.core.api.Assertions.assertThat;
// @EnabledIfRuntimeHintsAgent signals that the annotated test class or test
// method is only enabled if the RuntimeHintsAgent is loaded on the current JVM.
// It also tags tests with the "RuntimeHints" JUnit tag.
@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);
}
}
If you forgot to contribute a hint, the test will fail and provide some details about the invocation:
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
There are various ways to configure this Java agent in your build, so please refer to the documentation of your build tool and test execution plugin.
The agent itself can be configured to instrument specific packages (by default, only org.springframework
is instrumented).
You’ll find more details in the Spring Framework buildSrc
README file.