乐趣区

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

生命太短暂,不要去做一些基本没有人想要的货色。本文已被 https://www.yourbatman.cn 收录,外面一并有 Spring 技术栈、MyBatis、JVM、中间件等小而美的 专栏 供以收费学习。关注公众号【BAT 的乌托邦】一一击破,深刻把握,回绝浅尝辄止。

前言

各位小伙伴大家好,我是 A 哥。上篇文章理解了 static 关键字 + @Bean 办法 的应用,通晓了它可能晋升 Bean 的优先级,在 @Bean 办法前标注 static 关键字,特定状况下能够防止一些烦人的“正告”日志的输入,排除隐患让工程变得更加平安。咱们晓得 static 关键字它不仅可应用在办法上,那么本文将持续开掘 static 在 Spring 环境下的用途。

依据所学的 JavaSE 根底,static 关键字除了可能润饰办法外,还能应用在这两个中央:

  1. 润饰类。确切的说,应该叫润饰 外部类,所以它叫动态外部类
  2. 润饰成员变量

其实 static 还能够润饰代码块、static 动态导包等,但很显著,这些与本文无关

接下来就以这为两条主线,别离钻研 static 在对应场景下的作用,本文将聚焦在动态外部类上。


版本约定

本文内容若没做非凡阐明,均基于以下版本:

  • JDK:1.8
  • Spring Framework:5.2.2.RELEASE

注释

说到 Java 里的 static 关键字,这当属最根底的入门常识,是 Java 中罕用的关键字之一。你平时用它来润饰变量和办法了,然而对它的理解,即便放在 JavaSE 情景下晓得这些还是 不够的,问题虽小但这往往反映了你对 Java 根底的理解水平。

当然喽,本文并不探讨它在 JavaSE 下应用,毕竟咱们还是 有肯定逼格的 专栏,须要进阶一把,玩玩它在 Spring 环境下到底可能迸出怎么样的火花呢?比方动态外部类~


Spring 下的动态外部类

static 润饰类 只有一种状况 :那就是这个类属于外部类,这就是咱们津津有味的 动态外部类,形如这样:

public class Outer {

    private String name;
    private static Integer age;

    // 动态外部类
    private static class Inner {

        private String innerName;
        private static Integer innerAge;

        public void fun1() {
            // 无法访问外部类的成员变量
            //System.out.println(name);
            System.out.println(age);

            System.out.println(innerName);
            System.out.println(innerAge);
        }

    }

    public static void main(String[] args) {
        // 动态外部类的实例化并不需要依赖于外部类的实例
        Inner inner = new Inner();}
}

在理论开发中,动态外部类的应用场景是十分之多的。


意识动态 / 一般外部类

因为一些小伙伴对一般外部类 vs 动态外部类傻傻分不清,为了不便后续解说,本处把 要害因素 做简要比照阐明:

  1. 动态外部类能够申明动态 or 实例成员 (属性和办法);而一般外部类则 不能够 申明动态成员(属性和办法)
  2. 动态外部类实例的创立 不依赖于 外部类;而一般外部类实例创立必须先有外部类实例才行(绑定关系拿捏得死死的,不信你问郑凯)
  3. 动态外部类 不能 拜访外部类的实例成员;而一般外部类能够随便拜访(不论动态 or 非动态) –> 我了解这是一般外部类能 “存活” 下来的最大理由了吧????

总之,一般外部类和外部类的关系属于 强绑定 ,而动态外部类简直不会受到外部类的限度,能够游离 独自应用。既然如此,那为何还须要 static 动态外部类呢,间接独自写个 Class 类岂不就好了吗?存在即正当,这么应用的起因我集体感觉有如下两方面思考,供以你参考:

  • 动态外部类是弱关系并不是没关系,比方它还是能够拜访外部类的 static 的变量的不是(即使它是 private 的)
  • 高内聚的体现

