一文吃透@SpringbootApplication的前世与今生

2022-08-23 14:09:42 浏览数 (1)

一.SpringBootApplication介绍

SpringBootApplication是标注在启动类上的复合注解,是springboot启动加载IOC容器的核心实现,也是springboot能够实现自动装配的关键逻辑,无论是面试还是自我提升都是必不可少的知识点。

废话少说,老规矩,先来看一下这个注解的源码

代码语言:javascript复制
@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 {
 
   @AliasFor(annotation = EnableAutoConfiguration.class)
   Class<?>[] exclude() default {};

   @AliasFor(annotation = EnableAutoConfiguration.class)
   String[] excludeName() default {};
  
   @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
   String[] scanBasePackages() default {};

   @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
   Class<?>[] scanBasePackageClasses() default {};

   @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
   Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

   @AliasFor(annotation = Configuration.class)
   boolean proxyBeanMethods() default true;

}

从源码可以看到,这个注解由三个子注解组成。

1.1.@SpringBootConfiguration

代码语言:javascript复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

SpringBootConfiguration的代码比较简单,由@Configuration标注,将启动类注册为springboot管理的配置类。这样,就可以在启动类上配置一些通用的系统级配置bean,比如外置tomcat的配置等。

1.2.@EnableAutoConfiguration

代码语言:javascript复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

   String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

   Class<?>[] exclude() default {};

   String[] excludeName() default {};

}
代码语言:javascript复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

   String[] basePackages() default {};
   
   Class<?>[] basePackageClasses() default {};

}

@EnableAutoConfiguration的功能从注解上定义来看,主要起作用的是@Import引入的相关自动配置类。

1.3.@ComponentScan

@ComponentScan代码比较长就不贴了,主要作用是扫描符合定义的组件与bean定义。并将扫描得到的bean与组件信息加载到IOC容器中。

二.深入理解@Configuration

2.2.@Component

日常业务开发过程中,最常用将一个类定义为spring的一个bean的方式无非是@Configuration/@Component/@Service/@Controller等。不知道大家有没有点击过以上的注解,如果点击注解逐层查看各个注解的定义就会发现,上面在springboot中可以将一个类生命以一个ioc的bean的源头都来自于@Component这个注解。是不是发现了一点奥秘,只要是基于@Component注解拓展出来的bean都都可以成为一个spring的bean。

2.2.@Bean

那么除了@Component还有别的操作可以将往IOC中添加一个bean吗?第一反应是@Bean

那么被@Bean与@Component有什么区别呢

