犹记我当年初学 Spring 时,还需写一个个 XML 文件,当时心里不知所以然,跟着网上的步骤一个一个配置下来,配错一个看着 error 懵半天,不知所谓地瞎改到最后能跑就行,暗自感叹 tmd 这玩意真复杂。
到后来用上 SpringBoot,看起来少了很多 XML 配置,心里暗暗高兴。起初根据默认配置跑的很正常,后面到需要改动的时候,我都不知道从哪下手。
稀里糊涂地在大部分时候也能用,但是遇到奇怪点的问题都得找老李帮忙解决。
到后面发现还有 SpringCloud ,微服务的时代来临了,我想不能再这般“犹抱琵琶半遮面”地使用 Spring 全家桶了。
一时间就钻入各种 SpringCloud 细节源码中,希望能领悟框架真谛,最终无功而返且黯然伤神,再次感叹 tmd 这玩意真复杂。
其间我已经意识到了是对 Spring 基础框架的不熟悉,导致很多封装点都不理解。
毕竟 SpringCloud 是基于 SpringBoot,而 SpringBoot 是基于 Spring。
于是乎我又回头重学 Spring,不再一来就是扎入各种细节中,我换了个策略,先从高纬角度总览 Spring ,理解核心原理后再攻克各种分支脉路。
于是我,我变强了。
其实学任何东西都是一样,先要总览全貌再深入其中,等回过头之后再进行总结。
这篇我打算用自己的理解来阐述下 Spring 的核心(思想),碍于个人表达能力可能有不对或啰嗦的地方,还请担待,如有错误恳请指出。
在初学 JAVA 时,我们理所当然得会写出这样的代码:
public class ServiceA {
private ServiceB serviceB = new ServiceB();
}
我们把一些逻辑封装到 ServiceB 中,当 ServiceA 需用到这些逻辑时候,在 ServiceA 内部 new 个ServiceB 。
如果 ServiceB 封装的逻辑非常通用,还会有 ServiceC.....ServiceF等都需要依赖它,也就是说代码里面各个地方都需要 new 个ServiceB ,这样一来如果它的构造方法发生变化,你就要在所有用到它的地方进行代码修改。
比如 ServiceB 实例的创建需要 ServiceC ,代码就改成这样:
public class ServiceA {
private ServiceB serviceB = new ServiceB(new ServiceC());
}
确实有这个问题。
但实际上如若我们封装通用的service 逻辑,没必要每次都 new 个实例,也就是说单例就够了,我们的系统只需要 new一个 ServiceB 供各个对象使用,就能解决这个问题。
public class ServiceA {
private ServiceB serviceB = ServiceB.getInstance();
}
public class ServiceB {
private static ServiceB instance = new ServiceB(new ServiceC());
private ServiceB(){}
public static ServiceB getInstance(){
return instance;
}
}
看起来好像解决问题了,其实不然。
当项目比较小时,例如大学的大作业,上面这个操作其实问题不大,但是一到企业级应用上来说就复杂了。
因为涉及的逻辑多,封装的服务类也多,之间的依赖也复杂,代码中可能要有ServiceB1、ServiceB2...ServiceB100,而且相互之间还可能有依赖关系。
抛开依赖不说,就拿 ServiceB单纯的单例逻辑代码,重复的逻辑可能需要写成百上千份。
且扩展不易,以前可能 ServiceB 的操作都不需要事务,后面要上事务了,因此需要改 ServiceB 的代码,嵌入事务相关逻辑。
没过多久 ServiceC 也要事务,一模一样关于事务的代码又得在 ServiceC 上重复一遍,还有D、E、F...
对几个 Service 事务要求又不一样,还有嵌套事务的问题,总之有点麻烦。
忙了一段时间满足事务需求,上线了,想着终于脱离了重复代码的噩梦可以好好休息一波。
紧接着又来了个需求,因为经常要排查线上问题,所以接口入参要打个日志,方便问题排查,又得大刀阔斧操作一波全部改一遍。
有需求要改动很正常,但是每次改动需要做一大堆重复性工作,又累又没技术含量还容易漏,这就不够优雅了。
所以有人就开始想办法,想从这个耦合泥沼中脱离出来。
人类绝大部分的发明都是因为懒,人们讨厌重复的工作,而计算机最喜欢也最适合做重复的工作。
既然之前的开发会有很多重复的工作,那为什么不制造一个“东西”出来帮我们做这类重复的事情呢?
就像以前人们手工一步一步组装制造产品,每天一样的步骤可能要重复上万次,到后面人们研究出全自动机器来帮我们制造产品,解放了人们的双手还提高了生产效率。
拔高了这个思想后,编码的逻辑就从我们程序员想着且写着 ServiceA 依赖具体的 ServiceB ,且一个字母一个字母的敲完 ServiceB 具体是如何实例化的代码,变成我们只关心 ServiceA 依赖 ServiceB,但 ServiceB 是如何生成的我们不管,由那个“东西”帮我们生成它且关联好 ServiceA 和 ServiceB。
public class ServiceA {
@注入
private ServiceB serviceB;
}
听起来好像有点悬乎,其实不然。
还是拿机器说事,我们创造这台机器,如果要生产产品 A,我们只要画好图纸 A,将图纸 A 塞到这个机器里,机器识别图纸 A,按照我们图纸 A 的设计制造出我们要的产品 A。
Spring就是这台机器,图纸就是依托 Spring 管理的对象代码以及那些 XML 文件(或标注了@Configuration的类)。
这时候逻辑就转变了。程序员知道 ServiceA 具体依赖哪个 ServiceB,但是我们不需要显示的在代码中写上完整的关于如何创建 ServiceB 的逻辑,我们只需要写好配置文件,具体地创建和关联由 Spring 帮我们做。
继续拿机器举例,我们给了图纸(配置),机器帮我们制造产品,具体如何制造出来不需要我们操心,但是我们心里是有数的,因为我们的图纸写明了制造 ServiceA 需要哪样的 ServiceB,而那样的 ServiceB 又需要哪样的 ServiceC等等逻辑。
我找个图纸例子,Spring 里关于数据库的配置:
可以看到我们的图纸写的很清楚,创建 MyBatis 的MApperScannerConfigurer需要告诉它两个属性的值,比如第一个是sqlSessionFactoryBeanName,值是 sqlSessionFactory。
而sqlSessionFactory又依赖 dataSource,而 dataSource 又需要配置好 driverClassName、url 等等。
所以,其实我们心里很清楚一个产品(Bean)要创建的话具体需要什么东西,只过不这个创建过程由 Spring 代劳了,我们只需要清楚的告诉它即可。
因此,不是说用了 Spring 我们不再关心 ServiceA 具体依赖怎样的 ServiceB、ServiceB具体是如何创建成功的,而是说这些对象组装的过程由 Spring 帮我们做好。
我们还是需要清楚地知道对象是如何创建的,因为我们需要画好正确的图纸告诉 Spring。
所以 Spring 其实就是一台机器,根据我们给它的图纸,自动帮我们创建关联对象供我们使用,我们不需要显示得在代码中写好完整的创建代码。
这些由 Spring 创建的对象实例,叫作 Bean。
我们如果要使用这些 Bean 可以从 Spring 中拿,Spring 将这些创建好的单例 Bean 放在一个 Map 中,通过名字或者类型我们可以获取这些 Bean。
这就是 IOC。
也正因为这些 Bean 都需要经过 Spring 这台机器创建,不再是懒散地在代码的各个角落创建,我们就能很方便的基于这个统一收口做很多事情。
比如当我们的 ServiceB 标注了 @Transactional 注解,由 Spring 解析到这个注解就能明白这个 ServiceB 是需要事务的,于是乎织入的事务的开启、提交、回滚等操作。
但凡标记了 @Transactional 注解的都自动添加事务逻辑,这对我们而言减轻了太多重复的代码,只要在需要事务的方法或类上添加 @Transactional注解即可由 Spring 帮我们补充上事务功能,重复的操作都由 Spring 完成。
再比如我们需要在所有的 controller 上记录请求入参,这也非常简单,我们只要写个配置,告诉 Spring xxx路径(controller包路径)下的类的每个方法的入参都需要记录在 log 里,并且把日志打印逻辑代码也写上。
Spring 解析完这个配置后就得到了这个命令,于是乎在创建后面的 Bean 时就看看它所处的包是否符合上述的配置,若符合就把我们添加日志打印逻辑和原有的逻辑编织起来。
这样就把重复的日志打印动作操作抽象成一个配置,Spring 这台机器识别配置后执行我们的命令完成这些重复的动作。
这就叫 AOP。
至此我相信你对 Spring 的由来和核心概念有了一定的了解,基于上面的特性能做的东西有很多。
因为有了 Spring 这个机器统一收口处理,我们就可以灵活在不同时期提供很多扩展点,比如配置文件解析的时候、Bean初始化的前后,Bean实例化的前后等等。
基于这些扩展点就能实现很多功能,例如 Bean 的选择性加载、占位符的替换、代理类(事务等)的生成。
好比 SpringBoot redis 客户端的选择,默认会导入 lettuce 和 jedis两个客户端配置
基于配置的先后顺序会优先导入 lettuce,然后再导入 jedis。
如果扫描发现有 lettuce 那么就用 lettuce 的 RedisConnectionFactory,而后面再加载 jedis 时,会基于@ConditionalOnMissingBean(RedisConnectionFactory.class) 来保证 jedis不会被注入,反之就会被注入。
ps:@ConditionalOnMissingBean(xx.class) 如果当前没有xx.class才能生成被这个注解修饰的bean
就上面这个特性就是基于 Spring 提供的扩展点来实现的。
很灵活地让我们替换所需的 redis 客户端,不用改任何使用的代码,只需要改个依赖,比如要从默认的 lettuce 变成 jedis,只需要改个 maven 配置,去除 lettuce 依赖,引入 jedis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
说这么多其实就是想表达:Spring 全家桶提供的这些扩展和封装可以灵活地满足我们的诸多需求,而这些灵活都是基于 Spring 的核心 IOC 和 AOP 而来的。
最后我用一段话来简单描述下 Spring 的原理:
Spring 根据我们提供的配置类和XML配置文件,解析其中的内容,得到它需要管理的 Bean 的信息以及之间的关联,并且 Spring 暴露出很多扩展点供我们定制,如 BeanFactoryPostProcessor、BeanPostProcessor,我们只需要实现这个接口就可以进行一些定制化的操作。
Spring 得到 Bean 的信息后会根据反射来创建 Bean 实例,组装 Bean 之间的依赖关系,其中就会穿插进原生的或我们定义的相关PostProcessor来改造Bean,替换一些属性或代理原先的 Bean 逻辑。
最终创建完所有配置要求的Bean,将单例的 Bean 存储在 map 中,提供 BeanFactory 供我们获取使用 Bean。
使得我们编码过程无需再关注 Bean 具体是如何创建的,也节省了很多重复性地编码动作,这些都由我们创建的机器——Spring帮我们代劳。
大概就说这么多了,我自己读了几遍也不知道到底有没有把我想表达的东西说明白,其实我本来从源码层面来聊这个核心的,但是怕更难说清。