上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

自动装配源码解析 底层实现的细节 springboot

[复制链接]
  • TA的每日心情
    奋斗
    2025-4-23 20:09
  • 签到天数: 7 天

    [LV.3]偶尔看看II

    18

    主题

    17

    回帖

    290

    积分

    荣誉开发者

    积分
    290

    油中2周年新人报道荣誉开发者

    发表于 2025-5-29 13:37:39 | 显示全部楼层 | 阅读模式

    本帖最后由 溯水流光 于 2025-5-29 13:39 编辑

    下载和备份: https://github.com/HHsomeHand/springboot-code-reading/tree/main

    前言

    使用 springboot 开发时

    只需要导入 starter 就 ok 了

    starter 会帮我们, 自动装配 (自动配置组件到 IOC 容器中)

    如:

    • spring mvc starter 会自动装配 dispatchServlet, 并映射到 / 路径
    • mybatis starter 会自动装配 连接池(DataSource) 事物管理器

    这些装配的操作都是由一个个的配置类实现的

    而这些配置类在 starter 相关包下

    这些包不在我们的 @SpringBootApplication 标注类的路径下

    所以自动扫描, 无法扫描到

    所以 starter 会在 jar 包的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 这个文件里面

    登记所有要加载的配置类, springboot 会加载所有的配置类, 于是便实现了自动装配

    源码分析, 探寻背后的实现原理

    这里分析的源码版本为 3.0.4, springboot 新旧源码差异大, 特此说明

    创建一个 springboot 项目

    <dependencies>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>3.0.4</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    </dependencies>
    @SpringBootApplication
    public class MainSpringApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(MainSpringApplication.class, args);
        }
    
    }

    IDEA 按住 ctrl 并点击 @SpringBootApplication, 转到定义

    乍一看, 花花绿绿, 但源码分析的要义就是抓重点看

    所以其他内容, 我们晚点再分析, 先看主线剧情

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @EnableAutoConfiguration // 看这里
    public @interface SpringBootApplication {

    转到 @EnableAutoConfiguration 的定义

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Import({AutoConfigurationImportSelector.class}) // 看这里
    public @interface EnableAutoConfiguration {

    @Import 是用于将一个类注入 IOC 容器

    public class AutoConfigurationImportSelector implements DeferredImportSelector

    AutoConfigurationImportSelector 是一个 DeferredImportSelector

    DeferredImportSelector 是一个 ImportSelector

    ImportSelector 有一个成员方法: selectImports

    这个成员方法, 返回一个字符串数组, 内容为类的权限定名, 也就是完整的包名 + 类名

    spring 收到后, 会把这些类装配到 IOC 容器中

    我们可以通过 ImportSelector 来将外部的配置类, 装配到 IOC 容器中, 使其生效

    ImportSelector 无法通过 @Component 包扫描生效, 必须用 @Import 导入才能生效

    public String[] selectImports(AnnotationMetadata annotationMetadata) {

    annotationMetadata 为 @Import({ImportSelector.class}) 下面的注解

    这里便是 EnableAutoConfiguration

    public @interface EnableAutoConfiguration {
        String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    
        Class<?>[] exclude() default {};
    
        String[] excludeName() default {};
    }

    EnableAutoConfiguration 携带了一些信息, 用于排除不想自动装配的项

    继续看这个 ImportSelector 的具体实现

    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // 先看这里
        var autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
    
        // ...
    }
    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        // 先看这里
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        // ...
    }
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        // 看这里
        ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader());
    
        // ...
    }
    public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
    
        // annotation 为 AutoConfiguration.class
        // 所以 location 为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
        String location = String.format("META-INF/spring/%s.imports", annotation.getName());
    
        Enumeration<URL> urls = findUrlsInClasspath(classLoader, location);
        List<String> importCandidates = new ArrayList();
    
        while(urls.hasMoreElements()) {
            URL url = (URL)urls.nextElement();
    
            // readCandidateConfigurations 为按行读取配置文件, 返回一个字符串数组
            importCandidates.addAll(readCandidateConfigurations(url));
        }
    
        return new ImportCandidates(importCandidates);
    }
    

    这里 urls 为配置文件的路径名

    一个 springboot 项目经常使用多个 starter

    每个 starter 相关包 都提供了配置文件

    配置文件 按行存放 配置类路径

    且该配置文件在 jar 包下的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 路径

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        // 也就是读取了一个字符串数组, 这个数组存储了全部要加载的配置类
        ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation, this.getBeanClassLoader());
    
        List<String> configurations = importCandidates.getCandidates();
    
        return configurations;
    }
    // annotationMetadata 为 @EnableAutoConfiguration
    // @EnableAutoConfiguration 有 exclude 和 excludeName, 两个属性
    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
    
        // 获取要加载的配置类路径
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
    
        // removeDuplicates 中文意思 -> 移除重复项
        configurations = this.removeDuplicates(configurations);
    
        // 获取要排除的配置类
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
    
        // 删除要排除的项目
        configurations.removeAll(exclusions);
    
        return new AutoConfigurationEntry(configurations, exclusions);
    }
    
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // 获取要加载的 配置类 的 全限定名 数组
        var autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
    
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }

    到此, 算是完全分析完毕了, 功德圆满了

    其他的噪音

    分析源码的一大忌讳便是钻牛角尖, 扣细节

    这完全是无意义的内耗, 扣那些噪音干什么?

    难不成看到 HashMap, 还要顺便去扣 HashMap 的具体实现?

    然后发现需要数据结构和算法的知识.

    于是恶补一下数据结构和算法, 然后发现需要数学

    然后去专研数学, 最后成为了数学大师, 从此功德圆满.

    这太荒诞了, 而且也不可能.

    研究数学, 也去扣细节, 于是开始研究人类的逻辑, 开始研究心理学.

    最后在无尽的内耗和痛苦中, 成为了神经病, 于是变成了行为艺术家.

    当然上面都是极端情况的逻辑推演, 如有雷同, 纯属巧合, 我只是在开玩笑

    这些噪音在进行源码分析的时候, 最好排除, 以免被干扰

    再看 @SpringBootApplication

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(
        excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
    ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
    )}
    )
    public @interface SpringBootApplication {

    @SpringBootConfiguration 就是 @Configuration

    TypeExcludeFilter 用于加载我们注入 IOC 容器中的类型过滤器, 来辅助 包扫描 排除项目, 这里不做探讨

    AutoConfigurationExcludeFilter 的实现, 成员方法 match 是用来做匹配的, 如果返回 true, 包扫描工具 则排除此项

    AutoConfigurationExcludeFilter 的作用为排除所有的 自动配置类

    让包扫描工具, 不去加载这些配置类

    这些配置类留给 AutoConfigurationImportSelector 用 importSelector 去加载, 这么设计的因由, 我后文分析, 先读源码:

    public class AutoConfigurationExcludeFilter implements TypeFilter, BeanClassLoaderAware {
        private ClassLoader beanClassLoader;
        private volatile List<String> autoConfigurations;
    
        public void setBeanClassLoader(ClassLoader beanClassLoader) {
            this.beanClassLoader = beanClassLoader;
        }
    
        public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
            return this.isConfiguration(metadataReader) && this.isAutoConfiguration(metadataReader);
        }
    
        private boolean isConfiguration(MetadataReader metadataReader) {
            // 判断是否带 Configuration 注解
            return metadataReader.getAnnotationMetadata().isAnnotated(Configuration.class.getName());
        }
    
        // 判断是否是 自动配置类
        private boolean isAutoConfiguration(MetadataReader metadataReader) {
            // 判断是否带 AutoConfiguration 注解
            boolean annotatedWithAutoConfiguration = metadataReader.getAnnotationMetadata().isAnnotated(AutoConfiguration.class.getName());
    
            return annotatedWithAutoConfiguration || this.getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
        }
    
        protected List<String> getAutoConfigurations() {
            if (this.autoConfigurations == null) {
                // 上面分析过 ImportCandidates.load 的逻辑
                // 这里的功能为读取所有 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 
                // 读取后, 可以获取到要加载的 配置类全限定名 数组
                ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, this.beanClassLoader);
                this.autoConfigurations = importCandidates.getCandidates();
            }
    
            return this.autoConfigurations;
        }
    }

    上面有简单提到过, AutoConfigurationImportSelector 为 DeferredImportSelector

    DeferredImportSelector 相较于 ImportSelector 的区别是:

    DeferredImportSelector 是 Deferred 延迟执行的

    DeferredImportSelector 会在所有 @Configuration 类解析完成后执行

    DeferredImportSelector 这么做的好处就是不与用户配置产生冲突

    如用户配置了阿里的连接池德鲁伊, starter 内用于 连接池初始化的配置类, 就不应该生效了

    因为用户都配置了连接池, starter 就不应该再配置了

    下面是一段DataSourceAutoConfiguration.class摘录:

    @Configuration
    @ConditionalOnMissingBean({DataSource.class, XADataSource.class})
    @Import({DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class})
    protected static class PooledDataSourceConfiguration {

    逻辑为, 如果 DataSource.class 在容器中不存在, 再去使用DataSourceConfiguration.Hikari.class配置类

    HikariCP 是 DataSource, 也就是连接池

    找找 AutoConfiguration.imports

    IDEA 项目(左侧的树状结构) -> 外部库 -> spring-boot-autoconfigure-3.5.0.jar -> META-INF.spring -> org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件

    打开的内容:

    // 连接池的配置类
    org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 
    
    // DispatcherServlet 的配置类
    org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration 

    按 Ctrl + 点击名字, 可以转到定义

    我们这里来简单看看 DispatcherServletAutoConfiguration 的内容

    @Bean(
        name = {"dispatcherServlet"}
    )
    public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
        DispatcherServlet dispatcherServlet = new DispatcherServlet();
    
        // ...
    
        return dispatcherServlet;
    }
    
    @Bean(
        name = {"dispatcherServletRegistration"}
    )
    public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
        DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
    
        // ...
    
        return registration;
    }

    综上所述, springboot 的 自动配置类 并不神秘, 其实就是配置类(@Configuration)

    只是通过了 ImportSelector 进行了加载

    也就是通过 配置文件, 指明要加载的配置类, springboot 会自动帮我们加载

    加载了配置类, 自然就可以把组件加载到 IOC 容器中

    而那些 starter 都提供了这样的配置文件, 于是只用添加了这些 starter, 便可以实现自动装配

    尾声

    笔者以前也写过许多源码解析, elmGetter, duilib 之类的云云

    我也阅读了大量且优质的源码解析, 并在持续创作的时候, 总结了一些方法论, 比如如何简化源码, 如何突出重点, 如何层层递进

    所以我写的这篇, 我个人感觉是相当好的, 当然, 以后会更好.

    文有文风, 曲有曲风, 一篇文章不可能满足所有人.

    所以希望这篇文章能带给有缘人, 知识和收获吧!

    关于 AI

    写源码解析的另外一个忌讳就是, 完全使用 AI

    AI 没有人类的逻辑, 完全是靠概率堆叠词汇, 所以可能会出错, 尤其是缺乏上下文

    你冷不丁地去问 AI, springboot 源码的细节, AI 大概率会胡说八道

    不过等未来, AI 强大了, 个人 PC 也可以跑 AI, 让 AI 做源码"陪读", 也是完全现实的

    而且因为部署在本地, 可以把上下文 token 拉满, 让 AI 记忆细节, 就不会像现在一样, 胡说八道, 一堆幻觉

    是时候 Say Goodbye 了

    最后的最后, 感谢你的阅读!

    已有1人评分好评 油猫币 理由
    王一之 + 1 + 4 赞一个!

    查看全部评分 总评分:好评 +1  油猫币 +4 

  • TA的每日心情
    开心
    2024-11-21 13:37
  • 签到天数: 213 天

    [LV.7]常住居民III

    308

    主题

    4592

    回帖

    4359

    积分

    管理员

    积分
    4359

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2025-5-29 13:43:09 | 显示全部楼层
    Java真繁琐啊
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

  • TA的每日心情
    奋斗
    2025-4-23 20:09
  • 签到天数: 7 天

    [LV.3]偶尔看看II

    18

    主题

    17

    回帖

    290

    积分

    荣誉开发者

    积分
    290

    油中2周年新人报道荣誉开发者

    发表于 2025-5-29 15:10:09 | 显示全部楼层

    毕竟是后端屠龙刀, 个人轻量化的场景还是适合用 GO

    拿战斧切肉片, 多少有点不合适

    SpringMVC 确实是有很多前后端未分离, 遗留下来的遗产 (Legacy)

    但注解化 + springboot, 开发也还算"轻量化"了

    当年 Spring 打着 "轻量化" 的旗号革掉了 EJB 的命.

    虽然 EJB 还有小部分大型公司在用, 但已经不是主流了.

    现在 GO 会不会取代 Spring?

    未来的日子实在是不好说, 只能等历史的潮流滚滚向前, 把前浪拍死在沙滩上.
    回复

    使用道具 举报

  • TA的每日心情
    无聊
    2025-1-31 20:04
  • 签到天数: 195 天

    [LV.7]常住居民III

    753

    主题

    6623

    回帖

    7285

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    7285

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2025-5-29 23:01:31 | 显示全部楼层
    虽然看不懂
    但是支持哥哥!
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.com/a/lihengdao666
    回复

    使用道具 举报

  • TA的每日心情
    奋斗
    2025-4-23 20:09
  • 签到天数: 7 天

    [LV.3]偶尔看看II

    18

    主题

    17

    回帖

    290

    积分

    荣誉开发者

    积分
    290

    油中2周年新人报道荣誉开发者

    发表于 2025-5-30 08:56:11 | 显示全部楼层
    李恒道 发表于 2025-5-29 23:01
    虽然看不懂
    但是支持哥哥!

    感谢哥哥的支持!!
    回复

    使用道具 举报

    该用户从未签到

    0

    主题

    7

    回帖

    5

    积分

    助理工程师

    积分
    5
    发表于 4 天前 | 显示全部楼层
    我的偶像 tampermonkey 710.9551488118273
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表