引用一段解析(https://www.cnblogs.com/just-for-beyond/p/12783889.html)

@Component 作用于类上,只有在我们的SpringBoot应用程序启用了组件扫描并且包含了被注解的类时才有效。通过组件扫描,Spring将扫描整个类路径,并将所有@Component注释类添加到Spring Context,这里有的不足就是会把整个类当成bean注册到spring 容器上,如果这个类中并不是所有方法都需要注册为bean的话,会出现不需要的方法都注册成为bean,这时候必须确保这些不需要的方法也能注册为bean或者在扫描中加filter 过滤这些不需要的bean,否者spring将无法成功启动。 @Bean相对来说就更加灵活了,它可以独立加在方法上,按需注册到spring容器,而且如果你要用到第三方类库里面某个方法的时候,你就只能用@Bean把这个方法注册到spring容器,因为用@Component你需要配置组件扫描到这个第三方类路径而且还要在别人源代码加上这个注解,很明显是不现实的。

2.3.@Import

@Component与@Bean是日常工作中使用频率最高的场景,显示定义一个类或者方法为一个bean。那现在如果一个类很复杂,涉及到的关联逻辑很多,是一个核心配置类。那将很多的配置逻辑或者bean的加载逻辑都写在这个bean中就显得很臃肿而且很难理解加载逻辑。这时候@Import这个注解就登场了。相信阅读过部分spring源码或者看过开源框架的同学都对这个注解不陌生。那这个注解到底有什么作用?它又是怎么使用的呢?

@Import作用

1.导入三方jar包中的类为一个bean 2.将复杂的配置类分解作为单位的配置类存在,与主配置类共同生效与失效

@Import的三种用法主要包括:

1、直接填class数组方式 2、ImportSelector方式【重点】 3、ImportBeanDefinitionRegistrar方式

@Import方法的使用

代码语言:javascript复制
package com.baiyan.demo;

public class User {
}
代码语言:javascript复制
package com.baiyan.demo;

import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;

@Component
@Import(User.class)
public class Test {
}

打印一下ioc容器中的类可以看到:
com.baiyan.demo.User

2.4.ImportSelector

ImportSelector这个是一个接口,配合@Import使用,将接口方法返回的数组注册为ioc管理的bean

代码语言:javascript复制
@Component
@Import(BaiyanImportSelector.class)
public class Test {
}
代码语言:javascript复制
public class BaiyanImportSelector implements ImportSelector {
  @Override
  public String[] selectImports(AnnotationMetadata importingClassMetadata) {
      return new String[] {BaiyanServiceA.class.getName(), BaiyanServiceB.class.getName()};
  }
}

打印一下ioc容器中的类可以看到:
com.baiyan.demo.BaiyanServiceA
com.baiyan.demo.BaiyanServiceB

如果直接是固定的bean定义,那完全可以用上面的方式代替,但如果需要动态的带有逻辑性的定义bean,则使用ImportSelector还是很有用处的。因为在它的selectImports()你可以实现各种获取bean Class的逻辑,通过其参数AnnotationMetadata可以获取到@Import标注的Class的各种信息,包括其Class名称,实现的接口名称、父类名称、添加的其它注解等信息,通过这些额外的信息可以辅助我们选择需要定义为Spring bean的Class名称。现假设我们在HelloConfiguration上使用了@ComponentScan进行bean定义扫描,我们期望BaiyanImportSelector也可以扫描@ComponentScan指定的Package下HelloService实现类并把它们定义为bean。

2.5.ImportBeanDefinitionRegistrar

使用方式上与ImportSelector接口类似,接口上多了一个参数BeanDefinitionRegistry

代码语言:javascript复制
public class User implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(User.class);
        beanDefinitionRegistry.registerBeanDefinition("User123455",rootBeanDefinition);
    }

}

支持手动注册bean到ioc容器中

2.6.总结

  1. @Component:将类注册为一个bean
  2. @Bean : 将方法返回值注册为一个bean
  3. @Import({ 要导入的容器中的组件 } ): 容器会自动注册这个组件,id默认是全类名**
  4. ImportSelector:返回需要导入的组件的全类名数组,springboot底层用的特别多
  5. ImportBeanDefinitionRegistrar:手动注册bean到容器

三.深入理解@ComponentScan

这个注解从名称就可以很直观的知道是用来组件扫描的。扫描什么?就是用来spring的bean定义。这个注解本身不做什么业务含义,但是注解上的各个参数配置将会用来被解析成扫描的包路径与需要排除的类定义。这个类在启动类中的具体作用请继续往后看。

四.深入理解EnableAutoConfiguration

4.1.AutoConfigurationImportSelector

点击注解后我们发现,通过@Import引入了AutoConfigurationImportSelector这个配置类,根据上面文章中入@Import的讲解,这个类必定是一个配置类,点进去发现实现了DeferredImportSelector接口,这个接口又继承了ImportSelector接口,逻辑是不是瞬间清晰了。DeferredImportSelector接口实现与ImportSelector具有差异,不做展开,方法入口将会变为DeferredImportSelector.process方法

具体差异请参考:https://blog.csdn.net/it_lihongmin/article/details/106955547

注意查看AutoConfigurationImportSelector的内部类AutoConfigurationGroup处理

代码语言:javascript复制
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
   Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
         () -> String.format("Only %s implementations are supported, got %s",
               AutoConfigurationImportSelector.class.getSimpleName(),
               deferredImportSelector.getClass().getName()));
   AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
         .getAutoConfigurationEntry(annotationMetadata);
   this.autoConfigurationEntries.add(autoConfigurationEntry);
   for (String importClassName : autoConfigurationEntry.getConfigurations()) {
      this.entries.putIfAbsent(importClassName, annotationMetadata);
   }
}

其中关注核心语句

代码语言:javascript复制
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
      .getAutoConfigurationEntry(annotationMetadata);

