SpringBoot自动装配原理解析

4次阅读

共计 7592 个字符,预计需要花费 19 分钟才能阅读完成。

本文包含:SpringBoot 的自动配置原理及如何自定义 SpringBootStar 等

我们知道,在使用 SpringBoot 的时候,我们只需要如下方式即可直接启动一个 Web 程序:

@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);
    }
}

和我们之前使用普通 Spring 时繁琐的配置相比简直不要太方便,那么你知道 SpringBoot 实现这些的原理么

首先我们看到类上方包含了一个 @SpringBootApplication 注解

@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 {};}

这个注解上边包含的东西还是比较多的,咱们先看一下两个简单的热热身

@ComponentScan 注解

@ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

这个注解咱们都是比较熟悉的,无非就是自动扫描并加载符合条件的 Bean 到容器中,这个注解会默认扫描声明类所在的包开始扫描,例如:
cn.shiyujun.Demo 类上标注了 @ComponentScan 注解,则cn.shiyujun.controllercn.shiyujun.service 等等包下的类都可以被扫描到

这个注解一共包含以下几个属性:

basePackages:指定多个包名进行扫描
basePackageClasses:对指定的类和接口所属的包进行扫
excludeFilters:指定不扫描的过滤器
includeFilters:指定扫描的过滤器
lazyInit:是否对注册扫描的 bean 设置为懒加载
nameGenerator:为扫描到的 bean 自动命名
resourcePattern:控制可用于扫描的类文件
scopedProxy:指定代理是否应该被扫描
scopeResolver:指定扫描 bean 的范围
useDefaultFilters:是否开启对 @Component,@Repository,@Service,@Controller 的类进行检测

@SpringBootConfiguration注解

这个注解更简单了,它只是对 Configuration 注解的一个封装而已

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {}

EnableAutoConfiguration注解

这个注解可是重头戏了,SpringBoot 号称的约定大于配置,也就是本文的重点自动装配的原理就在这里了

@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

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

    String[] excludeName() default {};}

简单概括一下,这个注解存在的意义就是:利用 @Import 注解,将所有符合自动装配条件的 bean 注入到 IOC 容器中,关于 @Import 注解原理这里就不再阐述,感兴趣的同学可以参考此篇文章:Spring @Import 注解源码解析

进入类 AutoConfigurationImportSelector,观察其selectImports 方法,这个方法执行完毕后,Spring 会把这个方法返回的类的全限定名数组里的所有的类都注入到 IOC 容器中

public String[] selectImports(AnnotationMetadata annotationMetadata) {if (!this.isEnabled(annotationMetadata)) {return NO_IMPORTS;} else {AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return StringUtils.toStringArray(configurations);
        }
    }

观察上方代码:

  1. 第一行 if 时会首先判断当前系统是否禁用了自动装配的功能,判断的代码如下:
 protected boolean isEnabled(AnnotationMetadata metadata) {return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    }
  1. 如果当前系统禁用了自动装配的功能则会返回如下这个空的数组,后续也就无法注入 bean 了
private static final String[] NO_IMPORTS = new String[0];
  1. 此时如果没有禁用自动装配则进入 else 分枝,第一步操作首先会去加载所有 Spring 预先定义的配置条件信息,这些配置信息在 org.springframework.boot.autoconfigure 包下的 META-INF/spring-autoconfigure-metadata.properties 文件中

    1. 这些配置条件主要含义大致是这样的:如果你要自动装配某个类的话,你觉得先存在哪些类或者哪些配置文件等等条件,这些条件的判断主要是利用了 @ConditionalXXX 注解,关于 @ConditionalXXX 系列注解可以参考这篇文章:SpringBoot 条件注解 @Conditional

      1. 这个文件里的内容格式是这样的:

org.springframework.boot.actuate.autoconfigure.web.servlet.WebMvcEndpointChildContextConfiguration.ConditionalOnClass=org.springframework.web.servlet.DispatcherServlet
org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration.ConditionalOnClass=javax.sql.DataSource,io.micrometer.core.instrument.MeterRegistry
org.springframework.boot.actuate.autoconfigure.flyway.FlywayEndpointAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration

 3. 具体的加载代码就不列出了,无法就是个读取配置文件
 4. 这里放个加载之后的结果图:![file](https://image-static.segmentfault.com/233/146/2331463853-5d8426dc72166_articlex)
4. 获取 `@EnableAutoConfiguration` 注解上的 exclude、excludeName 属性,这两个属性的作用都是排除一些类的
5. 这里又是关键的一步,可以看到刚才图片中 spring-autoconfigure-metadata.properties 文件的上方存在一个文件 spring.factories,这个文件可就不止存在于 `org.springframework.boot.autoconfigure` 包里了,所有的包里都有可能存在这个文件,所以这一步是加载整个项目所有的 spring.factories 文件。这个文件的格式是这样的

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthIndicatorAutoConfiguration,org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration,org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration

6. 这里存在一个知识点,SpringBoot 中的 star 就是依靠这个文件完成的,假如我们需要自定义一个 SpringBoot 的 Star,就可以在我们的项目的 META-INF 文件夹下新建一个 spring.factories 文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.shiyujun.TestAutoConfiguration

这样当别的项目依赖我们的项目时就会自动把我们的 `TestAutoConfiguration` 类注入到 Spring 容器中
7. 删除重复的自动配置类
8. 下面三行就是去除我们指定排除的配置类
9. 接着这一行的逻辑稍微复杂一些,主要就是根据加载的配置条件信息来判断各个配置类上的 `@ConditionalXXX` 系列注解是否满足需求
10. 最后就是发布自动装配完成事件,然后返回所有能够自动装配的类的全限定名


到了这里我们已经把 SpringBoot 自动装配的原理搞清楚了,但是总感觉差点什么,那我们从这些自动装配的类里面挑一个我们比较熟悉的关于 Servlet 的类来看看咋回事吧:

@Configuration
@ConditionalOnWebApplication(

type = Type.SERVLET

)
public class ServletEndpointManagementContextConfiguration {

public ServletEndpointManagementContextConfiguration() {}

@Bean
public ExposeExcludePropertyEndpointFilter<ExposableServletEndpoint> servletExposeExcludePropertyEndpointFilter(WebEndpointProperties properties) {Exposure exposure = properties.getExposure();
    return new ExposeExcludePropertyEndpointFilter(ExposableServletEndpoint.class, exposure.getInclude(), exposure.getExclude(), new String[0]);
}

@Configuration
@ConditionalOnClass({ResourceConfig.class})
@ConditionalOnMissingClass({"org.springframework.web.servlet.DispatcherServlet"})
public class JerseyServletEndpointManagementContextConfiguration {public JerseyServletEndpointManagementContextConfiguration() { }

    @Bean
    public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties, ServletEndpointsSupplier servletEndpointsSupplier) {return new ServletEndpointRegistrar(properties.getBasePath(), servletEndpointsSupplier.getEndpoints());
    }
}

@Configuration
@ConditionalOnClass({DispatcherServlet.class})
public class WebMvcServletEndpointManagementContextConfiguration {
    private final ApplicationContext context;

    public WebMvcServletEndpointManagementContextConfiguration(ApplicationContext context) {this.context = context;}

    @Bean
    public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties, ServletEndpointsSupplier servletEndpointsSupplier) {DispatcherServletPathProvider servletPathProvider = (DispatcherServletPathProvider)this.context.getBean(DispatcherServletPathProvider.class);
        String servletPath = servletPathProvider.getServletPath();
        if (servletPath.equals("/")) {servletPath = "";}

        return new ServletEndpointRegistrar(servletPath + properties.getBasePath(), servletEndpointsSupplier.getEndpoints());
    }
}

}

自上而下观察整个类的代码,你会发现这些自动装配的套路都是一样的
1. 如果当前是 Servlet 环境则装配这个 bean
2. 当存在类 `ResourceConfig` 以及不存在类 `DispatcherServlet` 时装配 `JerseyServletEndpointManagementContextConfiguration`
3. 当存在 `DispatcherServlet` 类时装配 `WebMvcServletEndpointManagementContextConfiguration`
4. 接下来如果还有面试官问你,你会了么?

正文完
 0