static关键字有何魔法?竟让Spring Boot搞出那么多静态内部类(下)

简介: static关键字有何魔法?竟让Spring Boot搞出那么多静态内部类(下)

源码分析


关于@Configuration配置类的顺序问题,事前需强调两点:


  1. 不同 .java文件 之间的加载顺序是不重要的,Spring官方也强烈建议使用者不要去依赖这种顺序

     1.因为无状态性,因此你在使用过程中可以认为垮@Configuration文件之前的初始化顺序是不确定的

  1. 同一.javaw文件内也可能存在多个@Configuration配置类(比如静态内部类、普通内部类等),它们之间的顺序是我们需要关心的,并且需要强依赖于这个顺序编程(比如Spring Boot)


@Configuration配置类只有是被@ComponentScan扫描进来(或者被Spring Boot自动配置加载进来)才需要讨论顺序(倘若是构建上下文时自己手动指好的,那顺序就已经定死了嘛),实际开发中的配置类也确实是酱紫的,一般都是通过扫描被加载。接下来我们看看@ComponentScan是如何扫描的,把此注解的解析步骤(伪代码)展示如下:


说明:本文并不会着重分析@ComponentScan它的解析原理,只关注本文“感兴趣”部分


1、解析配置类上的@ComponentScan注解(们):本例中TestSpring作为扫描入口,会扫描到A_OuterConfig/OuterConfig等配置类们


ConfigurationClassParser#doProcessConfigurationClass:
  // **最先判断** 该配置类是否有成员类(普通内部类)
  // 若存在普通内部类,最先把普通内部类给解析喽(注意,不是静态内部类)
  if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
    processMemberClasses(configClass, sourceClass);
  }
  ...
  // 遍历该配置类上所有的@ComponentScan注解
  // 使用ComponentScanAnnotationParser一个个解析
  for (AnnotationAttributes componentScan : componentScans) {
    Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan,...);
    // 继续判断扫描到的bd是否是配置类,递归调用
    ... 
  }


细节说明:关于最先解析内部类时需要特别注意,Spring通过sourceClass.getMemberClasses()来获取内部类们:只有普通内部类属于这个,static静态内部类并不属于它,这点很重要哦


2、解析该注解上的basePackages/basePackageClasses等属性值得到一些扫描的基包,委托给ClassPathBeanDefinitionScanner去完成扫描


ComponentScanAnnotationParser#parse
  // 使用ClassPathBeanDefinitionScanner扫描,基于类路径哦
  scanner.doScan(StringUtils.toStringArray(basePackages));


3、遍历每个基包,从文件系统中定位到资源,把符合条件的Spring组件(强调:这里只指外部@Configuration配置类,还没涉及到里面的@Bean这些)注册到BeanDefinitionRegistry注册中心


ComponentScanAnnotationParser#doScan
  for (String basePackage : basePackages) {
    // 这个方法是本文最需要关注的方法
    Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
    for (BeanDefinition candidate : candidates) {
      ...
      // 把该配置**类**(并非@Bean方法)注册到注册中心
      registerBeanDefinition(definitionHolder, this.registry);
    }
  }


到这一步就完成了Bean定义的注册,此处可以验证一个结论:多个配置类之间,谁先被扫描到,就先注册谁,对应的就是谁最先被初始化。那么这个顺序到底是咋样界定的呢?那么就要来到这中间最为重要(本文最关心)的一步喽:findCandidateComponents(basePackage)。


说明:Spring 5.0开始增加了@Indexed注解为云原生做了准备,可以让scan扫描动作在编译期就完成,但这项技术还不成熟,暂时几乎无人使用,因此本文仍旧只关注经典模式的实现


ClassPathScanningCandidateComponentProvider#scanCandidateComponents
  // 最终返回的候选组件们
  Set<BeanDefinition> candidates = new LinkedHashSet<>();
  // 得到文件系统的路径,比如本例为classpath*:com/yourbatman/**/*.class
  String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
          resolveBasePackage(basePackage) + '/' + this.resourcePattern;
  // 从文件系统去加载Resource资源文件进来
  // 这里Resource代表的是一个本地资源:存在你硬盘上的.class文件
  Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
  for (Resource resource : resources) {
    if (isCandidateComponent(metadataReader)) {
      if (isCandidateComponent(sbd)) {
        candidates.add(sbd);
      }
    }
  }


这段代码的信息量是很大的,分解为如下两大步:


1.通过ResourcePatternResolver从磁盘里加载到所有的 .class资源Resource[]。这里面顺序信息就出现了,加载磁盘Resource资源的过程很复杂,总而言之它依赖于你os文件系统。所以关于资源的顺序可简单理解为:你磁盘文件里是啥顺序它就按啥顺序加载进来


注意:不是看.java源代码顺序,也不是看你target目录下的文件顺序(该目录是经过了IDEA反编译的结果,无法反应真实顺序),而是编译后看你的磁盘上的.class文件的文件顺序


2.遍历每一个Resource资源,并不是每个资源都会成为candidates候选,它有个双重过滤(对应两个isCandidateComponent()方法):


1.过滤一:使用TypeFilter执行过滤,看看是否被排除;再看看是否满足@Conditional条件


2.过滤二:它有两种case能满足条件(任意满足一个case即可)

  1. isIndependent()是独立类(top-level类 or 静态内部类属于独立类) 并且 isConcrete()是具体的(非接口非抽象类)
  2. isAbstract()是抽象类 并且 类内存在标注有@Lookup注解的方法