用于获取自动配置节点,查看getAutoConfigurationEntry方法

代码语言:javascript复制
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
   configurations = removeDuplicates(configurations);
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
   configurations = getConfigurationClassFilter().filter(configurations);
   fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}

关注核心语句

代码语言:javascript复制
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
代码语言:javascript复制
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
         getBeanClassLoader());
   Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
           "are using a custom packaging, make sure that file is correct.");
   return configurations;
}

发现此方法在加载jar包中的配置文件,具体加载的是什么?

逐层进入方法追溯到SpringFactoriesLoader.loadSpringFactories方法发现加载各个jar包下META-INF/spring.factories文件内的一堆以AutoConfiguration结尾的类。

看出一点端倪没有,各位观众老爷,是不是跟我们EnableAutoConfiguration有点相似?这里就是springboot开箱即用的精妙之处,也是各个框架基于springboot的做三方starter的扩展点。

以上的言外之意即为:如果日常业务开发,我们想要做一个starter或者sdk,可以再我们的根资源文件夹resourece目录下定义 一个META-INF/spring.factories文件夹,然后在里面配置一些自动配置类,这样当别人引用你的包时,包扫描路径跟你的bean定义不一致时,也能将你的各种配置类注册到IOC容器中

4.2.AutoConfigurationPackages.Registrar

AutoConfigurationPackages.Registrar是**@AutoConfigurationPackage注解上Import进来的配置类。还是一样继承了DeterminableImports**。

代码语言:javascript复制
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

   @Override
   public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
      register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
   }

   @Override
   public Set<Object> determineImports(AnnotationMetadata metadata) {
      return Collections.singleton(new PackageImports(metadata));
   }

}

先看一下PackageImports这个类

代码语言:javascript复制
PackageImports(AnnotationMetadata metadata) {
   AnnotationAttributes attributes = AnnotationAttributes
         .fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
   List<String> packageNames = new ArrayList<>();
   for (String basePackage : attributes.getStringArray("basePackages")) {
      packageNames.add(basePackage);
   }
   for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
      packageNames.add(basePackageClass.getPackage().getName());
   }
   if (packageNames.isEmpty()) {
      packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
   }
   this.packageNames = Collections.unmodifiableList(packageNames);
}

直观了吗?是不是跟上面的@ComponentScan中的basePackages属性串联起来了,整个方法就是在设置一个属性,包的扫描路径是什么?

@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现从声明@ComponentScan所在类的package进行扫描,默认情况下是不指定的,所以SpringBoot的启动类最好放在root package下

扫描得到的包路径通过AutoConfigurationPackages.Registrar.registerBeanDefinitions内的 register方法注入register这个类,而Registrar类作用是扫描主配置类同级目录以及子包,并将通过determineImports方法将相应的标注了符合bean定义的组件导入到springboot创建管理的容器中。

4.3.总结

总览EnableAutoConfiguration注解的作用,通过AutoConfigurationImportSelector这个类加载到所有三方jar中的bean定义,通过AutoConfigurationPackages.Registrar这个类加载到包扫描路径下的bean定义

5.SpringbootApplication自动配置步骤

  1. springboot应用启动。
  2. @SpringBootApplication起作用。
  3. @EnableAutoConfiguration。
  4. @AutoConfigurationPackage:主要功能是@Import(AutoConfigurationPackages.Registrar.class),它通过将Registrar类导入到容器中,而Registrar类作用是扫描@ComponentScan注解定义的basePackages属性解析得到bean组件的根扫描路径【如果确实则以启动类所在包路径为准,因此建议把启动类放置在业务应用包的根目录】,并将相应的组件导入到springboot创建管理的容器中。
  5. @Import(AutoConfigurationImportSelector.class):它通过将AutoConfigurationImportSelector类导入到容器中,通过SpringFactoriesLoader.SpringFactoriesLoader加载引入jar包中META-INF/spring.factories文件中定义的自动配置类,将一系列的组件加载到IOC的容器中

6.引用

https://blog.csdn.net/elim168/article/details/88131614

https://www.cnblogs.com/yichunguo/p/12122598.html

https://juejin.cn/post/6844903661269696519

https://my.oschina.net/u/3872757/blog/3059922

0 人点赞