@Import 是 Spring 框架中一个看似低调但非常强大的注解。它的核心作用是将一个或多个类导入到当前的 Spring 容器中,使其成为受管理的 Bean。与 @ComponentScan 自动扫描不同,@Import 提供了一种显式、精确的导入机制,常被用在 Spring Boot 的自动配置、自定义 Starter 和模块化组件装配中。
在实际开发中,@Import 主要有三种典型的用法:导入普通组件类、导入 @Configuration 配置类、通过 ImportSelector 或 ImportBeanDefinitionRegistrar 实现编程式导入。
24.1.1 用法一:直接导入普通组件类
这是最简单直接的形式。将一个标注了 @Component、@Service、@Repository 等注解的类,或者哪怕只是一个普通的 POJO 类,通过 @Import 显式引入到容器中。
场景示例
假设我们有一个工具类 StringUtils,它并非通过组件扫描发现,而希望显式注册为 Bean:
public class StringUtils {
public String toUpperCase(String input) {
return input != null ? input.toUpperCase() : null;
}
}
现在需要将它注入到当前应用中。不用在它上面加 @Component,而是在某个配置类上使用 @Import:
@Configuration
@Import(StringUtils.class)
public class AppConfig {
}
启动容器后,StringUtils 会成为一个 Spring Bean,默认的 bean 名称是全限定类名(如 com.example.StringUtils),可以被其他 Bean 注入使用。这种方式的优点是不需要修改原始类的代码,非常适合引入外部库中的类。
也可以一次导入多个类:
@Configuration
@Import({StringUtils.class, DateFormatter.class})
public class AppConfig {
}
Spring 会为 StringUtils 和 DateFormatter 分别创建 Bean 定义并纳入容器管理。
内部机制:Spring 在处理 @Import 时,会将指定的类视作一个配置源。如果该类自身有 @Configuration、@Component 等注解,会按正常流程解析;如果只是一个普通类,也会直接为它生成一个 Bean 定义,效果等同于在该类上标注了 @Component 并被扫描到。
实用提示:当需要将某个不能被包扫描覆盖的类注册为 Bean(比如来自不同 JAR 包的类),但又不想在 XML 中配置
<bean>,使用@Import是最快的方式。
24.1.2 用法二:导入 @Configuration 配置类
@Import 的第二种常见用法是导入另一个 @Configuration 配置类,实现配置的模块化拆分与聚合。
场景示例
假设我们将数据相关的 Bean 放在一个独立的配置类中:
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("root");
config.setPassword("secret");
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
主配置类并不希望使用 @ComponentScan 去扫描这个包,而是想显式引入以保持结构清晰:
@Configuration
@Import(DataSourceConfig.class)
public class AppConfig {
// 其他中心配置
}
启动容器后,DataSourceConfig 中定义的 dataSource 和 jdbcTemplate Bean 也会一同加载到容器中,效果如同 AppConfig 本身包含了这些 @Bean 方法。
如果使用 @ComponentScan,往往需要指定具体的包路径,还可能意外引入不需要的 Bean。而 @Import 则做到了精准导入,每个模块提供了哪些配置一目了然。
导入多个配置类的组合:
@Configuration
@Import({DataSourceConfig.class, SecurityConfig.class, CacheConfig.class})
public class AppConfig {
}
这样做的好处是:AppConfig 成为整个应用的装配中心,其他模块专注于各自领域的配置,再由中心统一导入,实现“组件化配置”。
24.1.3 用法三:通过 ImportSelector 和 ImportBeanDefinitionRegistrar 实现编程式导入
这是 @Import 最灵活、最高级的用法。通过实现 ImportSelector 或 ImportBeanDefinitionRegistrar 接口,可以在代码运行时动态决定要导入哪些类,以及如何注册 Bean 定义。
24.1.3.1 ImportSelector:基于条件筛选导入类
ImportSelector 接口只定义了一个方法:
String[] selectImports(AnnotationMetadata importingClassMetadata);
该方法返回一组类的全限定名,Spring 会将这些类当作被导入的配置类一样处理。
场景示例:根据运行环境决定导入哪个数据源配置
public class DataSourceSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
String env = System.getProperty("app.env", "dev");
if ("prod".equals(env)) {
return new String[]{"com.example.config.ProdDataSourceConfig"};
} else {
return new String[]{"com.example.config.DevDataSourceConfig"};
}
}
}
然后在配置类上使用:
@Configuration
@Import(DataSourceSelector.class)
public class AppConfig {
}
Spring 启动时会调用 selectImports,根据运行时属性动态决定导入生产或开发环境的配置类。这种方式比使用 @Profile 更加灵活,因为逻辑可以完全自定义。
Spring Boot 的自动配置核心 @EnableAutoConfiguration 就是利用 ImportSelector 来实现的:AutoConfigurationImportSelector 读取 META-INF/spring.factories 或 spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中的配置类列表,结合 @Conditional 条件注解,动态选择需要导入的自动配置类。
自定义注解封装 ImportSelector
我们可以将选择器封装成一个自定义注解,简化使用:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(DataSourceSelector.class)
public @interface EnableDataSource {
String env() default "dev";
}
使用时只需标注 @EnableDataSource(env="prod"),将参数通过 AnnotationMetadata 传递给选择器即可。
24.1.3.2 ImportBeanDefinitionRegistrar:编程式注册 Bean 定义
ImportBeanDefinitionRegistrar 提供了更底层的控制能力。它直接操作 BeanDefinitionRegistry,可以在运行时动态注册 Bean 定义,甚至可以修改已有 Bean 的定义。
接口定义:
void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry);
场景示例:根据接口扫描注册代理 Bean
假设我们希望将某个包下所有实现了 MyService 接口的类,自动代理为具有特定行为的 Bean,而不是简单地作为普通 Bean 注册。
public class MyServiceRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 使用自定义的扫描器扫描指定包
ClassPathBeanDefinitionScanner scanner =
new ClassPathBeanDefinitionScanner(registry, false);
scanner.addIncludeFilter(
new AssignableTypeFilter(MyService.class));
scanner.scan("com.example.services");
// 还可以对扫描到的 BeanDefinition 做后置处理
for (String beanName : registry.getBeanDefinitionNames()) {
BeanDefinition bd = registry.getBeanDefinition(beanName);
if (bd.getBeanClassName() != null &&
bd.getBeanClassName().startsWith("com.example.services")) {
bd.getPropertyValues().add("interceptorName", "myInterceptor");
}
}
}
}
然后在配置类上导入:
@Configuration
@Import(MyServiceRegistrar.class)
public class AppConfig {
}
这样,无需在每个服务类上标注 @Component,也不用通过 XML 定义大量重复配置,所有实现 MyService 的类都会被自动注册并织入额外的属性。
另一个经典应用是 MyBatis-Spring 中的 MapperScannerRegistrar。@MapperScan 注解本身被 @Import(MapperScannerRegistrar.class) 标记,MapperScannerRegistrar 实现了 ImportBeanDefinitionRegistrar,在运行时根据注解指定的包路径,动态扫描接口并将它们注册为 MapperFactoryBean 的 Bean 定义,从而让 MyBatis 的 Mapper 接口可以直接被注入使用。
24.1.4 三种用法的选择与对比
| 用法 | 适用场景 | 典型代表 |
|------|----------|----------|
| 直接导入普通类 | 引入外部工具类、简单 POJO | @Import(StringUtils.class) |
| 导入 @Configuration 类 | 模块配置拆分与组合 | @Import({SecurityConfig.class, DataConfig.class}) |
| ImportSelector / ImportBeanDefinitionRegistrar | 条件化装配、框架级封装、自定义注解驱动 | @EnableAutoConfiguration / @MapperScan |
在开发自定义 Starter 或框架级组件时,后两种用法尤为重要。通常会用 ImportSelector 做条件筛选导入,用 ImportBeanDefinitionRegistrar 做更精细的 Bean 定义控制。对于一般应用开发者,掌握前两种用法已经可以解决绝大多数显式导入需求。
@Import 的精髓在于让配置变得显式和可控,与自动扫描形成互补。当你不满足于“扫描所有标注了 @Component 的类”,而需要更精确地控制哪些组件被加载、何时被加载时,@Import 就是最重要的工具之一。