@SpringBootApplication源码详细解析

Posted 元数据

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了@SpringBootApplication源码详细解析相关的知识,希望对你有一定的参考价值。

本文章的spring-boot版本为2.5.0-SNAPSHOT

在学习springboot源码前我在网上看了很多@SpringBootApplication解析的文章,在学习了spring源码和部分springboot源码后发现那些文章大多都没讲完整,甚至有些是错的。所以想写一个稍微完整点的解析文章。

前置知识

  1. 注解的继承性(或者叫派生性,传递性),怎么叫无所谓。

    @C
    @interface B{}//定义一个B注解,B注解中添加了C注解
    
    @B
    class A{}//A类使用了B注解,也相当于同时使用了C注解

    一个类添加了一个注解,那么同时也相当于添加了该注解内的注解,这个层级无论嵌套多少层都有效。

  1. BeanDefinition接口

    spring通过asm技术加载类文件,然后并不是直接创建对象,而是解析这个类的信息,如:所有需要spring处理的注解,Aware接口,FactoryBean接口等信息。这个接口的子类功能非常强大,可以通过这个BeanDefinition设置一个类的父类,接口,要使用的构造器,需要在初始化过程中调用的方法和传入的参数等。可以说通过BeanDefinition可以完全的创建一个新的类,这也是第三方组件的重要扩展方式。spring会根据BeanDefinition中包含的信息构建一个bean。
  1. @Import的作用,这个需要对spring的解析流程有些了解才能知道具体作用,这里只讲个大概。

    @Import(B.class) //使用方式,根据B类的类型选择不同的处理方式
    class A{}

    spring在解析类是如果发现有@Import注解,那么解析注解中的类,一般有三种类型

    1. 一个普通类,使用通用逻辑去加载,跟@Compoent一样
    2. ImportSelector接口的子类

      public interface ImportSelector {
          //传入被注解类的注解信息元数据,可以从importingClassMetadata中获取被注解类
          //的所有注解信息,包括传递的注解
          //返回包含了一个类的全类名的String[],spring会根据全类名去加载这些类成为bean
          String[] selectImports(AnnotationMetadata importingClassMetadata);
      }
    3. DeferredImportSelector

      //DeferredImportSelector继承了ImportSelector但是处理逻辑略有不同
      public interface DeferredImportSelector extends ImportSelector{
          //先使用getImportGroup返回一个Group的实现类,这个Group相当于一个处理器,可以多个类使用同一个Group,调用Group的两个方法返回一个Entry
          //spring会根据Entry中包含的信息进行初始化
          //如果getImportGroup返回null,那么还是调用ImportSelector的selectImports方法
          @Nullable
          default Class<? extends Group> getImportGroup() {
              return null;
          }
      
          //一个内部类
          interface Group {
              void process(AnnotationMetadata metadata, DeferredImportSelector selector);
              Iterable<Entry> selectImports();
              
              //内部类的内部类
              class Entry {
                  private final AnnotationMetadata metadata;
                  private final String importClassName;
              }
          }
      
      }

      这个DeferredImportSelector处理的具体逻辑有些复杂,有兴趣的可以看spring源码

      class ConfigurationClassParser {
          public void parse(Set<BeanDefinitionHolder> configCandidates) {
              //...
              //DeferredImportSelector的处理
              this.deferredImportSelectorHandler.process();
          }
      }
    4. ImportBeanDefinitionRegistrar的子类

      //传入被注解类的注解信息元数据,BeanDefinition的注册器
      default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                           BeanDefinitionRegistry registry) {
          //这里可以通过registry获取指定的BeanDefinition,对其进行改造
          //也可以通过registry注册一些新的BeanDefinition
      }

由于@Import有这四种处理方式,所以一般的@Enablexxx的功能都是由@Import实现

@SpringBootApplication

下面就可以开始分析这个注解了。

