Why SpringBoot 3.0 Dropped spring.factories and How to Migrate to Imports

SpringBoot 3.0 removed the traditional spring.factories file to improve startup performance, modularity, and GraalVM native image support, and introduces a new imports‑based registration mechanism with detailed migration steps, code examples, and best‑practice guidance for developers.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Why SpringBoot 3.0 Dropped spring.factories and How to Migrate to Imports

Why SpringBoot 3.0 removed spring.factories

1. Introduction

During the evolution of SpringBoot, version 3.0 introduced a major change by removing the long‑standing spring.factories file, which was the core of auto‑configuration and extension mechanisms. Engineers accustomed to earlier versions need to understand the new mechanism and migration strategy.

2. What is spring.factories

Before discussing its removal, we need to understand the role of the spring.factories file.

2.1 Basic concept

spring.factories

is a configuration file located under META-INF/. It is 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.factories file had several primary uses:

图片
图片

2.3 How it works

When SpringBoot starts, it uses the SpringFactoriesLoader class to scan all JARs on the classpath for META-INF/spring.factories files, reads the configuration, and loads the corresponding classes. This implements the "convention over configuration" approach for auto‑configuration.

public final class SpringFactoriesLoader {
    // ...
    public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
        // Load META-INF/spring.factories configuration
        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 the classpath
        // ...
    }
    // ...
}

3. Why spring.factories was removed

3.1 Performance issues

The mechanism scans all JARs for configuration files 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), class‑path scanning conflicts with modular design, and spring.factories cannot support the module system well.

3.3 Lack of conditional loading

The configuration in spring.factories is static and cannot decide at runtime whether to load a particular implementation. Although the @Conditional annotation can be used, it is inefficient because all classes are loaded into memory for condition evaluation.

3.4 Configuration scattered across many JARs

In large projects, spring.factories entries are scattered in multiple JARs, making global configuration hard to manage.

3.5 GraalVM native image support

SpringBoot 3.0 aims to provide first‑class support for GraalVM native images. The runtime class‑path scanning used by spring.factories fundamentally conflicts with GraalVM's Ahead‑of‑Time (AOT) compilation model.

Static analysis limitation: GraalVM needs static analysis, but spring.factories scanning is dynamic.

Reflection usage issue: spring.factories relies on reflection, which GraalVM must know ahead of time.

Resource access limitation: Resource file scanning in native images differs from the JVM.

To better support GraalVM, SpringBoot needs a static configuration determined at build time rather than a runtime dynamic scan.

4. Alternative: imports files

4.1 New mechanism introduction

Starting with SpringBoot 3.0, a new imports‑based 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 configuration.

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 categorized into different files.

4.3 Comparison example

Old way ( spring.factories):

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.FooAutoConfiguration,\
com.example.BarAutoConfiguration

New way ( AutoConfiguration.imports):

com.example.FooAutoConfiguration
com.example.BarAutoConfiguration

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

:

// Original auto‑configuration class
@Configuration
@ConditionalOnXxx
public class MyAutoConfiguration {
    // ...
}
// Add the fully‑qualified name to the imports file
// com.example.MyAutoConfiguration

5.2 Migrating other extension points

For other extension‑point types, SpringBoot 3.0 still supports spring.factories, but new projects should use the imports mechanism:

// Register ApplicationListener (old way)
org.springframework.context.ApplicationListener=com.example.MyListener
// New way (bean registration)
@Configuration
public class MyConfiguration {
    @Bean
    public MyListener myListener() {
        return new MyListener();
    }
}

5.3 Custom extension‑point migration

Provide a similar imports file for custom extension points:

// Custom extension loader (old)
public class MyExtensionLoader {
    public List<MyExtension> loadExtensions() {
        return SpringFactoriesLoader.loadFactories(MyExtension.class, null);
    }
}
// New way (imports file loading)
public class MyExtensionLoader {
    public List<MyExtension> loadExtensions() {
        List<String> classNames = SpringFactoriesLoader.loadFactoryNames(MyExtension.class, null);
        // ... custom logic
    }
}

6. Changes in SpringFactoriesLoader

6.1 API changes

In SpringBoot 3.x, SpringFactoriesLoader itself has been updated:

图片
图片
public final class SpringFactoriesLoader {
    @Deprecated
    public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
        // ...
    }
    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        // Load corresponding imports files
        // ...
    }
    // ...
}

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

// 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());
    }
}

7.2 Register the auto‑configuration

Create the file

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

with the line:

com.example.MyAutoConfiguration

7.3 Project structure

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

8. Performance comparison

Typical medium‑size SpringBoot application startup performance before and after the new mechanism:

图片
图片

Note: actual performance gain depends on project size and structure.

9. FAQ

9.1 Compatibility issues

Question: Existing dependencies still use spring.factories. Will there be compatibility problems?

Answer: SpringBoot 3.0 retains support for spring.factories, so old libraries continue to work. New code should use the imports mechanism.

9.2 Migration difficulty

Question: Migrating a large project to the new mechanism seems heavy.

Answer: Migrate in phases—first move auto‑configuration classes, then other extension points.

9.3 Custom loader migration

Question: How to migrate custom SpringFactoriesLoader users?

Answer: Follow SpringBoot's new implementation and provide a similar imports‑file loading logic.

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). Native images start in milliseconds, use far less memory, and do not require a JVM at runtime.

Fast startup: Millisecond‑level launch time.

Low memory footprint: Suitable for cloud‑native and container environments.

No JVM needed: Runs as a standalone binary.

Ahead‑of‑Time compilation: All code is compiled to machine code at build time.

10.2 Challenges for SpringBoot

SpringBoot's dynamic features clash with GraalVM's static analysis model:

图片
图片

10.3 Compatibility of imports files with GraalVM

Static analyzability: Imports files list all configuration classes, enabling build‑time analysis.

Clear path: Each extension point has a dedicated file, reducing runtime scanning.

Less reflection: Simpler parsing reduces reflection usage.

Build‑time handling: Imports can be processed during AOT compilation to generate metadata.

10.4 SpringBoot AOT engine

// SpringBoot 3.0 AOT processing example
public 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 at build time
    generateProxies(effectiveConfigurations);
    // 4. Generate reflection configuration
    generateReflectionConfig(effectiveConfigurations);
    // 5. Generate resources configuration
    generateResourcesConfig();
}

10.5 GraalVM integration example

Maven configuration for building a native image:

<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>

10.6 Performance comparison: JVM vs GraalVM native image

图片
图片

10.7 Best practices for GraalVM integration

Reduce reflection: Prefer constructor injection over field 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 with required metadata.

10.8 Limitations and considerations

Dynamic features limited: Runtime bytecode generation and dynamic class loading are restricted.

Reflection usage: Classes used via reflection must be declared explicitly.

Build time: Native image generation takes longer; plan CI/CD accordingly.

Debugging complexity: Debugging native images is more involved than JVM debugging.

Third‑party compatibility: Some libraries may not yet be optimized for GraalVM.

By removing spring.factories and introducing the imports‑based registration, SpringBoot 3.0 significantly improves GraalVM integration, enabling developers to build high‑performance, low‑latency cloud‑native applications more easily.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

MigrationspringbootGraalVMimportsSpring Factoriesauto-configuration
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.