精通Spring?请吃我一狗腿!
我要是说,很多Java大牛,根本没有深入用过Spring。你可能会非常吃惊,因为Spring应用面如此广泛,特性如此之多,况且Spring、SpringBoot、SpringCloud子孙三代,多次避免了Java的衰弱,功不可没,没有理由不深入了解一下。
Java能走到今天,Spring功不可没。要说Spring好,那是100个赞美之词都说不清的。夸到极致就是骂,我们只能说:Spring牛B!
但为什么很多大牛很少使用Spring呢?这也是由于工作特性决定的。他们经常写一些中间件,做一些分布式引用,这个时候引入一个Spring,就臃肿的多。比起Spring,他们使用Netty反而更多一些。
Spring家族多解决的是工程方面的问题,然而世界上的项目,工程类问题居多,也最容易让老板赚钱,所以一拍即合。
大多数情况下,Spring非常难用,尤其是你经历过xml配置爆炸的年代。所以SpringBoot在此基础上,整合了一套快速开发的工具包。开箱即用,一行代码就能启动。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码
按理来说,框架使用如此简单了,是件好事。但偏有些不要命的,简历上赤裸裸的写上了精通
两个发光的金字,直接亮瞎了面试官的眼睛。想把精通者捶成二维的---这种荷尔蒙就开始飙升。你要知道,Spring家族现在的源码大小,可足足有数GB之巨,你真的全搞定了么?
怕不是个神人。
下面的内容不区分Spring和SpringBoot,因为区分本身就意义不大。
Spring的面试题大多无意义
对于工作年龄超过3年的面试者,面试官搜肠刮肚,找不出与Spring相关的几个问题。IOC、AOP这种侮辱人的发问,大多数工作多年的人回答不好,因为就是个文字游戏。翻来覆去没啥问头,突然间灵光一闪:我们聊点设计模式方面的东西吧。
不不不,设计模式本质上来说,也是属于咬文嚼字的范畴,而且容易过度设计,不过这也总比Spring源码什么的好太多。
不是精通Spring么?跑题了跑题了。
spring-data?spring-security?spring-webflow?这和spring本身有毛的关系,这是解决方案。不在行业场景上浸润个一两年,谈什么精通?要是聊起来,你会发现和这就是行业知识,其中有没有Spring参与关系并不大。
所以你的精通Spring,大概率是精通在初级使用上,尤其是那几个看多了会吐、会自我繁殖的注解。
-
什么是spring?
问这个问题的人脑子有病,下一题。
-
使用Spring框架的好处是什么?
问这种问题的人,占用你宝贵的人生时间,建议你反问:您是相对于哪个框架来说的呢?
-
Spring都用了哪些设计模式?
他想听工厂、模板、单例,那就随便找一个让他听。10秒钟解决。
你要问一个精通的人,这样的问题,会让人措手不及。我就见过一个喜欢照搬《effective java》
里面一些又臭又长的定理去死扣的面试官。不要觉得这是本神书,那只适合你静静思考的时候。
就拿《effective java》第一条规则来说吧:为什么静态工厂方法替代构造方法?书中列举了5个理由。
- 可以有名字
- 不需要每次创建一个新对象
- 可以返回任何某个类型的子类型
- 可以根据参数返回不同的对象
- 返回的对象并不需要一定存在
仔细感受一下吧。你的周围,几乎不会有任何人能够回答全面,但是并不影响他们写代码,甚至是写核心库。所以这些又臭又长的废话,其实是你心里约定俗成的知识。
比如我问你,为什么心脏要长在左边而不是右边?这是没意义的问题。
那么Spring相关的,出现频率最高的题目是什么呢?那就是SpringBoot的启动过程。见微知著,每一个深受Spring其害的人,都应该了解一二。它本来是可以按照顺序编程的方式来实现的,但不知道那位天才脑子一零光,就发明了@Import注解,从此各种spring-boot-starter进入了只有翻源码才能知道它干啥的年代。
其实我们可以分为两部分。
SpringBoot加载流程
首先,SpringBootApplication这个注解,里面其实包含了另外一堆注解。比较重要的就有,
SpringBootConfiguration
表明这是一个配置类,会被扫描加载到IoC
容器ComponentScan
定义扫描的路径,默认从注解所在类的package进行扫描EnableAutoConfiguration
这是SpringBoot特有的注解,用于加载SPI定义的类
除了最后一个,其他两个没什么好说的。Enable系列,其实做的事情特别多,它借助@Import注解,动态的注册特定场景的bean定义。比如常见的@EnableCaching
、@EnableDiscoveryClient
等。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
复制代码
请看上面的代码,Import这个注解,实际上干了一一些非常重的活。它导入了一个叫做AutoConfigurationImportSelector
的类。SpringBoot所有的初始化动作,都是从这里进入的。不仅仅是Enable*
注解,就连AutoConfigurationPackage注解,也是通过Import导入了一个叫做Registrar
的类。
所以@Import这个注解,真的很强大。由于AutoConfigurationImportSelector实现了ImportSelector
接口,所以也就实现了它的selectImports
方法。META-INF/spring.factories
的加载,就是在这个方法中实现的。
就这样一层套一层,从EnableAutoConfiguration注解,进入到ImportSelector实现内部,进而将加载的控制权,交给了META-INF/spring.factories
所指定的AutoConfiguration上。
这里要强调一下:这种加载流程,并不见得就一定非常好。但由于它是SpringBoot的实现方式,加上SpringBoot又非常流行,遇到问题的时候希望你能很快去处理。综合起来,它就变成了一种必要的知识。
SpringBoot启动流程
要了解SpringBoot的启动流程,得先看一下它提供给我们的各种钩子。我们有@PostConstruct
注解,也有InitializingBean
接口,还有CommandLineRunner
等。甚至会有更多。
我们要对这些实现方式进行归类,因为有的是属于全局的,有的是属于Bean的。
就拿全局的SpringBoot监听来说,竟然就有CommandLineRunner
和ApplicationRunner
两个功能雷同的接口,不得不说是非常的臃肿。
具体流程肯定是又臭又长,但这些钩子函数是怎么处理的呢?实际上是通过事件进行分发的。不管是全局的还是Bean的,都会缓存一个钩子列表,等到执行到相应的流程时,就调用这个接口。大多数钩子,既然是个列表,拿就会有顺序,加入@Order注解就能指定执行的顺序。
我向来对这里面的繁文缛节不感兴趣,因为机制就放在那里,到底要暴露多少事件,拆成多细,要看框架设计者的心情。没必要把它当作一种真理,换了一个框架粒度和名字都会不一样,但原理相通。
End
非常可惜的是,Spring和SpringBoot,选择了最大可能的暴露这些内部的接口,并在功能上加入了很多语义化的API,比如原来的Bean竟然扩展除了@Service、@Component、@Repository等等一些列注解。造成的后果就是用什么有什么,上手简单但用起来复杂;各种包装和跳转,连debug信息都不知所云;只要你暴露了接口,总有人不按规矩出牌,所以使用的时候,一般会使用规范去约束。
这些不是真理,是不过是Spring选择了众多可能中的一种而已。比如Python和yml的缩进,go的自动format,不是真理,是选择,不要搞混了。你可以要求别人这么做,但你不能说这么做就是绝对正确的。
对于一个公司来说,并不希望看到这种灵活性。比如你提供了CommandLineRunner
接口,就不要再闲操心做个ApplicationRunner
,这会增加程序员心智负担。一个spring-data-jpa
,光查询方式就有五六种,难道自己不会痛么?
更要命的是,有些人以此为豪,这大可不必。殊不知你我的工作,不过是从一座屎山脱身,然后去梳理另外一座屎山罢了。但问题是,你需要带这么多铲子干什么?