前言
关于@Import在之前的文章里,也都零散的提到过多次,也支出了它的重要性,甚至它的一个解析过程。
但是由于@Import模式向容器导入Bean确实非常非常的重要,特别是在注解驱动的Spring项目中、@Enablexxx的设计模式中有大量的使用,在当下最流行的Spring Boot中,可以说作为设置是最重要的一种方式,来做底层抽象、组件式的设计。
比如我们熟悉的:@EnableAsync、@EnableAspectJAutoProxy、@EnableMBeanExport、@EnableTransactionManagement…等等统一采用的都是借助@Import注解来实现的
本篇文章旨在着眼于对@Import的使用上,以及结合ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar这三个接口的一些高级应用~
需要注意的是:ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar这三个接口都必须依赖于@Import一起使用,而@Import可以单独使用
基本环境如下
@ComponentScan(value = "com.fsx", excludeFilters = { @Filter(type = FilterType.ANNOTATION, classes = {Controller.class}), //排除掉web容器的配置文件,否则会重复扫描 @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {AppConfig.class}), }) @Configuration public class RootConfig { } @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired private ApplicationContext applicationContext; @Test public void test1() { Arrays.stream(applicationContext.getBeanDefinitionNames()) .filter(x -> !x.contains(".internal")) //过滤调用Spring内部给我们默认注册Bean,方便我们查看结果 .forEach(System.out::println); // 当前输出两个Bean:rootConfig、helloServiceImpl } }
说一句,采用SpringJUnit4ClassRunner为我们自动创建的容器,为GenericApplicationContext这个类型的容器。Bean工厂为DefaultListableBeanFactory。
关于GenericApplicationContext的使用,相当来说是都需要手动的,比如根据配置类加载Bean、刷新容器等等。。。这里面Spring的Spring-test包都帮我们把这些事做了~
目前这种容器(只能测试Service、Dao),不能测试Controller(web环境)下的Bean或者接口,因为junit这不是web环境,是不会启动web容器的。(毕竟缺少相关tomcat环境 jar包等等),若想测试web环境,请增加这么处理
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class, AppConfig.class}) @WebAppConfiguration //创建web容器,这样就能初始化AppConfig配置类,也能记载进来web先关的Bean了(比如Spring MVC的九大组件等等) public class TestSpringBean { ... }
至于Controller层的接口怎么通过url方式去请求测试 ,可以结合MockMvc来mock测试。具体的使用方式,这里就不多介绍了~(Junit不会有父子容器的概念。。。)
@Import注解
这里讲述单独使用@Import的例子,使用它有一个非常方便的地方在于:它可以导入Jar包里面的类(因为我们的@ComponentScan是不会扫描jar包的),可以看看下面这个例子:
//@ComponentScan 部分省略,下同 @Configuration @Import({Parent.class, // 这是Spring-code包里面的Bean,我随便找的一个 AntPathMatcher.class}) public class RootConfig { }
打印输出如下: 我们成功的向容器内注入了这些Bean,并且BeanName为全类名
rootConfig helloServiceImpl com.fsx.bean.Parent org.springframework.util.AntPathMatcher
那么,若我不把它放在@Configuration
上,而是放在一个普通的@Component
组件上呢?比如我们放在HelloServiceImpl
上:
@Service @Import({Parent.class, AntPathMatcher.class}) public class HelloServiceImpl implements HelloService { ... }
我们发现,效果是相同的。(其实如果你阅读过之前的@Configuration的解析过程,就能知道不管是Lite模式还是Full模式,这里导入Bean方面都是一样的)
参见ConfigurationClassParser#parse/processConfigurationClass方法
虽然放哪个组件都行,但在实际开发中,我们一般约定都放在@Configuration配置文件里
ImportSelector和DeferredImportSelector
使用@Import的时候,它的类可以是实现了ImportSelector或者DeferredImportSelector接口的类。
Spring容器会实例化这个实现类,并且执行其selectImports方法(执行时机,下面会源码分析)
我们先来看一个ImportSelector的Demo Show:
public class MyImportSelector implements ImportSelector //虽然不能@Autowired,但是实现了这些接口是可以感知到的,下面看源码会发现,Spring会给他注入进去 // 这样我们就可以根据特定的条件,来决定某些Bean能注入,有些Bean不能注入了 //,BeanClassLoaderAware,BeanFactoryAware,EnvironmentAware,ResourceLoaderAware { // 备注:这里各种@Autowired的注入都是不生效的,都是null // 了解Spring容器刷新过程的时候就知道,这个时候还没有开始解析@Autowired,所以肯定是不生效的 @Autowired private HelloService helloService; /** * 容器在会在特定的时机,帮我们调用这个方法,向容器里注入Bean信息 * * @param importingClassMetadata 包含配置类上面所有的注解信息,以及该配置类本身 * 若有需要,可以根据这些其它注解信息,来判断哪些Bean应该注册进去,哪些不需要 * @return 返回String数组,注意:都必须是类的全类名,才会被注册进去的(若你返回的全类名不存在该类,容器会抛错) */ @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { System.out.println("this MyImportSelector..."); //return new String[]{"com.fsx.bean.Child"}; // 一般建议这么玩 用字符串写死的方式只是某些特殊场合(比如这个类不一定存在之类的。。。) return new String[]{Child.class.getName()}; } }
输出如下:
rootConfig helloServiceImpl com.fsx.bean.Parent org.springframework.util.AntPathMatcher com.fsx.bean.Child
这里我提供提一个Spring的默认实现AdviceModeImportSelector(它通过解析注解信息,选择合适的Bean加入),大家可以供以参考。 这个类在后续分析事物的原理以及AOP的原理的时候,会再次见面~
再来一个DeferredImportSelector的Demo Show:
public class MyDeferredImportSelector implements DeferredImportSelector { // 同样的,它也只需要实现这个方法即可 但是它还提供了一些更高级的功能 @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { System.out.println("this MyDeferredImportSelector..."); // 这里面若容器里已经有名为`com.fsx.bean.Child`的Bean,就不会再注册进去了的 return new String[]{"com.fsx.bean.Child"}; } }
我们发现使用方式几乎一样,真的一样吗?其实容器启动的时候还有一个细节输出:
this MyImportSelector... this MyDeferredImportSelector...
从现象已经和名字中,我们能够更加直观的看出来:DeferredImportSelector显然是属于延迟加载、靠后加载的,那到底有多延迟,他们执行时机都是啥时候呢? 这就是我们接下来讨论的重点
再次强调一次:实现此接口的Bean必须是放在@Import进去的才会生效,而不能直接@Bean加入进去
ImportSelector和DeferredImportSelector的区别:
看了看DeferredImportSelector类的JavaDoc,得到如下信息:
- DeferredImportSelector是ImportSelector的一个扩展
- ImportSelector实例的selectImports方法的执行时机,是在@Configguration注解中的其他逻辑被处理**之前**,所谓的其他逻辑,包括对@ImportResource、@Bean这些注解的处理(注意,这里只是对@Bean修饰的方法的处理,并不是立即调用@Bean修饰的方法,这个区别很重要!);
- DeferredImportSelector实例的selectImports方法的执行时机,是在@Configguration注解中的其他逻辑被处理**完毕之后*
- DeferredImportSelector的实现类可以用Order注解,或者实现Ordered接口来对selectImports的执行顺序排序(ImportSelector不支持)
- ImportSelector是Spring3.1提供的,DeferredImportSelector是Spring4.0提供的
- Spring Boot的自动配置功能就是通过DeferredImportSelector接口的实现类EnableAutoConfigurationImportSelector做到的(因为自动配置必须在我们自定义配置后执行才行)
分析Spring源码中对此两个接口的处理
结合这篇博文【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser)分析,public void parse(Set<BeanDefinitionHolder> configCandidates){ ... }的最最最后一步,才去处理实现了DeferredImportSelector接口的类,因此是非常滞后的(此事已经处理好了@Bean、@ComponentScan、@ImportResource等等事宜)
ImportSelector 被设计成其实和@Import注解的类同样的导入效果,但是实现 ImportSelector的类可以条件性地决定导入哪些配置。
DeferredImportSelector 的设计目的是在所有其他的配置类被处理后才处理。这也正是该语句被放到本函数最后一行的原因。
看看前面做了些什么,我们直接来到核心处理方法doProcessConfigurationClass:(此处稍微详细点)