@Import注解的作用
使用场景
通常我们会使用@Enable开头的注解到配置类中,表示开启某个功能特性,比如@EnableCaching、@EnableAsync,这就需要结合@Import注解来实现,这种方式可以简化配置,实现配置的可插拔。
@Import是Spring框架提供的
@Import注解作用是导入一些特定的配置类,这些特定类包括下面三种:
- @Configuration注解的类
- 实现ImportSelector接口的类
- 实现ImportBeanDefinitionRegistrar接口的类
上面三种效果都是一样,但是会有各自的优缺点:
- @Configuration使用简单明了,但是同样的拓展性不强
- SelectorImports可以通过实现接口,在接口方法中写复杂点的逻辑,还支持多个配置类的引用
- ImportBeanDefinitionRegistrar最灵活但也最复杂,需要手动创建和注入Bean
就现在我们对这三种方式的优缺点可能还很模糊,接下来我们结合实际代码来加深理解。
@Import的实际使用说明
现在我们通过现成的框架为例子,再手动写一个简单示例,来分别说明上面提到的三种场景。
一:@Configuration注解的类
例子:@EnableScheduling
比如定时任务开关:@EnableScheduling注解,
- @EnableScheduling
- @Import({SchedulingConfiguration.class})
里面Import了SchedulingConfiguration类,这个类的代码不多:
@Configuration
@Role(2)
public class SchedulingConfiguration {
public SchedulingConfiguration() {
}
@Bean(
name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
)
@Role(2)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}
现在说明一下,首先必须是用@Configuration修饰,这是必要条件,然后是@Role注解,这个注解对于我们来说没有意义,是Spring框架内部对Bean的一个标识:
int ROLE_APPLICATION = 0; // 应用程序或者业务Bean
int ROLE_SUPPORT = 1; // 框架内部,用于逻辑区分
int ROLE_INFRASTRUCTURE = 2; // 框架内部的Bean
简单说这个@Role不用理会,现在继续看,这个配置类里面注册了一个ScheduledAnnotationBeanPostProcessor的Bean,这个Bean初始化后,回去执行里面的代码逻辑,源码太长就不展示了,阅读完源码,就可以知道是去找到用@Scheduled注解修饰的方法,然后执行定制任务的逻辑。
所以我们这第一种方式的工作流程是这样的:
- 在配置类中使用@EnableScheduling注解
- 配置类会被Spring扫描,所以@EnableScheduling也会开始工作
- @EnableScheduling内的@Import({SchedulingConfiguration.class})生效,SchedulingConfiguration配置类里面的Bean被注入
- ScheduledAnnotationBeanPostProcessor类被初始化,开始执行里面关于定时任务的代码逻辑
实践
了解了上面的工作原理后,我们也照葫芦画瓢写一个类似的:
需求:写一个@EnableMyLog,使用了这个注解就会输出一段话,不使用就不会输出
-
步骤一:创建工作类
public class MyLog { public MyLog() { System.out.println("@EnableMyLog注解生效!!"); } -
步骤二:创建配置类,将工作类注入进来
@Configuration public class MyLogConfig { @Bean public MyLog myLog() { return new MyLog(); } } -
步骤三:声明@EnableMyLog注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Import({MyLogConfig.class}) public @interface EnableMyLog { }
这个示例要注意不要和启动类在同一个包路径下,因为SpringBoot启动类的@SpringBootApplication默认会扫描该路径下的所有配置类,那么直接就生效了,我们要测试的是可开关,所以新建一个包,在这里写代码即可,也就是外部路径:
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ ├─cc
│ │ │ │ Application.java
│ │ │ │
│ │ │ └─ext
│ │ │ └─mylog
│ │ │ EnableMyLog.java
│ │ │ MyLog.java
│ │ │ MyLogConfig.java
│ │ │
│ │ └─resources
│ │ application.yml
好了,现在我们直接启动程序,此时并没有输出,将@EnableMyLog修饰在启动类上(或者其他配置类),再次启动,就可以看到输出了:
@EnableMyLog注解生效!!
二:实现ImportSelector接口的类
例子:@EnableAsync
这个一个支持Spring异步的注解开关,
有了上面的学习经验,多余的就不赘述了,直接看这个注解Import的类:
@Import({AsyncConfigurationSelector.class})
进去看源码:
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";
public AsyncConfigurationSelector() {
}
@Nullable
public String[] selectImports(AdviceMode adviceMode) {
switch(adviceMode) {
case PROXY:
return new String[]{ProxyAsyncConfiguration.class.getName()};
case ASPECTJ:
return new String[]{"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"};
default:
return null;
}
}
}
这个类继承的父类AdviceModeImportSelector实现了接口ImportSelector,接口提供了一个方法:
String[] selectImports(AnnotationMetadata var1);
AsyncConfigurationSelector重写了这个方法,里面的内容是根据adviceMode作为条件来返回需要的配置类,相比起上面用@Import的方式,这种方式更加灵活,并且可以一次性返回多个配置类。
实践
需求:还是输出一段话,但我们输出多条不一样的,用来模拟返回多个类
-
加一个工作类
public class MyLog2 { public MyLog2() { System.out.println("@EnableMyLog注解生效!!22222"); } } -
加一个配置类
@Configuration public class MyLogConfig2 { @Bean public MyLog2 myLog2() { return new MyLog2(); } } -
实现ImportSelector写一个类
public class MyLogConfigSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { String[] configs = new String[2]; configs[0] = MyLog.class.getName(); configs[1] = MyLog2.class.getName(); return configs; } } -
写一个新的注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Import({MyLogConfigSelector.class}) public @interface EnableMyLogMulti { }
注意路径和启动类不在一个包路径下哦,上面已经说过了。
然后直接启动程序,没有输出,使用了@EnableMyLogMulti注解后,输出:
@EnableMyLog注解生效!!
@EnableMyLog注解生效!!22222
三:实现ImportBeanDefinitionRegistrar接口的类
例子:@EnableAspectJAutoProxy
它会向容器注册一个自动代理创建器,学习过AOP机制的应该多少了解一点,不了解也没关系,这不是重点,我们只需要知道这种方式的效果就可以了。
@Import({AspectJAutoProxyRegistrar.class})
查看源码:
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
AspectJAutoProxyRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
if (enableAspectJAutoProxy != null) {
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}
}
它实现了ImportBeanDefinitionRegistrar接口,在接口方法里面自己实现了Bean的注册逻辑,
接口实现方法里面关键代码是:enableAspectJAutoProxy.getBoolean("xxx")判断,来区分是使用JDK还是CGLIB来实现动态代理。
想了解Java动态代理可以看我这篇文章Java中的两种动态代理
相比起上面第二种方式,这种方式最灵活,但也最麻烦,一般前两种就足够使用了。
实践
需求:自定义Bean的注入,手动设置属性值
-
修改MyLog工作类,添加一个属性name
public class MyLog { private String name; public MyLog() { System.out.println("@EnableMyLog注解生效!!"); } public String getName() { return name; } public void setName(String name) { this.name = name; } } -
实现ImportBeanDefinitionRegistrar接口,在接口方法里面写自定义的逻辑
public class MyLogRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 已经被注入了就跳过 if (registry.containsBeanDefinition(MyLog.class.getName())) { return ; } // 已经被注入了就跳过 String[] candidates = registry.getBeanDefinitionNames(); for (String candidate : candidates) { BeanDefinition beanDefinition = registry.getBeanDefinition(candidate); if (Objects.equals(beanDefinition.getBeanClassName(), MyLog.class.getName())) { return ; } } // 创建MyLog类的Bean对象,并赋予值,然后注入 BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MyLog.class).getBeanDefinition(); beanDefinition.getPropertyValues().add("name", "cc"); registry.registerBeanDefinition(MyLog.class.getName(), beanDefinition); } } -
写一个新的注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Import({MyLogRegistrar.class}) public @interface EnableMyLogFlexible { }
还是老步骤,不用不输出,用了就输出。
但是我们还要测试一下name属性是否成功设置进去了,写一个测试类:
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationTest {
@Autowired
private ApplicationContext context;
@Test
public void test() {
MyLog bean = context.getBean(MyLog.class);
System.out.println("bean.getName() = " + bean.getName());
}
}
bean.getName() = cc
ok,大功告成。
Spring Boot的自动配置
学习了@Import之后,我们可以聊一聊Spring Boot的自动配置原理了,Spring Boot的启动类需要用注解@SpringBootApplication修饰,该注解是一个复合注解,里面有一个@EnableAutoConfiguration,该注解可以理解成我们上面说的@EnableMyLog注解,就是说用这个注解开启自动配置。
然后我们继续看,这个注解里面有@Import({AutoConfigurationImportSelector.class}),对应我们上面说的第二种方法,现在可以理解了吧,和实践例子中@EnableMyLog的工作原理是一样的:
-
@EnableAutoConfiguration开启自动配置,即开关生效
-
@Import({AutoConfigurationImportSelector.class})生效,执行AutoConfigurationImportSelector里面的代码
-
AutoConfigurationImportSelector代码的作用是:
- 扫描所有引入的starter里面的spring.factories文件,将所有的自动配置类扫描出来
- 将扫描到的自动配置类进行一些去重、排除
- 得到最终的结果并注入
自动配置类中会有一些默认配置,比如内嵌的Tomcat有一个additional-spring-configuration-metadata.json文件,里面写了关于端口的配置:
{ "name": "server.port", "defaultValue": 8080 },在配置类中它将这个默认配置文件加载进来了,所以开发者引入这个starter的时候,自动配置也就完成了。
这样看来,自动配置的原理并不复杂,只要深入一点进行研究就可以理解。
总结
总的来说,学习这个对阅读源码、手撸功能开关有帮助,所以还是有点用的。