在传统 Spirng Framework 的配置类场景下,你可能鲜有接触到 static 关键字应用在类上的场景,但这在 Spring Boot 下应用十分频繁,比方属性配置类的典型利用:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
    
    // server.port = xxx 
    // server.address = xxx
    private Integer port;
    private InetAddress address;
    ...
    
    // tomcat 配置
    public static class Tomcat {
        
        // server.tomcat.protocol-header = xxx
        private String protocolHeader;
        ...
        
        // tomcat 内的 log 配置
        public static class Accesslog {
            
            // server.tomcat.accesslog.enabled = xxx
            private boolean enabled = false;
            ...
        }
    }    
}

这种嵌套 case 使得代码(配置)的 key 内聚性十分强 ,应用起来更加不便。试想一下,如果你 不应用 动态外部类去集中管理这些配置,每个配置都独自书写的话,像这样:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
}

@ConfigurationProperties(prefix = "server.tomcat", ignoreUnknownFields = true)
public class TomcatProperties {
}

@ConfigurationProperties(prefix = "server.tomcat.accesslog", ignoreUnknownFields = true)
public class AccesslogProperties {}

这代码,就问你,如果是你共事写的,你骂不骂吧!用 臃肿 来形容还是个中意词,层次结构体现得也十分的不直观嘛。因而,对于这种属性类里应用动态外部类是非常适合,内聚性一下子高很多~

除了在内聚性上的作用,在 Spring Boot 中的 @Configuration 配置类下(特地常见于主动配置类)也能常常看到它的身影:

@Configuration(proxyBeanMethods = false)
public class WebMvcAutoConfiguration {

    // web MVC 个性化定制配置
    @Configuration(proxyBeanMethods = false)
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {...}

    @Configuration(proxyBeanMethods = false)
    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {...}

}

利用动态外部类把类似配置类 归并 在一个 .java 文件 内,这样多个 static 类还可 专用 外部类的属性、办法,也是一种高内聚的体现。同时 static 关键字晋升了初始化的优先级,比方本例中的 EnableWebMvcConfiguration 它会优先于外部类加载~

对于 static 动态外部类优先级相干是 重点,动态外部类的优先级会更高吗?应用一般外部能达到同样成果吗?拍脑袋间接答复是没用的,带着这两个问题,接下来 A 哥举例领你一探到底 …


static 动态配置类晋升配置优先级

本人先结构一个 Demo,场景如下:

@Configuration
class OuterConfig {OuterConfig() {System.out.println("OuterConfig init...");
    }
    @Bean
    static Parent parent() {return new Parent();
    }

    @Configuration
    private static class InnerConfig {InnerConfig() {System.out.println("InnerConfig init...");
        }
        @Bean
        Daughter daughter() {return new Daughter();
        }
    }
}

测试程序:

@ComponentScan
public class TestSpring {public static void main(String[] args) {new AnnotationConfigApplicationContext(TestSpring.class);
    }
}

启动程序,后果输入:

InnerConfig init...
OuterConfig init...
Daughter init...
Parent init...

后果细节:仿佛都是依照字母表的程序来执行的。I 在前 O 在后;D 在前 P 在后;

看到这个后果,如果你就过早的得出结论:动态外部类优先级高于外部类,那么就太随便了,图样图森破啊。大胆猜测,小心求证 应该是程序员应有的态度,那么持续往下看,在此基础上我新减少一个动态外部类:

@Configuration
class OuterConfig {OuterConfig() {System.out.println("OuterConfig init...");
    }
    @Bean
    static Parent parent() {return new Parent();
    }


    @Configuration
    private static class PInnerConfig {PInnerConfig() {System.out.println("PInnerConfig init...");
        }
        @Bean
        Son son() {return new Son();
        }
    }

    @Configuration
    private static class InnerConfig {InnerConfig() {System.out.println("InnerConfig init...");
        }
        @Bean
        Daughter daughter() {return new Daughter();
        }
    }
}