//省略jdk的元注解
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  1. @ComponentScan

    //这里自定义了两个排除过滤器,spring会通过反射创建这两个过滤器,对每一个扫描出的类进行判断
    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
            @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    public class TypeExcludeFilter implements TypeFilter{
        @Override
        public boolean match(MetadataReader metadataReader, 
                             MetadataReaderFactory metadataReaderFactory) throws IOException {
            //获取所有的TypeExcludeFilter(就是当前类)的子类,
            //调用match进行判断,只要一个为true,那么就返回true
            for (TypeExcludeFilter delegate : getDelegates()) {
                if (delegate.match(metadataReader, metadataReaderFactory)) {
                    return true;
                }
            }
            return false;
        }
    }
    public class AutoConfigurationExcludeFilter implements TypeFilter{
        @Override
        public boolean match(MetadataReader metadataReader, MetadataReaderFactory
                             metadataReaderFactory) throws IOException {
            //这个类如果有@Configuration注解并且是自动配置类,返回true
            //这个自动配置的判断逻辑是:spring-boot加载spring.factories中的键值对,有没有这个类的全类名
            //具体的看下方@EnableAutoConfiguration的解析
            return isConfiguration(metadataReader) && isAutoConfiguration(metadataReader);
        }
    }
    //@ComponentScan包的解析逻辑
    
    Set<String> basePackages = new LinkedHashSet<>();
    
    //获取解析basePackages的值
    String[] basePackagesArray = componentScan.getStringArray("basePackages");
    Collections.addAll(basePackages, basePackagesArray);
    
    //从basePackageClasses解析出包名
    for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
        basePackages.add(ClassUtils.getPackageName(clazz));
    }
    
    //如果componentScan注解中没有配置包,那么就注入被注解的类所在的包,这是spring中定义的逻辑,springboot中也是调用的这个方法。
    //所以如果在spring中把配置类放在其他包的上层,那么就不需要配置包路径
    if (basePackages.isEmpty()) {
        basePackages.add(ClassUtils.getPackageName(declaringClass));
    }
  2. @EnableAutoConfiguration

    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class) //导入一个类
    public @interface EnableAutoConfiguration {
    public class AutoConfigurationImportSelector implements DeferredImportSelector{
        //使用AutoConfigurationGroup中的方法去获取Entry
        @Override
        public Class<? extends Group> getImportGroup() {
            return AutoConfigurationGroup.class;
        }
    }
    private static class AutoConfigurationGroup implements DeferredImportSelector.Group{
        //emmm,这里有些复杂,想半天不知道怎么写,所以就写个简单逻辑。
        //这里会去classpath*:spring.factories中获取键值对保存到一个集合里面。
        //然后从集合中获取EnableAutoConfiguration全类名为key的value,包装成一个Entry
        //经过合并,排除,排序等步骤返回Iterable<Entry>
        //spring根据Entry中的信息去加载对应的类
    }
  3. @AutoConfigurationPackage,这个注解很多文章没有写,也有很多文章把spring-boot扫描当前包和子包的功能归结于这个注解,这个错得就有些离谱,因为扫描包是@ComponentScan的功能,而且是spring中定义的逻辑,spring-boot只是使用而已。

    @Import(AutoConfigurationPackages.Registrar.class)//导入了一个ImportBeanDefinitionRegistrar的子类
    public @interface AutoConfigurationPackage {
         String[] basePackages() default {};
    
        Class<?>[] basePackageClasses() default {};   
    }
    public abstract class AutoConfigurationPackages {
    
    
        //这是个静态内部类
        static class Registrar implements ImportBeanDefinitionRegistrar {
            //spring会调用这个方法
            @Override
            public void registerBeanDefinitions(AnnotationMetadata metadata,
                                                BeanDefinitionRegistry registry) {
                //第二个参数是从AutoConfigurationPackage注解中获取的包路径
                //如果无法解析出包路径,那么使用被注解类所在的包路径
                register(registry, 
                         new PackageImports(metadata).getPackageNames().toArray(new String[0]));
            }
        }
    
    
        private static final String BEAN = AutoConfigurationPackages.class.getName();
    
        //上面的registerBeanDefinitions方法会调用到这个方法
        public static void register(BeanDefinitionRegistry registry, String... packageNames) {
            //这里的逻辑很简单,就是判断有没有AutoConfigurationPackages全类名为名称的BeanDefinition
            //如果有,那么获取,并把包路径添加到beanDefinition
            if (registry.containsBeanDefinition(BEAN)) {
                BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN);
                beanDefinition.addBasePackages(packageNames);
            }
            else {
                //如果没有该BeanDefinition,那么创建一个新的BasePackagesBeanDefinition注册到spring中
                registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));
            }
        }
    
    
    
        //获取包路径
        public static List<String> get(BeanFactory beanFactory) {
            //这里就是从BasePackages中获取保存的包路径
            return beanFactory.getBean(BEAN, BasePackages.class).get();
    
        }
        //这也是个静态内部类
        static final class BasePackages {
            List<String> get() {
                //返回传入的包路径
                return this.packages;
            }
        }
    }

    上方的逻辑比较简单,如果没有额外配置,那么就是把被注解类所在的包路径包装成一个BasePackagesBeanDefinition并注册到spring中。
    那么这个有什么用呢?他的作用就是给其他组件提供spring-boot的扫描路径。如:mybatis-spring-boot-autoconfigure自动配置类导入的AutoConfiguredMapperScannerRegistrar源码中有这样一行代码。

    //这里就是调用上方那个AutoConfigurationPackages的get方法获取包路径
    //mybatis会扫描这个包路径下的类,经过过滤后得到注解了@Mapper的接口
    List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
  4. @SpringBootConfiguration

    @Configuration //这个注解就相当于一个@Configuration
    public @interface SpringBootConfiguration {

    @SpringBootConfiguration这个注解不知道具体是干什么的,这个注解的注释写的是:"可以自动找到配置(例如在测试中)"

    //一个测试类
    @SpringBootTest
    public class LearnMybatisTest {
        @Test
        public void test() {
    
        }
    }
    
    //@SpringBootTest源码
    @BootstrapWith(SpringBootTestContextBootstrapper.class)
    @ExtendWith({SpringExtension.class})
    public @interface SpringBootTest {
    
    //在SpringBootTestContextBootstrapper的这个方法中确实获取了@SpringBootConfiguration,
    //并配置了一些信息,但具体配置了什么,看不懂。。。
    org.springframework.boot.test.context.SpringBootTestContextBootstrapper#getOrFindConfigurationClasses
  5. @Configuration,很多人只知道要在配置类要添加这个注解,但是不知道这个注解到底是干什么的,也有人认为这个注解跟@Component一样,但实际上是有点不一样的。

    //先看一下@Configuration的源码
    @Component
    public @interface Configuration {
        //这个参数翻译一下就是: 代理bean方法 ,默认为true
        boolean proxyBeanMethods() default true;
    }
    
    //这样的就是bean方法
    @Bean
    public A a(){
        return new A();
    }

    可以尝试写这样一个配置类,主要逻辑是一个bean方法中调用了另一个bean方法。

    //@Configuration
    @Compoent
    public class A{
        @Bean
        public A a(){
            return new A();
        }
    
        @Bean
        public A a1(){
            A a= a();
            return a;
        }
    
        @Bean
        public A a2(){
            A a= a();
            return a;
        }
    }

    如果使用@Compoent,多次调用同一个bean方法返回的对象是不同的。而使用@Configuration就会返回同一个对象。这样来看proxyBeanMethods这个参数的很好理解了,就是是否要给方法添加代理,而这个代理的作用就是当调用bean方法的时候不是直接调用方法,而是从spring容器中获取这个方法已经创建好的bean并返回。

    //创建代理的位置
    org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanFactory
    //spring在解析时会判断是不是@Configuration,是的话标记为full
    //如果有Component,ComponentScan,Import,ImportResource,Bean注解标记为lite
    //spring只会对标记为full的配置类创建方法代理
    //代理逻辑
    class ConfigurationClassEnhancer {
        private static final Callback[] CALLBACKS = new Callback[] {
            //@Configuration代理的主要逻辑。
            new BeanMethodInterceptor(),
            //与BeanFactoryAware相关,内部逻辑不复杂,但不知道在什么地方使用,这里也没用到
            new BeanFactoryAwareMethodInterceptor(),
            //无操作,相当于不代理,直接调用。
            NoOp.INSTANCE 
        };
    
        private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
            Enhancer enhancer = new Enhancer();
            //cglib在创建代理时会对每个方法都调用一次CallbackFilter的accept方法,并传入方法,返回一个整数
            //这个整数代表CALLBACKS的索引,这个索引对应的callback就是这个方法所使用的增强逻辑。
            //所以在这个CallbackFilter中判断如果方法没有使用@Bean,那么返回2,对应的增强就是NoOp.INSTANCE 
            //如果使用了@Bean,那么返回0,表示BeanMethodInterceptor为增强逻辑。
            enhancer.setCallbackFilter(ConditionalCallbackFilter);
            return enhancer;
        }
    }

    BeanMethodInterceptor中有一个正在调用的方法的概念,如a方法调用了b方法,代理拦截了b方法(根据上面的CallbackFilter的逻辑,没有@Bean注解是不会被拦截的),那么此时a是正在调用的方法,这个方法从内部的ThreadLocal中获取,b会传入代理方法,只要判断这个两个方法相不相等,就能分辨出是不是从方法内部调用的方法。如果是从内部调用的方法,那么获取b的方法名或一些注解,获取到这个方法反会的bean的beanName,再从spring容器中获取返回给a方法。

总结

@SpringBootConfiguration,给集成测试初始化环境

@Configuration,给方法添加代理

@EnableAutoConfiguration,从spring.factories导入自动配置类

@AutoConfigurationPackage,把被注解的包路径包装成BeanDefinition,提供给其他组件使用

@ComponentScan,配置扫描路径和过滤类

以上是关于@SpringBootApplication源码详细解析的主要内容,如果未能解决你的问题,请参考以下文章

24HSpring源码解读与设计详析

@SpringBootApplication之自动配置源码分析

Springboot系列:@SpringBootApplication注解

@SpringBootApplication的说明

@SpringBootApplication注解的理解

@SpringBootApplication 之 @SpringBootConfiguration