通透,浓缩多年一文俯瞰Spring核心原理基础

你好,我是yes。

犹记我当年初学 Spring 时,还需写一个个 XML 文件,当时心里不知所以然,跟着网上的步骤一个一个配置下来,配错一个看着 error 懵半天,不知所谓地瞎改到最后能跑就行,暗自感叹 *** 这玩意真复杂。

到后来用上 SpringBoot,看起来少了很多 XML 配置,心里暗暗高兴。起初根据默认配置跑的很正常,后面到需要改动的时候,我都不知道从哪下手。

稀里糊涂地在大部分时候也能用,但是遇到奇怪点的问题都得找老李帮忙解决。

到后面发现还有 SpringCloud ,微服务的时代来临了,我想不能再这般“犹抱琵琶半遮面”地使用 Spring 全家桶了。

一时间就钻入各种 SpringCloud 细节源码中,希望能领悟框架真谛,最终无功而返且黯然伤神,再次感叹 *** 这玩意真复杂。

其间我已经意识到了是对 Spring 基础框架的不熟悉,导致很多封装点都不理解。

毕竟 SpringCloud 是基于 SpringBoot,而 SpringBoot 是基于 Spring。

于是乎我又回头重学 Spring,不再一来就是扎入各种细节中,我换了个策略,先从高纬角度总览 Spring ,理解核心原理后再攻克各种分支脉路。

于是我,我变强了。

其实学任何东西都是一样,先要总览全貌再深入其中,等回过头之后再进行总结。

这篇我打算用自己的理解来阐述下 Spring 的核心(思想),碍于个人表达能力可能有不对或啰嗦的地方,还请担待,如有错误恳请指出。

抛开IOC、DI去想为什么要有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;
    }
}

看起来好像解决问题了,其实不然。

当项目比较小时,例如大学的大作业,上面这个操作其实问题不大,但是一到企业级应用上来说就复杂了。

因为涉及的逻辑多,封装的服务类也多,之间的依赖也复杂,代码中可能要有ServiceB1ServiceB2...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 里关于数据库的配置:

可以看到我们的图纸写的很清楚,创建 mybatisMapperScannerConfigurer需要告诉它两个属性的值,比如第一个是sqlSessionFactoryBeanName,值是 sqlSessionFactory

sqlSessionFactory又依赖 dataSource,而 dataSource 又需要配置好 driverClassNameurl 等等。

所以,其实我们心里很清楚一个产品(Bean)要创建的话具体需要什么东西,只过不这个创建过程由 Spring 代劳了,我们只需要清楚的告诉它即可。

因此,不是说用了 Spring 我们不再关心 ServiceA 具体依赖怎样的 ServiceBServiceB具体是如何创建成功的,而是说这些对象组装的过程由 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 客户端的选择,默认会导入 lettucejedis 两个客户端配置

基于配置的先后顺序会优先导入 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 暴露出很多扩展点供我们定制,如 BeanFactoryPostProcessorBeanPostProcessor,我们只需要实现这个接口就可以进行一些定制化的操作。

Spring 得到 Bean 的信息后会根据反射来创建 Bean 实例,组装 Bean 之间的依赖关系,其中就会穿插进原生的或我们定义的相关PostProcessor来改造Bean,替换一些属性或代理原先的 Bean 逻辑。

最终创建完所有配置要求的Bean,将单例的 Bean 存储在 map 中,提供 BeanFactory 供我们获取使用 Bean。

使得我们编码过程无需再关注 Bean 具体是如何创建的,也节省了很多重复性地编码动作,这些都由我们创建的机器——Spring帮我们代劳。

大概就说这么多了,我自己读了几遍也不知道到底有没有把我想表达的东西说明白,其实我本来从源码层面来聊这个核心的,但是怕更难说清。最后关于 Spring 的 IOC和 DI 概念的面试回答我之前也写过了,可以看下:

我是yes,从一点点到亿点点,我们下篇见~

#春招#
全部评论
多谢大佬的分享 写的好有趣
点赞 回复 分享
发布于 2023-02-22 11:06 浙江
满满的都是精华
点赞 回复 分享
发布于 2023-02-22 11:54 甘肃

相关推荐

10-11 17:45
门头沟学院 Java
走吗:别怕 我以前也是这么认为 虽然一面就挂 但是颇有收获!
点赞 评论 收藏
分享
5 8 评论
分享
牛客网
牛客企业服务