我先解释下我这么做的用意:

  1. 减少一个字母 P 结尾的外部类,天然程序 P 在 O(外部类)前面,打消影响
  2. P 结尾的外部类在源码摆放程序上 成心 放在了 I 结尾的外部类的 下面,同样为了打消字母表程序带来的影响

    1. 目标:看看是依照字节码程序,还是字母表程序呢?
  3. PInnerConfig 外面的 @Bean 实例为 Son,字母表程序是三者中最为靠后的,但字节码却在两头,这样也可能打消影响

运行程序,后果输入:

InnerConfig init...
PInnerConfig init...
OuterConfig init...
Daughter init...
son init...
Parent init...

后果细节:外部类貌似总是滞后于外部类初始化;同一类的多个外部类之间程序是依照字母表程序(天然排序)初始化而非字节码程序;@Bean 办法的程序按照了类的程序

请注意本后果和下面后果是否有区别,你应该若有所思。

这是单.java 文件的 case(所有 static 类都在同一个.java 文件内),接下来我在同目录下 减少 2 个.java 文件(请自行注意类名第一个字母,我将不再赘述我的设计用意):

// 文件一:@Configuration
class A_OuterConfig {A_OuterConfig() {System.out.println("A_OuterConfig init...");
    }
    @Bean
    String a_o_bean(){System.out.println("A_OuterConfig a_o_bean init...");
        return new String();}


    @Configuration
    private static class PInnerConfig {PInnerConfig() {System.out.println("A_OuterConfig PInnerConfig init...");
        }
        @Bean
        String a_p_bean(){System.out.println("A_OuterConfig a_p_bean init...");
            return new String();}
    }

    @Configuration
    private static class InnerConfig {InnerConfig() {System.out.println("A_OuterConfig InnerConfig init...");
        }
        @Bean
        String a_i_bean(){System.out.println("A_OuterConfig a_i_bean init...");
            return new String();}
    }
}

// 文件二:@Configuration
class Z_OuterConfig {Z_OuterConfig() {System.out.println("Z_OuterConfig init...");
    }
    @Bean
    String z_o_bean(){System.out.println("Z_OuterConfig z_o_bean init...");
        return new String();}


    @Configuration
    private static class PInnerConfig {PInnerConfig() {System.out.println("Z_OuterConfig PInnerConfig init...");
        }
        @Bean
        String z_p_bean(){System.out.println("Z_OuterConfig z_p_bean init...");
            return new String();}
    }

    @Configuration
    private static class InnerConfig {InnerConfig() {System.out.println("Z_OuterConfig InnerConfig init...");
        }
        @Bean
        String z_i_bean(){System.out.println("Z_OuterConfig z_i_bean init...");
            return new String();}
    }
}

运行程序,后果输入:

A_OuterConfig InnerConfig init...
A_OuterConfig PInnerConfig init...
A_OuterConfig init...
InnerConfig init...
PInnerConfig init...
OuterConfig init...
Z_OuterConfig InnerConfig init...
Z_OuterConfig PInnerConfig init...
Z_OuterConfig init...


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

这个后果大而全,是有说服力的,通过这几个示例能够总结出如下论断:

  1. 垮.java 文件(垮配置类)之间的程序,是由天然程序来保障的(字母表程序)

    1. 如上:下加载 A 打头的配置类(含动态外部类),再是 O 打头的,再是 Z 打头的
  2. 同一.java 文件外部 ,static 动态外部类 优先于 外部类初始化。若有多个动态外部类,那么依照类名天然排序初始化(并非依照定义程序哦,请务必留神)

    1. 阐明:个别外部类只可能与外部类“产生关系”,与兄弟之间不倡议有任何分割,否则顺序控制上你就得当心了。毕竟靠天然程序去保障是一种弱保障,容错性太低
  3. 同一.java 文件内,不同类内的 @Bean 办法之间的执行程序,放弃同 2 统一(也就说你的 @Bean 所在的 @Configuration 配置类先加载,那你就优先被初始化喽)

    1. 同一 Class 内多个 @Bean 办法的执行程序,上篇文章 static 关键字真能进步 Bean 的优先级吗?答:真能 就曾经说过了哈,请移步参见

