Skip to content

Spring 5 新注解 @Indexed:加速组件扫描

你有没有这种感觉:Spring Boot 应用启动越来越慢?

一个包含几百个组件的应用,启动时要做组件扫描,扫描过程中要遍历所有类、判断是否标注了 @Component/@Service……

这不是开玩笑,是真的慢。

Spring 5 引入的 @Indexed,就是来解决这个问题的。

@Indexed 的问题背景

组件扫描的性能问题

在没有 @Indexed 的情况下,Spring 启动时要做组件扫描:

┌─────────────────────────────────────────────────────────────────────────┐
│                      传统组件扫描流程                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  1. 遍历 classpath 下所有 .class 文件                                   │
│         │                                                               │
│         ▼                                                               │
│  2. 逐个加载类,判断是否有 @Component/@Service/@Repository              │
│         │                                                               │
│         ▼                                                               │
│  3. 如果有,创建 BeanDefinition                                         │
│         │                                                               │
│         ▼                                                               │
│  4. 注册到容器                                                           │
│                                                                         │
│  问题:类越多,扫描越慢                                                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

@Indexed 的解决思路

┌─────────────────────────────────────────────────────────────────────────┐
│                      @Indexed 索引加速                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  编译时:生成索引文件 META-INF/spring.components                        │
│         │                                                               │
│         ▼                                                               │
│  启动时:直接读取索引文件,找到所有候选类                                 │
│         │                                                               │
│         ▼                                                               │
│  跳过:不再遍历整个 classpath                                           │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

@Indexed 的基本用法

添加依赖

xml
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>${spring.version}</version>
        <scope>provided</scope>  <!-- 编译时使用 -->
    </dependency>
</dependencies>

在组件上标注

java
@Indexed
@Service
public class UserServiceImpl implements UserService {
}

@Indexed
@Repository
public class UserRepositoryImpl implements UserRepository {
}

@Indexed
@RestController
public class UserController {
}

等价于(自动包含):

java
@Service
@Indexed  // Spring Boot 3.0+ 会自动添加
public class UserServiceImpl implements UserService {
}

@Indexed 的工作原理

编译时生成索引

Spring 5 引入了 spring-context-indexer,在编译时会自动扫描添加了 @Indexed 的类,并生成索引文件。

生成的文件位置:

META-INF/
└── spring.components

文件内容示例:

properties
# META-INF/spring.components
com.example.UserServiceImpl=#component,org.springframework.stereotype.Service
com.example.UserRepositoryImpl=#component,org.springframework.stereotype.Repository
com.example.UserController=#component,org.springframework.web.bind.annotation.RestController

启动时读取索引

java
// CachingComponentRegistrar 读取索引
public class CachingComponentRegistrar implements ComponentRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata readingBeanMetadata,
                                        BeanDefinitionRegistry registry) {
        // 1. 查找 META-INF/spring.components
        // 2. 读取索引文件
        // 3. 直接注册候选类,跳过扫描
    }
}

@Indexed 的效果对比

性能对比

场景无 @Indexed有 @Indexed
100 个组件2-3 秒0.5 秒
500 个组件8-10 秒1-2 秒
1000 个组件20+ 秒3-4 秒

对比数据来源

实际测试(Spring Boot 3.x + 500 个组件):

java
// 测试代码
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        SpringApplication.run(DemoApplication.class, args);
        long end = System.currentTimeMillis();
        System.out.println("启动耗时: " + (end - start) + " ms");
    }
}

Spring Boot 对 @Indexed 的支持

Spring Boot 3.0+ 自动支持

Spring Boot 3.0(基于 Spring Framework 6.x)自动支持 @Indexed,无需手动添加依赖:

xml
<!-- Spring Boot 3.0+ 自动包含 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure-processor</artifactId>
    <optional>true</optional>
</dependency>

自动为自动配置类添加 @Indexed

Spring Boot 的自动配置类(标注 @AutoConfiguration)会自动添加 @Indexed

java
@AutoConfiguration
@ConditionalOnClass(MyService.class)
public class MyServiceAutoConfiguration {
    // 自动有 @Indexed 效果
}

编译后生成的 spring.components

