Why SpringBoot 3.0 Dropped spring.factories and What to Use Instead
SpringBoot 3.0 removes the long‑standing spring.factories file due to performance, modularity, and GraalVM native image challenges, introduces a new imports‑file mechanism, and provides detailed migration steps, code examples, performance comparisons, and best practices for GraalVM integration.
1. Introduction
During the evolution of SpringBoot, version 3.0 removed the long‑standing
spring.factoriesfile, which was the core of auto‑configuration and extension mechanisms. Engineers need to understand the new mechanism and migration strategy.
2. What is spring.factories
Before discussing its removal, we first need to understand the role of the
spring.factoriesfile in SpringBoot.
2.1 Basic concept
spring.factoriesis a configuration file located under
META-INF/that uses a variant of Java's Service Provider Interface (SPI) mechanism. Its main function is to allow developers to declare implementation classes for interfaces, enabling SpringBoot's auto‑configuration and extension point registration.
2.2 Main uses
Before SpringBoot 3.0, the
spring.factoriesfile had the following main uses:
2.3 Working principle
When SpringBoot starts, it uses the
SpringFactoriesLoaderclass to scan all JARs on the classpath for
META-INF/spring.factoriesfiles, reads the configuration, and loads the corresponding classes. This enables “convention over configuration” auto‑configuration.
<code>// SpringFactoriesLoader core code example (SpringBoot 2.x)
public final class SpringFactoriesLoader {
// ...
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
// ...
Map<String, List<String>> result = loadSpringFactories(classLoader);
// ...
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// load all META-INF/spring.factories files from classpath
// ...
}
// ...
}
</code>3. Why spring.factories was removed
The SpringBoot team decided to drop the spring.factories mechanism for several key reasons:
3.1 Performance issues
The mechanism scans all JARs at startup, which can consume a lot of time in projects with many dependencies, affecting startup performance.
3.2 Lack of modular support
With Java 9’s module system (JPMS), the class‑path scanning approach conflicts with modular design, and spring.factories cannot support it well.
3.3 Lack of conditional loading
Configurations in spring.factories are static; although @Conditional can be used, it is inefficient because all classes are loaded into memory for condition evaluation.
3.4 Configuration dispersion
In large projects, spring.factories configurations are scattered across many JARs, making global management difficult.
3.5 GraalVM native image support
SpringBoot 3.0 aims to provide first‑class GraalVM native image support. The runtime class‑path scanning of spring.factories fundamentally conflicts with GraalVM’s ahead‑of‑time compilation model.
Static analysis limitation: GraalVM needs static analysis, but spring.factories scanning is dynamic.
Reflection usage issue: spring.factories relies on reflection, which requires explicit configuration for native images.
Resource access limitation: Resource file scanning differs in native images and needs special handling.
Therefore a static, build‑time configuration approach is required.
4. Alternative: imports files
4.1 New mechanism introduction
Starting with SpringBoot 3.0, a new imports‑file mechanism replaces spring.factories. These files are placed under
META-INF/spring/, with one file per extension‑point type.
4.2 Advantages of the new mechanism
Better performance: Each extension‑point type uses a separate file, avoiding loading unnecessary configurations.
Java module support: Compatible with the Java module system.
Simplified configuration: One fully‑qualified class name per line, no key‑value pairs.
Clearer organization: Configurations are grouped by functionality.
4.3 Example comparison
Old way (spring.factories):
<code>org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.FooAutoConfiguration,\
com.example.BarAutoConfiguration
</code>New way (AutoConfiguration.imports):
<code>com.example.FooAutoConfiguration
com.example.BarAutoConfiguration
</code>5. Migration guide
5.1 Auto‑configuration class migration
Move the auto‑configuration classes registered in spring.factories to
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
<code>// Original auto‑configuration class
@Configuration
@ConditionalOnXxx
public class MyAutoConfiguration {
// ...
}
</code> <code>// In META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration
</code>5.2 Migration of other extension points
For other extension points, spring.factories is still supported, but new projects should use the imports mechanism. Example for ApplicationListener:
<code>// Before SpringBoot 3.0 (spring.factories)
org.springframework.context.ApplicationListener=com.example.MyListener
// After SpringBoot 3.0 (bean registration)
@Configuration
public class MyConfiguration {
@Bean
public MyListener myListener() {
return new MyListener();
}
}
</code>5.3 Custom extension point migration
Provide a similar imports file for custom extension points:
<code>// Custom extension loader (old)
public class MyExtensionLoader {
public List<MyExtension> loadExtensions() {
return SpringFactoriesLoader.loadFactories(MyExtension.class, null);
}
}
// New approach
public class MyExtensionLoader {
public List<MyExtension> loadExtensions() {
List<String> classNames = SpringFactoriesLoader.loadFactoryNames(MyExtension.class, null);
// custom imports handling
}
}
</code>6. Changes in SpringFactoriesLoader
6.1 API changes
In SpringBoot 3.0, SpringFactoriesLoader has been refactored:
<code>// Deprecated method
@Deprecated
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) { ... }
// New method
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { ... }
</code>6.2 Compatibility considerations
SpringBoot 3.0 still supports registering some extension points via spring.factories for backward compatibility, but new projects should prefer the imports mechanism.
7. Practical example
7.1 Create custom auto‑configuration
Complete example of a custom auto‑configuration in SpringBoot 3.0:
<code>// 1. Configuration properties class
@ConfigurationProperties(prefix = "myapp")
public class MyProperties {
private boolean enabled = true;
private String name = "default";
// getters and setters
}
// 2. Auto‑configuration class
@AutoConfiguration
@EnableConfigurationProperties(MyProperties.class)
@ConditionalOnProperty(prefix = "myapp", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MyAutoConfiguration {
private final MyProperties properties;
public MyAutoConfiguration(MyProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
public MyService myService() {
return new MyServiceImpl(properties.getName());
}
}
</code>7.2 Register the auto‑configuration
Create
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importscontaining:
<code>com.example.MyAutoConfiguration
</code>7.3 Project structure
<code>myproject/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/
│ │ ├── MyProperties.java
│ │ ├── MyService.java
│ │ ├── MyServiceImpl.java
│ │ └── MyAutoConfiguration.java
│ └── resources/
│ └── META-INF/
│ └── spring/
│ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── pom.xml
</code>8. Performance comparison
Typical medium‑size SpringBoot application startup performance after switching to the new mechanism:
Note: actual improvement depends on project size and structure.
9. FAQ and solutions
9.1 Compatibility issues
Question: Existing dependencies still use spring.factories; will there be compatibility problems?
Solution: SpringBoot 3.0 retains support for spring.factories, so old libraries work, but new code should use the imports mechanism.
9.2 Migration effort
Question: Large projects have a heavy migration workload.
Solution: Migrate in phases, starting with auto‑configuration classes, then other extension points.
9.3 Custom loader migration
Question: How to migrate custom SpringFactoriesLoader users?
Solution: Follow SpringBoot’s new implementation and provide a similar imports‑file loading mechanism.
10. SpringBoot 3.0 and GraalVM integration
10.1 GraalVM overview
GraalVM is a high‑performance JDK that can compile Java applications into native executables (Native Image) with fast startup, low memory usage, and no JVM requirement.
Fast startup: Millisecond‑level launch time.
Low memory footprint: Suitable for cloud‑native and container environments.
No JVM: Runs independently of a Java runtime.
Ahead‑of‑time compilation: All code is compiled to machine code at build time.
10.2 Challenges for GraalVM support
SpringBoot’s dynamic features conflict with GraalVM’s static analysis model.
10.3 Compatibility of imports files
The imports mechanism solves key GraalVM integration problems:
Static analyzability: All configuration classes are listed explicitly for build‑time analysis.
Clear paths: Each extension‑point has a specific file path, reducing runtime scanning.
Less reflection: Simpler parsing reduces reliance on reflection.
Build‑time processing: Imports can be processed during AOT compilation to generate metadata.
10.4 SpringBoot AOT engine
SpringBoot 3.0 introduces an AOT engine that performs the following at build time:
<code>public final class SpringAotProcessor {
// 1. Read imports files instead of scanning spring.factories
List<String> configurations = readImportsFiles();
// 2. Evaluate conditions at build time
List<String> effectiveConfigurations = evaluateConditions(configurations, buildTimeProperties);
// 3. Generate proxies statically
generateProxies(effectiveConfigurations);
// 4. Generate reflection configuration
generateReflectionConfig(effectiveConfigurations);
// 5. Generate resource configuration
generateResourcesConfig();
}
</code>10.5 GraalVM integration example
Maven configuration for a native image build:
<code><dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>${spring-native.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</code>Old spring.factories registration:
<code># META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.MyNativeCompatibleConfig
</code>New imports registration:
<code># META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyNativeCompatibleConfig
</code>Native‑compatible auto‑configuration:
<code>@AutoConfiguration
@NativeHint(options = "--enable-url-protocols=http")
public class MyNativeCompatibleConfig {
@Bean
public MyService myService() {
return new MyNativeCompatibleService();
}
}
</code>10.6 Performance: JVM vs native image
After using the imports mechanism, SpringBoot applications show the following performance in GraalVM native images:
10.7 Best practices for GraalVM integration
Reduce reflection usage: Prefer constructor injection.
Avoid dynamic proxies: Minimize features that require runtime proxies.
Static initialization: Initialize static data at build time.
Use imports files: Register all configuration classes via imports.
Add necessary hints: Use @NativeHint annotations to provide GraalVM hints.
10.8 Limitations and considerations
Dynamic features limited: Runtime bytecode generation and class loading are restricted.
Reflection declarations required: All reflective accesses must be declared.
Long build times: Native image builds take longer, requiring CI/CD planning.
Debugging complexity: Debugging native images is more difficult than JVM.
Third‑party library compatibility: Some dependencies may not be GraalVM‑ready.
By removing spring.factories and adopting the imports mechanism, SpringBoot 3.0 greatly improves GraalVM integration, enabling developers to build high‑performance, low‑latency cloud‑native applications.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.