总的来说,当 static 标注在 class 类上时,在 同.java 文件内 它是可能晋升优先级的,这对于 Spring Boot 的主动配置十分有意义,次要体现在如下两个办法:

  • static 动态外部类配置优先于外部类加载,从而动态外部类外面的 @Bean 也 优先于 外部类的 @Bean 先加载
  • 既然这样,那么 Spring Boot 主动配置就能够联合此个性,就能够进行具备优先级的 @Conditional 条件判断了。这里我举个官网的例子,你便能感触到它的魅力所在:
@Configuration
public class FeignClientsConfiguration {
    ...
    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    public Feign.Builder feignBuilder(Retryer retryer) {return Feign.builder().retryer(retryer);
    }

    @Configuration
    @ConditionalOnClass({HystrixCommand.class, HystrixFeign.class})
    protected static class HystrixFeignConfiguration {
        @Bean
        @Scope("prototype")
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "feign.hystrix.enabled")
        public Feign.Builder feignHystrixBuilder() {return HystrixFeign.builder();
        }
    }
}

因为 HystrixFeign.builder() 它属于动态外部类,所以这个 @Bean 必定是优先于内部的 Feign.builder() 先加载的。所以这段逻辑可解释为:优先应用 HystrixFeign.builder()(若条件满足),否则应用Feign.builder().retryer(retryer) 作为兜底。通过此例你应该再一次感触到Bean 的加载程序之于 Spring 利用的重要性,特地在 Spring Boot/Cloud 下此个性尤为凸显。

你认为记住这几个论断就完事了?不,这显著不合乎 A 哥的逼格嘛,上面咱们就来持续挖一挖吧。


源码剖析

对于 @Configuration 配置类的程序问题,事先需强调两点:

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

    1. 因为无状态性,因而你在应用过程中能够认为垮 @Configuration 文件之前的初始化程序 是不确定的
  2. 同一.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 文件的文件程序

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

    1. 过滤一:应用 TypeFilter 执行过滤,看看是否被排除;再看看是否满足 @Conditional 条件
    2. 过滤二:它有两种 case 能满足条件(任意满足一个 case 即可)

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

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

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

特地阐明:实践上不同的操作系统(如 windows 和 Linux)它们的文件系统是有差别的,对文件寄存的程序是可能不同的(比方 $xxx 外部类可能放在前面),但 现实状况 它们是一样的,因而各位同学对此无需放心跨平台问题哈,这由 JVM 底层来给你保障。

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


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

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

阐明: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),所以它并不能像动态外部类那样事后就被 扫描进去,如图后果展现:

  1. 一般外部类初始化之前,肯定 得先初始化外部类,所以类自身的优先级是低于外部类的(不蕴含 @Bean 办法哦)
  2. 一般外部类属于外部类的 memberClasses,因而它会在解析 以后外部类 的第一步 processMemberClasses() 时被解析
  3. 一般外部类的 beanName 和动态外部类是有差别的,如下截图:


思考题:

请思考:为何应用一般外部类失去的是这个后果呢?倡议 copy 我的 demo,自行走一遍流程,多入手总是好的


总结

本文判若两人的很干哈。写本文的 原动力 是因为真的太多小伙伴在看 Spring Boot 主动配置类的时候,无奈了解为毛它有些 @Bean 配置要独自写在一个 static 动态类 外面,感觉挺麻烦;办法前间接价格 static 不香吗?通过 这篇文章 + 上篇文章 的解读,置信 A 哥曾经给了你答案了。

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


关注 A 哥

Author A 哥(YourBatman)
集体站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
沉闷平台
公众号 BAT 的乌托邦(ID:BAT-utopia)
常识星球 BAT 的乌托邦
每日文章举荐 每日文章举荐

退出移动版