文章目录
前言
在SpringBoot开发中,我们经常见到Enable的模式开发,例如在SpringCloud中需要使用feign,此时需要开启Feign的注册,打上@EnableFeignClients注解,即可扫描带@FeignClient注解的FeignClient。又或是开启异步的功能,你需要加上@EnableAsyn注解。这种模式被称为“Enable模式”,也被视作SpringBoot中的“手动挡式自动装配”。本篇文章将解析Enable模式是什么,且分析源码了解底层操作。
同时,本篇文章也是下一篇介绍SpringBoot"自动档式自动装配"的魔法的铺垫式文章,为什么这么说呢?因为在自动挡中,@EnableAutoConfiguration注解起到了关键作用,同时它也是Enable驱动模式。或许读者对此注解感到陌生,但对于@SpringBootApplication这个注解一定不会陌生,因为在SpringBoot引导类上一般都会打上、这么一个注解,而@EnableAutoConfiguration元标注了@SpringBootApplication
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// ...
}
在Spring式的注解中,注解是具有"派生性"的,比如日常开发经常用到的一些声明Bean的注解:
- @Service
- @Controller
- @Configuration
- @Component
他们均被@Component元标注,例如@Service:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
// ...
}
而Spring通常解析注解时,会将注解递归解析出来,并放入Map中,也就是说,会解析到最后一层,在判断是否需要视为Bean注册到IOC容器中的时候,只需要判断是否有@Component注解即可,所以不管是@Service还是@Controller,都视为@Component,将其视作SpringBean注册到IOC容器中。
需要注意的是,在Spring4才支持递归的方式解析注解,也就是不管注解有多少层,最终都能识别到最底层的关键性注解例如@Component,SpringBoot1.0开始就使用了Spring4,所以可以放心其“派生性”。但在Spring3,派生性只支持一层,如果我自定义一个注解,元标注了@Service,那么到达关键注解@Component时需要2层解析,Spring3只解析一层,并没有递归解析,此时就做不到派生性了。
在Spring中,注解都有它自己的一套方式,比如“派生性”,除此之外还有属性覆盖性,还有属性别名等等一系列的Spring专有注解模式。
综上所述,理解@Enable驱动模式,对于理解SpringBoot自动装配是必不可少的。
@Enable驱动模式
在@Enable驱动模式的开发中,可以发现,其都被@Import注解所元标注,例如@EnableFeignClients
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
// ...
}
而Import注解中的value,一般而言都会实现ImportBeanDefinitionRegistrar或ImportSelector这两个接口,又或者是类头被@Configuration注解标注,那么从这点来看,Enable模式大致分为两种方式:
- 注解驱动:在类中有配置注解,如@Configuration、@Bean等等
- 接口编程:实现ImportBeanDefinitionRegistrar或ImportSelector这两个接口
然后将以上类放入@Import的value中即可,再给其加上一个华丽的外表,Enable*,这样一个Enable模式就完成了。
自定义Enable模式
下面我们自定义一个Enable模块。
注解驱动
@Configuration方式
先来一个最简单的注解驱动。首先自定义一个Enable注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(StringBean.class)
public @interface EnableStringBean {
}
其中Import中定义的配置类如下
@Configuration
public class StringBean {
@Bean
public String stringBean(){
return "Hello,world";
}
}
其实这里StringBean配置类也可以不标注@Configuration注解,在Spring3.0中限制只解析@Configuration,在后面的版本中,只要类中有@Bean、@ComponentScan等等配置注解,都可以被解析处理
@Configuration
@EnableStringBean
public class StringBeanContextBootstrap {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(StringBeanContextBootstrap.class);
String string = context.getBean("stringBean", String.class);
System.out.println(string);
context.close();
}
}
在需要装配的类上打上我们的自定义注解@EnableStringBean,装配到上下文中,控制台打印
可见,我们的Enable模块自动注册了一个Bean到上下文中
接口编程
ImportSelector接口方式
这种方式需要实现以上接口,并实现其selectImports方法。该方法返回一个字符串数组,这些字符串是需要被注册到Spring中的Bean的全限定类名。
这次我们来个更加动态的方式,首先重新定义Enable注解的属性
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(StringBeanRegister.class)
public @interface EnableStringBean {
StringBean.StringType type();
}
新增一个type属性,我们可以选择性的装载某个Bean。接下来是import中的StringBeanRegister定义
public class StringBeanRegister implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 获取EnableStringBean注解上的type属性
Map<String, Object> attributes =
importingClassMetadata.getAnnotationAttributes(EnableStringBean.class.getName());
StringBean.StringType type = (StringBean.StringType) attributes.get("type");
switch (type) {
case HELLO_WORLD:
return new String[]{"com.mytest.condition.config.HelloWorldBean"};
case WORLD_HELLO:
return new String[]{"com.mytest.condition.config.WorldHelloBean"};
default:
return null;
}
}
}
根据type值,动态判断,接下来看看我们动态判断的两个类定义和共同的接口StringBean
public class HelloWorldBean implements StringBean {
@Override
public String getString() {
return "hello,world";
}
}
public class WorldHelloBean implements StringBean {
@Override
public String getString() {
return "world,hello";
}
}
public interface StringBean {
String getString();
enum StringType{
HELLO_WORLD,
WORLD_HELLO
}
}
很简单,我们写一个测试类来试验一下
@Configuration
@EnableStringBean(type = StringBean.StringType.WORLD_HELLO)
public class StringBeanContextBootstrap {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(StringBeanContextBootstrap.class);
StringBean string = context.getBean(StringBean.class);
System.out.println(string.getString());
context.close();
}
}
这里我们的EnableStringBean注解value=StringType.WORLD_HELLO
可见,反向输出helloWorld的Bean被成功装载。装载方式随着type的不同而装载不同的Bean
ImportBeanDefinitionRegistrar接口方式
这种实现方式稍稍增加了难度。因为其接口方法需要自行构造BeanDefinition,并注册到Spring中,不像上面两种方式,只需要提供要注册的Bean信息。这种导入方式可以在接口方法中自行控制注册流程。
首先我们定义一个ImportBeanDefinitionRegistrar接口的实现类
public class StringBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
StringBeanRegister register = new StringBeanRegister();
// 使用上一个例子的代码,选择出要注册的Bean名称的全限定类名
String[] beanName = register.selectImports(importingClassMetadata);
Stream.of(beanName)
// 将Stream变为BeanDefinitionBuilder集合
.map(BeanDefinitionBuilder::genericBeanDefinition)
// 将Stream变为BeanDefinition集合
.map(BeanDefinitionBuilder::getBeanDefinition)
// 将每个BeanDefinition注册到IOC中
.forEach(beanDefinition ->
BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry));
}
}
这里我们看到,不仅需要提供要注册的Bean,还需要自行实现注册。Spring此种设计,无谓是拓宽了扩展性,因为有些装配只需要提供要注册的Bean类名是哪些就好了,但有些装配,需要细节到,动态自定义Bean的更多信息例如构造器参数是哪些、注册进去的BeanName需要叫什么等等,多了一个可以自定义BeanDefinition信息的维度,但有些装配不需要这么复杂。所以分为简、中、难三种维度供装配Bean的扩展,此种设计令我佩服。
手动挡装配原理
ConfigurationClassPostProcessor的注册
从这一节开始,将深入源码分析以上Enable模式的原理。
到现在我们已知@Enable*注解被@Import注解所元标注,所以我们的重点就在Spring上下文是什么时候扫描解析@Import这个注解的呢?
说到这里不得不提负责解析@Configuration的一个处理类
ConfigurationClassPostProcessor
,这个类不仅解析了@Import注解,还负责解析@Configuration注解。ConfigurationClassPostProcessor是一个BeanFactoryPostProcessor,所以它具有在Spring实例化Bean之前,修改和添加Bean定义的能力。它解析大多数配置类注解,所以具有动态根据注解注册Bean到上下文中的能力。
那么是谁负责注册ConfigurationClassPostProcessor
到Spring上下文的呢?全局搜索这个类,我们发现在AnnotationConfigUtils这个类的registerAnnotationConfigProcessors方法中,注册了这个类成为SpringBean
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
BeanDefinitionRegistry registry, @Nullable Object source) {
// ...
if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
// 将ConfigurationClassPostProcessor类构造为BeanDefinition
RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
def.setSource(source);
// 注册到IOC中
beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// ...
}
那么这个工具类的这个注册方法又被哪里引用了呢?可以找到,这个工具类将被处理注解的上下文 AnnotationConfigApplicationContext
的构造函数中调用
public AnnotationConfigApplicationContext() {
// 将在以下AnnotatedBeanDefinitionReader类的构造函数中被构造
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
// ...
// 调用了上面说到的工具,注册了ConfigurationClassPostProcessor
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
到此,就是Spring将ConfigurationClassPostProcessor这个处理类注册为SpringBean的过程。
解析@Import
在这篇文章中,不对ConfigurationClassPostProcessor进行过多的分析,只分析解析@Import注解的流程。
在postProcessBeanDefinitionRegistry方法中可以看到,由此开始了对配置注解的解析方法processConfigBeanDefinitions
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
// 这里获取Spring中所有的Bean名称
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
// 转换为Bean的定义类
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
// 判断是否已经被解析过了
if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
// 这里确认此Bean没有被解析过,判断是否需要被解析
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
// 代码能到这里,说明此Bean需要被解析,放入集合中
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
// Parse each @Configuration class
// 解析配置注解的类
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
// 解析
parser.parse(candidates);
// ...
// 将解析到的Bean注册到Spring容器中去
this.reader.loadBeanDefinitions(configClasses);
// ...
}
这里值得一提的是,ConfigurationClassUtils的checkConfigurationClassCandidate方法,此方法用来判断Bean是否是一个配置类(需要被解析)
if (isFullConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
}
else if (isLiteConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
}
以上两个分支都表示此Bean需要被解析,至于Full和Lite模式,此时可以先看作一个黑盒,不作过多研究。
public static boolean isFullConfigurationCandidate(AnnotationMetadata metadata) {
return metadata.isAnnotated(Configuration.class.getName());
}
private static final Set<String> candidateIndicators = new HashSet<>(8);
static {
candidateIndicators.add(Component.class.getName());
candidateIndicators.add(ComponentScan.class.getName());
candidateIndicators.add(Import.class.getName());
candidateIndicators.add(ImportResource.class.getName());
}
public static boolean isLiteConfigurationCandidate(AnnotationMetadata metadata) {
// Do not consider an interface or an annotation...
if (metadata.isInterface()) {
return false;
}
// Any of the typical annotations found?
for (String indicator : candidateIndicators) {
if (metadata.isAnnotated(indicator)) {
return true;
}
}
// Finally, let's look for @Bean methods...
try {
return metadata.hasAnnotatedMethods(Bean.class.getName());
}
catch (Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex);
}
return false;
}
}
上面的代码很简单,我们这里总结一下当Bean是怎样的就会去解析它呢?
只要类中具有以下注解
- @Configuration
- @Component
- @ComponentScan
- @Import
- @ImportResource
- @Bean
所以,只要IOC容器中有被打上@Enable*注解的Bean,此时就会被拿出来解析,执行parser的parse方法,而parser的parse方法最终会来到doProcessConfigurationClass方法,真正解析配置注解。所以对@Import注解的解析就在该方法中
// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), true);
其中,在getImports方法中,将获取@Import注解中的value值,也就是上面所说的注解编程和接口编程自定义的那个类,然后将其封装为SourceClass对象
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
// importCandidates集合就是Import的value
if (importCandidates.isEmpty()) {
return;
}
// ...
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
// 判断是否是ImportSelector的实现
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
// 实例化成ImportSelector类
ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
// 如果有aware方法,在这里会进行aware设置
ParserStrategyUtils.invokeAwareMethods(
selector, this.environment, this.resourceLoader, this.registry);
// 延迟导入,其中ImportSelector接口有一个子类是DeferredImportSelector
// 表示该Import要延迟导入,此时不做解析,只是将其保存在一个集合中去
// 在所有配置类解析完成之后,才会对DeferredImportSelector实现类要导入的类进行解析
if (selector instanceof DeferredImportSelector) {
// 封装DeferredImportSelector类,将其信息保存到集合中
this.deferredImportSelectorHandler.handle(
configClass, (DeferredImportSelector) selector);
}
// 若不是延迟导入,那就直接开始导入工作
else {
// 调用selector的方法,获取需要被注册的Bean的类名
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归解析该需要被注册的Bean
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// 又或者是ImportBeanDefinitionRegistrar的实现?
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
ParserStrategyUtils.invokeAwareMethods(
registrar, this.environment, this.resourceLoader, this.registry);
// 这里的做法是先将ImportBeanDefinitionRegistrar存放起来
// 在稍后一点再调用其方法注册BeanDefinition
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// 到这里,判断不是以上的接口编程实现
// 或者是上面import接口编程返回出来的需要被导入的那些类,也会走到这一步
// 将此类当作配置类解析
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
// 接下来还会到doProcessConfigurationClass方法再进行解析
// 还会走一样的流程进行解析配置注解
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
// ...
}
可以看到,这段代码的逻辑正好就是我们开头所示范的三种方式去自定义Enable模式,所以在下面就可以进行一个总结了
总结
可见,Enable模式关键其实就是@Import注解中的value所配置的类,这个类就是引导注册某些Bean的核心,到这里应该也能理解为何称之为"手动挡",因为它无法自动判断动态装配一些Bean,而是要自己去实现一个注册Bean的类,它可以是接口编程的,可以是注解驱动的,要配置什么Bean完全由手动完成,并且手动打上一个@Enable*注解。
或许这篇文章需要有ConfigurationClassPostProcessor解析逻辑基础,那么下一篇文章将开启对其的具体解析
如果有看懂的读者,就会发现,其实Import中的value所配置的类可以不必是SpringBean,例如ImportSelector接口方式,其返回的类名所对应的类可以没有任何注解,其也会被注册到Spring容器中去,亦或是注解驱动,类头也不必打上@Configuration,也可以解析@Bean、@ComponentScan等等注解,只不过如果有@Configuration注解的话此配置类是Full模式,这里也推荐打上@Configuration。关于配置模式,在具体讲解ConfigurationClassPostProcessor中也会介绍到。
在SpringBoot自动装配(自动挡)技术中,也是用到了@Enable驱动模式,但是它为什么是自动挡呢?因为它可以自动解析包下的所有@Component的类或其派生注解,将它们注册到Spring中,还能注册jar包中需要被注册的Bean,然而以上操作并不需要在你的代码中加任何代码,只需要加入对应的jar依赖,不需要其被装配也只需移除jar依赖,或是配置exclude黑名单。而开启自动挡,只需要Spring容器中有Bean被打上@EnableAutoConfiguration(派生@SpringBootApplication)即可。