基于以上例子,磁盘中的.class文件情况如下:

image.png


看着这个顺序,再结合上面的打印结果,是不是感觉得到了解释呢?既然@Configuration类(外部类和内部类)的顺序确定了,那么@Bean就跟着定了喽,因为毕竟配置类也得遍历一个一个去执行嘛(有依赖关系的case除外)。


特别说明:理论上不同的操作系统(如windows和Linux)它们的文件系统是有差异的,对文件存放的顺序是可能不同的(比如$xxx内部类可能放在后面),但现实状况它们是一样的,因此各位同学对此无需担心跨平台问题哈,这由JVM底层来给你保证。


什么,关于此解析步骤你想要张流程图?好吧,你知道的,这个A哥会放到本专栏的总结篇里统一供以你白嫖,关注我公众号吧~


静态内部类在容器内的beanName是什么?


看到这个截图你就懂了:在不同.java文件内,静态内部类是不用担心重名问题的,这不也就是内聚性的一种体现麽。


image.png


说明:beanName的生成其实和你注册Bean的方式有关,比如@Import、Scan方式是不一样的,这里就不展开讨论了,知道有这个差异就成。


进阶:Spring下普通内部类表现如何?


我们知道,从内聚性上来说,普通内部类似乎也可以达到目的。但是相较于静态内部类在Spring容器内对优先级的问题,它的表现可就没这么好喽。基于以上例子,把所有的static关键字去掉,就是本处需要的case。


reRun测试程序,结果输出:


A_OuterConfig init...
OuterConfig init...
Z_OuterConfig init...
A_OuterConfig InnerConfig init...
A_OuterConfig a_i_bean init...
A_OuterConfig PInnerConfig init...
A_OuterConfig a_p_bean init...
A_OuterConfig a_o_bean init...
InnerConfig init...
Daughter init...
PInnerConfig init...
son init...
Parent init...
Z_OuterConfig InnerConfig init...
Z_OuterConfig z_i_bean init...
Z_OuterConfig PInnerConfig init...
Z_OuterConfig z_p_bean init...
Z_OuterConfig z_o_bean init...


对于这个结果A哥不用再做详尽分析了,看似比较复杂其实有了上面的分析还是比较容易理解的。主要有如下两点需要注意:


  1. 普通内部类它不是一个独立的类(也就是说isIndependent() = false),所以它并不能像静态内部类那样预先就被扫描进去,如图结果展示:

image.png


2.普通内部类初始化之前,一定得先初始化外部类,所以类本身的优先级是低于外部类的(不包含@Bean方法哦)


3.普通内部类属于外部类的memberClasses,因此它会在解析当前外部类的第一步processMemberClasses()时被解析


4.普通内部类的beanName和静态内部类是有差异的,如下截图:


image.png


思考题:


请思考:为何使用普通内部类得到的是这个结果呢?建议copy我的demo,自行走一遍流程,多动手总是好的


总结


本文一如既往的很干哈。写本文的原动力是因为真的太多小伙伴在看Spring Boot自动配置类的时候,无法理解为毛它有些@Bean配置要单独写在一个static静态类里面,感觉挺费事;方法前直接价格static不香吗?通过这篇文章 + 上篇文章的解读,相信A哥已经给了你答案了。


static关键字在Spring中使用的这个专栏,下篇将进入到可能是你更关心的一个话题:为毛static字段不能使用@Autowired注入的分析,下篇见~

相关文章
|
8月前
|
XML Java 应用服务中间件
Spring Boot 两种部署到服务器的方式
本文介绍了Spring Boot项目的两种部署方式:jar包和war包。Jar包方式使用内置Tomcat,只需配置JDK 1.8及以上环境,通过`nohup java -jar`命令后台运行,并开放服务器端口即可访问。War包则需将项目打包后放入外部Tomcat的webapps目录,修改启动类继承`SpringBootServletInitializer`并调整pom.xml中的打包类型为war,最后启动Tomcat访问应用。两者各有优劣,jar包更简单便捷,而war包适合传统部署场景。需要注意的是,war包部署时,内置Tomcat的端口配置不会生效。
2131 17
Spring Boot 两种部署到服务器的方式
|
6月前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——指定项目配置文件
在实际项目中,开发环境和生产环境的配置往往不同。为简化配置切换,可通过创建 `application-dev.yml` 和 `application-pro.yml` 分别管理开发与生产环境配置,如设置不同端口(8001/8002)。在 `application.yml` 中使用 `spring.profiles.active` 指定加载的配置文件,实现环境快速切换。本节还介绍了通过配置类读取参数的方法,适用于微服务场景,提升代码可维护性。课程源码可从 [Gitee](https://gitee.com/eson15/springboot_study) 下载。
216 0
|
11月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
315 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
10月前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
500 2
|
11月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
307 2
|
运维 Java 关系型数据库
Spring运维之boot项目bean属性的绑定读取与校验
Spring运维之boot项目bean属性的绑定读取与校验
153 2
|
存储 运维 Java
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
185 2
|
Java Maven
springboot项目打jar包后,如何部署到服务器
springboot项目打jar包后,如何部署到服务器
844 1
springboot2.4.5使用pagehelper分页插件
springboot2.4.5使用pagehelper分页插件
356 0
|
缓存 运维 Java
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
138 0