生命过短暂,不要去作一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以避免费学习。关注公众号【BAT的乌托邦】逐个击破,深刻掌握,拒绝浅尝辄止。java
各位小伙伴你们好,我是A哥。上篇文章了解了static关键字 + @Bean方法的使用,知晓了它可以提高Bean的优先级,在@Bean方法前标注static关键字,特定状况下能够避免一些烦人的“警告”日志的输出,排除隐患让工程变得更加安全。咱们知道static关键字它不只可以使用在方法上,那么本文将继续挖掘static在Spring环境下的用处。程序员
根据所学的JavaSE基础,static关键字除了可以修饰方法外,还能使用在这两个地方:web
其实static还能够修饰代码块、static静态导包等,但很明显,这些与本文无关spring
接下来就以这为两条主线,分别研究static在对应场景下的做用,本文将聚焦在静态内部类上。
编程
本文内容若没作特殊说明,均基于如下版本:windows
1.8
5.2.2.RELEASE
说到Java里的static关键字,这当属最基础的入门知识,是Java中经常使用的关键字之一。你平时用它来修饰变量和方法了,可是对它的了解,即便放在JavaSE情景下知道这些仍是不够的,问题虽小但这每每反映了你对Java基础的了解程度。tomcat
固然喽,本文并不讨论它在JavaSE下使用,毕竟我们仍是有必定逼格的专栏,须要进阶一把,玩玩它在Spring环境下到底可以迸出怎么样的火花呢?好比静态内部类~安全
static修饰类只有一种状况:那就是这个类属于内部类,这就是咱们津津乐道的静态内部类,形如这样:ide
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 静态内部类傻傻分不清,为了方便后续讲解,本处把关键要素作简要对比说明:
总之,普通内部类和外部类的关系属于强绑定,而静态内部类几乎不会受到外部类的限制,能够游离单独使用。既然如此,那为什么还须要static静态内部类呢,直接单独写个Class类岂不就行了吗?存在即合理,这么使用的缘由我我的以为有以下两方面思考,供以你参考:
在传统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哥举例领你一探究竟...
本身先构造一个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(); } } }
我先解释下我这么作的意图:
运行程序,结果输出:
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...
这个结果大而全,是有说服力的,经过这几个示例能够总结出以下结论:
总的来讲,当static标注在class类上时,在同.java文件内它是可以提高优先级的,这对于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
配置类的顺序问题,事前需强调两点:
@Configuration
文件以前的初始化顺序是不肯定的@Configuration
配置类(好比静态内部类、普通内部类等),它们之间的顺序是咱们须要关心的,而且须要强依赖于这个顺序编程(好比Spring Boot)@Configuration
配置类只有是被@ComponentScan
扫描进来(或者被Spring Boot自动配置加载进来)才须要讨论顺序(假若是构建上下文时本身手动指好的,那顺序就已经定死了嘛),实际开发中的配置类也确实是酱紫的,通常都是经过扫描被加载。接下来咱们看看@ComponentScan
是如何扫描的,把此注解的解析步骤(伪代码)展现以下:
说明:本文并不会着重分析@ComponentScan它的解析原理,只关注本文“感兴趣”部分
一、解析配置类上的@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静态内部类并不属于它,这点很重要哦
二、解析该注解上的basePackages/basePackageClasses等属性值获得一些扫描的基包,委托给ClassPathBeanDefinitionScanner去完成扫描
ComponentScanAnnotationParser#parse // 使用ClassPathBeanDefinitionScanner扫描,基于类路径哦 scanner.doScan(StringUtils.toStringArray(basePackages));
三、遍历每一个基包,从文件系统中定位到资源,把符合条件的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); } } }
这段代码的信息量是很大的,分解为以下两大步:
注意:不是看.java源代码顺序,也不是看你
target
目录下的文件顺序(该目录是通过了IDEA反编译的结果,没法反应真实顺序),而是编译后看你的磁盘上的.class文件的文件顺序
@Conditional
条件isIndependent()
是独立类(top-level类 or 静态内部类属于独立类) 而且 isConcrete()是具体的(非接口非抽象类)isAbstract()
是抽象类 而且 类内存在标注有@Lookup
注解的方法基于以上例子,磁盘中的.class文件状况以下:
看着这个顺序,再结合上面的打印结果,是否是感受获得了解释呢?既然@Configuration类(外部类和内部类)的顺序肯定了,那么@Bean就跟着定了喽,由于毕竟配置类也得遍历一个一个去执行嘛(有依赖关系的case除外)。
特别说明:理论上不一样的操做系统(如windows和Linux)它们的文件系统是有差别的,对文件存放的顺序是可能不一样的(好比$xxx内部类可能放在后面),但现实情况它们是同样的,所以各位同窗对此无需担忧跨平台问题哈,这由JVM底层来给你保证。
什么,关于此解析步骤你想要张流程图?好吧,你知道的,这个A哥会放到本专栏的总结篇里统一供以你白嫖,关注我公众号吧~
看到这个截图你就懂了:在不一样.java文件内,静态内部类是不用担忧重名问题的,这不也就是内聚性的一种体现麽。
说明:beanName的生成其实和你注册Bean的方式有关,好比@Import、Scan方式是不同的,这里就不展开讨论了,知道有这个差别就成。
咱们知道,从内聚性上来讲,普通内部相似乎也能够达到目的。可是相较于静态内部类在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哥不用再作详尽分析了,看似比较复杂其实有了上面的分析仍是比较容易理解的。主要有以下两点须要注意:
isIndependent() = false
),因此它并不能像静态内部类那样预先就被扫描进去,如图结果展现:processMemberClasses()
时被解析请思考:为什么使用普通内部类获得的是这个结果呢?建议copy个人demo,自行走一遍流程,多动手老是好的
本文一如既往的很干哈。写本文的原动力是由于真的太多小伙伴在看Spring Boot自动配置类的时候,没法理解为毛它有些@Bean配置要单独写在一个static静态类里面,感受挺费事;方法前直接价格static不香吗?经过这篇文章 + 上篇文章的解读,相信A哥已经给了你答案了。
static关键字在Spring中使用的这个专栏,下篇将进入到多是你更关心的一个话题:为毛static字段不能使用@Autowired注入的分析,下篇见~