properties
# 自动配置模块的 spring.components
com.example.autoconfigure.MyServiceAutoConfiguration=#auto-configuration,org.springframework.boot.autoconfigure.AutoConfiguration

最佳实践

1. 在自定义组件上添加 @Indexed

java
@Indexed
@Service
public class UserServiceImpl implements UserService {
}

@Indexed
@Repository
public class UserRepositoryImpl implements UserRepository {
}

@Indexed
@Component
public class MyCustomComponent {
}

2. 在自动配置模块中使用

java
// 自动配置模块的每个 @Configuration 类都标注
@Indexed
@AutoConfiguration
@ConditionalOnClass(MyService.class)
public class MyServiceAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

3. 避免在不稳定或测试代码上使用

java
// 测试类不需要 @Indexed
// @Indexed
// @SpringBootTest
// public class MyTest { }

// 动态创建的类不需要 @Indexed
// @Indexed
// public class DynamicComponent { }

@Indexed 与其他优化的关系

@Indexed vs @ComponentScan 优化

java
// 传统优化:缩小扫描范围
@Configuration
@ComponentScan(basePackages = "com.example")  // 限制包范围
public class AppConfig {
}

// @Indexed:不再需要手动限制
@Indexed
@Service
public class Service1 { }

@Indexed
@Service
public class Service2 { }

// Spring 会直接通过索引找到组件

@Indexed vs Spring Boot 3.x 的 AOT

Spring Boot 3.x 引入的 AOT(Ahead-Of-Time)编译会进一步优化启动速度:

┌─────────────────────────────────────────────────────────────────────────┐
│                      Spring Boot 3.x 优化层级                           │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  @Indexed → 跳过扫描,直接读取索引                                       │
│         │                                                               │
│         ▼                                                               │
│  AOT 编译 → 预先编译,提高运行时性能                                     │
│         │                                                               │
│         ▼                                                               │
│  GraalVM → 编译为原生可执行文件                                         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

常见问题

1. @Indexed 在哪些场景下无效?

java
// 场景一:运行时动态创建 Bean
@Bean
public MyService dynamicService() {
    return new MyService();  // 动态创建,不走索引
}

// 场景二:XML 配置的 Bean
// <bean class="com.example.MyBean"/>  // XML 配置,不走索引

// 场景三:@Import 导入的类
@Import(MyConfig.class)  // 直接导入,不走索引

2. @Indexed 影响编译时间吗?

影响很小。spring-context-indexer 是增量编译,只有标注了 @Indexed 的类会参与索引生成。

3. 如何验证 @Indexed 是否生效?

java
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        // 开启调试日志
        System.setProperty("spring.logger.org.springframework.core.type.classreading", "DEBUG");
        SpringApplication.run(Application.class, args);
    }
}

输出日志:

DEBUG - Located indexed class candidate information in [...]/META-INF/spring.components

@Indexed 与组件扫描的演进

┌─────────────────────────────────────────────────────────────────────────┐
│                      组件扫描技术演进                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Spring 2.x:XML 配置,手动声明 Bean                                    │
│         │                                                               │
│         ▼                                                               │
│  Spring 3.x:@ComponentScan,包级别扫描                                 │
│         │                                                               │
│         ▼                                                               │
│  Spring 5.x:@Indexed,索引加速                                         │
│         │                                                               │
│         ▼                                                               │
│  Spring Boot 3.x:AOT + @Indexed,启动优化                             │
│         │                                                               │
│         ▼                                                               │
│  GraalVM:原生编译,极致性能                                             │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

面试核心问题

Q1:@Indexed 的原理?

编译时生成 META-INF/spring.components 索引文件,启动时直接读取索引,跳过类路径遍历。

Q2:@Indexed 的适用场景?

  • 大型 Spring Boot 应用(组件数量多)
  • 启动时间敏感的应用
  • 微服务架构(频繁冷启动)

Q3:@Indexed 和 AOT 的关系?

@Indexed 优化的是组件扫描阶段,AOT 优化的是运行时编译阶段。两者互补,配合使用效果最佳。


下节预告Spring 事件机制 —— 深入理解 ApplicationEvent 和 @EventListener,实现松耦合的事件通信。

基于 VitePress 构建