人人都能看懂的Spring原理,看完绝对不会懵逼

news2024/11/15 18:05:23

人人都能看懂的Spring原理,绝对不会懵逼

  • 为什么要使用Spring?
  • Spring的核心组件
  • Spring是如何实现IOC和DI的?
    • 定义了BeanDefinition
    • 扫描加载BeanDefinition
    • 根据BeanDefinition进行Bean的实例化和初始化
      • 实例化
      • 属性赋值
      • 保存到单例缓冲池
  • 一个Bean从创建到销毁都会经历哪些步骤?
  • Spring怎么完成依赖注入?
    • byName
    • byType
    • @Autowired
  • 循环依赖怎么解决?
    • 什么是循环依赖?
    • 三级缓存解决循环依赖
  • Spring怎么实现AOP的?
    • AOP是什么?为什么会有AOP?
    • AOP的核心组件
    • AOP处理
    • Spring中的AOP实现
  • Spring怎么实现事务的?
  • Spring提供了哪些扩展点?

为什么要使用Spring?

其实回答这个问题,可以反过来思考,能不能不使用Spring,不使用Spring会怎么样?

如果只是写些小Demo代码,或者开发一个小工具,那真的没必要使用Spring。

但如果是公司级的项目或系统,里面的代码量是非常庞大的,工程里面会有很多的类很多的模块系统的运行会创建很多对象,而对象与对象之间又有错综复杂的依赖关系。在没有Spring的管理下,这些对象的创建,依赖关系的维护,都需要通过硬编码进行管理,那么久而久之代码就会变得非常的难以维护,类与类之间,模块与模块之间的耦合度会非常的高,一个小改动,就有可能影响很多地方。

其实不使用Spring也不是不可以,很多开源框架也没有依赖Spring进行开发,但是它们也在进行版本的迭代和扩展,那是因为它们使用了各种设计模式优化了它们代码的设计,使其变得更好维护和扩展,或者可以说是不得不使用设计模式去优化,因为不这么做的话,它们的项目也会慢慢的变得无法再继续维护和扩展

而我们基本上都是做业务开发的,都是以实现功能为主,功能可用基本上就不会再想着去优化,所以如果不使用Spring,那么10个项目可能就是10座祖传屎山。
在这里插入图片描述
其实问题主要就是以下两点:

  1. 所有需要用到的对象,都需要手动创建,这些都是硬编码
  2. 对象与对象间的依赖关系,需要人为管理,对象的属性需要手动赋值,一个对象里面依赖到另外10个对象,那连带的要创建这10个对象

使用Spring,就是为了降低类与类之间,模块与模块之间的耦合度,使得代码更易于维护。

Spring的核心功能,就是管理对象的生命周期,特别是对象的创建,以及对象依赖关系的管理,也就是所谓的IOC(控制反转)和DI(依赖注入)。而其他的功能(例如AOP、事务)都相当于是扩展功能。
在这里插入图片描述

Spring的核心组件

先看看Spring有哪些比较核心的组件,没有列全,只列了一些主要的,混个眼熟。

在这里插入图片描述
其中DefaultListableBeanFactory是Bean工厂的最终实现类,最终使用到的Bean工厂就是它,而它同时又实现了BeanDefinitionRegistry(bean定义注册表),所以它也是一个bean定义注册表。

Spring是如何实现IOC和DI的?

定义了BeanDefinition

首先,Spring把我们交给他管理的对象,称为Bean,然后又定义了BeanDefinition(Bean定义)去描述这些Bean

那为什么要弄一个BeanDefinition呢?

BeanDefinition相当于是对Class类元数据信息的一种扩展
因为Spring不是简单的帮我们创建一个对象就完事,他需要知道如何创建这个对象如何给它的属性赋值注入哪些值有哪些生命周期回调函数等等,单凭一个Class对象,不足以描述一个Bean的所有属性。
因此,Spring把Bean的作用域,是否懒加载,属性依赖信息,自动装配模型等描述信息,封装到了BeanDefinition中。
有了BeanDefinition之后,Spring就可以根据BeanDefinition去实例化和初始化Bean
在这里插入图片描述

扫描加载BeanDefinition

先有BeanDefinition,再有bean。

接下来Spring就要扫描我们的配置文件或注解,加载BeanDefinition,注册到Spring容器里面,正确来说应该是放到一个Map中,这个Map的名字叫beanDefinitionMap,key就是beanName,value就是BeanDefinition。

而这个工作是通过BeanDefinitionReader完成的。
如果是XML的配置方式,则通过XmlBeanDefinitionReader完成BeanDefinition的加载和注册,
如果是注解的配置方式,则通过AnnotatedBeanDefinitionReader完成BeanDefinition的加载和注册。

BeanDefinition最后会被注册到DefaultListableBeanFactory(也就是我们常说的的Bean工厂)的beanDefinitionMap中,DefaultListableBeanFactory实现了BeanDefinitionRegistry接口,所以拥有了BeanDefinition注册表的功能。
在这里插入图片描述

根据BeanDefinition进行Bean的实例化和初始化

有了BeanDefinition,就可以根据BeanDefinition进行Bean的实例化和初始化了。

实例化

首先是根据BeanDefinition进行Bean的实例化,实例化是通过反射完成的,会通过反射获取Constructor构造器对象,然后调用newInstance方法进行实例化。

但是一个类中可能有多个构造函数,具体要用哪个呢?
这就要看BeanDefinition中关于构造器参数的描述信息constructorArgumentValues,获取最匹配的构造器。这个步骤叫做推断构造
如果没有指定构造器参数,则使用的是默认的构造器。
在这里插入图片描述

属性赋值

Bean实例化之后,就要对Bean进行属性赋值,也就是依赖注入

Spring会根据BeanDefinition中记录的autowireMode自动注入模型,使用对应的策略进行依赖注入。

不同的自动注入模型,对应使用不同的策略为该Bean的属性找到匹配的值或对象:

  • byName:BeanFactory#getBean(属性名)
  • byType:根据类型从Spring容器中获取beanName,再通过BeanFactory#getBean(beanName)获取bean,这里如果获取到多个相同类型的bean,还要推断出一个最匹配的
  • constructor:构造器注入,不在这里处理,在实例化的时候已经处理了
  • no:不使用自动注入,使用我们手动指定的值(也就是配置文件指定要注入的属性值),手动指定的值保存在BeanDefinition的propertyValues中

为该Bean的属性找到匹配的值后,并不是马上进行属性赋值,而是收集到一个PropertyValues中,后面再根据PropertyValues为该Bean的所有属性进行赋值。PropertyValues相当于是一个对bean的不同属性要赋哪些值的描述对象。
BeanDefinition中的propertyValues也是一个PropertyValues对象,保存的是我们手动指定的属性值,这里会合并为一个PropertyValues对象。

属性赋值当然也是通过反射进行,通过setXXX方法的方法对象Method的 Method#invoke(…) 方法进行属性赋值。
在这里插入图片描述

保存到单例缓冲池

初始化完成后的Bean,就会放入到单例缓存池中。

这个单例缓存池的名字叫singletonObjects,也是一个Map,key是beanName,value是对应的Bean,当我们需要某个Bean时,就从该缓存池中返回。
在这里插入图片描述

一个Bean从创建到销毁都会经历哪些步骤?

上面这个流程是比较粗糙的,一个Bean在Spring中是有一个完整的生命周期的,也就是在实例化、初始化、初始化完成、再到销毁,有哪些步骤,要干哪些事情。

在这里插入图片描述

以上就是bean的生命周期的详细过程。

xxxAware接口是用于获取Spring自身的bean的,比如实现了BeanFactoryAware接口,Spring会回调setBeanFactory方法,我们就可以获取到BeanFactory,通过实现BeanNameAware接口获取beanName也是同理。

BeanPostProcessor是Spring提供的扩展点,该接口有两个方法postProcessBeforeInitializationpostProcessAfterInitialization(也就是before方法和after方法),会在bean初始化的时候被回调。用于在执行beab的初始化方法的前后,对该bean进行一些特殊处理,比如可以动态代理返回一个代理对象,替换掉原来的bean。
我们可以定义自己的BeanPostProcessor实现,然后注册到Spring容器中。
在这里插入图片描述

bean的初始化方法和销毁回调方法,我们应该比较熟悉。我们可以通过接口的方式xml注解,配置初始化方法和销毁回调方法。

生命周期阶段接口xml注解
初始化InitializingBean#afterPropertiesSetinit-method@PostConstruct
销毁DisposableBean#destroydestroy-method@PreDestroy

Spring怎么完成依赖注入?

以下是Spring对依赖注入的具体处理逻辑。

byName

byName的依赖注入比较简单,就是以属性名作为beanName,调用BeanFactory#getBean(属性名),从Spring容器中获取对应的bean,然后通过反射进行注入。
在这里插入图片描述

byType

byType稍微复杂一点。
byType使用到了DefaultListableBeanFactory的resolveDependency方法,这个方法顾名思义就是解决依赖关系

byType的处理,首先会根据属性名获取对应的setXXX方法,然后根据set方法的参数,生成一个DependencyDescriptor依赖描述符,DependencyDescriptor是对set方法的描述。这个描述符对象,包含了set方法所在类的Class,set方法名称,参数类型,set方法对应的属性名等。
在这里插入图片描述
然后,DefaultListableBeanFactory#resolveDependency有两步处理:

  1. 回调AutowireCandidateResolver的getSuggestedValue方法,根据DependencyDescriptor依赖描述符获取对应的值。
  2. 如果AutowireCandidateResolver的getSuggestedValue方法没有返回有效的值。则通过bean的类型获取所有符合该类型的beanName,然后获取生成一个Map,key是beanName,value是beanName对应的bean,然后推断哪个bean更符合,则使用哪个bean注入。

在这里插入图片描述
这里出现了一个比较陌生的东西-AutowireCandidateResolver(自动装配候选解析器),其实它就是Spring定义的一个接口,里面有个getSuggestedValue方法。我们可以通过自定义一个AutowireCandidateResolver然后实现getSuggestedValue方法,实现自定义的依赖注入,例如为某些特定类型的属性,注入我们自己给定的值或者对象。比如Spring自己就实现了一个QualifierAnnotationAutowireCandidateResolver类型的AutowireCandidateResolver,getSuggestedValue方法处理@value注解修饰的属性的依赖注入,返回@Value注解上给定的值或者@Value指定的配置项对应的值。
在这里插入图片描述
如果AutowireCandidateResolver的getSuggestedValue方法返回了null,那么就要走接下来的步骤:通过bean类型获取所有类型匹配的beanName,进而获取所有类型匹配的bean,然后推断一个最合适的。

那如何推断最符合的bean呢?

在这里插入图片描述
其实就是四步:

  1. 如果被 @Primary 注解修饰的,优先使用,但是发现有多个被@Primary修饰的,则报错
  2. 如果没有@Primary注解修饰的,则看有没有被 @Priority ,如果有,则使用,发现多个被@Priority注解修饰的,则报错
  3. 如果也没有,则寻找beanName和属性名匹配的bean
  4. 如何还没有,如果该属性是必须要注入的,则报错,否则返回null
    在这里插入图片描述

@Autowired

@Autowired注解修饰的属性的处理,比较特别。
Spring自定义了一个BeanPostProcessor实现类AutowiredAnnotationBeanPostProcessor,再进行依赖注入时会进行回调,扫描被被@Autowired注解修饰的属性,然后最终会调用DefaultListableBeanFactory的resolveDependency方法,所以最后的处理逻辑是跟byType一致的。
在这里插入图片描述
@Autowired如果在字段上,则通过Field#set(…)进行反射注入
@Autowired如果在方法上,则通过set方法的Method对象的Method.invoke(…)方法进行反射注入

循环依赖怎么解决?

什么是循环依赖?

就是你中有我,我中有你~
在这里插入图片描述
这种情况如果不做特殊处理,就会造成死循环,也就是初始化A时,发现依赖了B,就去初始化B,而初始化B时,发现依赖了A,又去初始化A…

那Spring如何解决循环依赖呢?答案就是三级环境。

三级缓存解决循环依赖

第一级缓存就是单例缓存池singletonObjects,这个是在bean初始化完成后才放入进去的,而依赖注入是在bean初始化的时候进行的,显然不能解决循环依赖。
所以应该多加一个二级缓存,在bean实例化后,初始化前,提前放入到二级缓存中,这叫提前暴露。然后当进行依赖注入时,发现属性所依赖的bean又依赖到自己,那就直接从缓存中拿
在这里插入图片描述

那看来搞两级缓存就可以了,为什么要搞三级呢?
那是因为Spring还要处理AOP处理,假如一个bean后面要进行AOP处理,返回一个代理对象,那这里提前暴露的又是原对象,就会出现注入的类型和最后放入到单例缓存池的类型不一致
在这里插入图片描述
所以再多搞一级缓存,也就是第三级缓存
第三级缓存存放什么呢?存放的是把bean包装一层后的ObjectFactory对象ObjectFactory#getObject()返回里面的bean。为什么要包装一层呢?因为不知道这个bean需不需要进行AOP,如果不需要,则返回原来的bean,如果需要,那么就要提前进行AOP处理,返回代理对象,这样依赖的bean和最后放入单例池的都是同一个对象(代理对象)。
在这里插入图片描述
然后依赖注入时获取依赖的bean就变成这样的流程:
在这里插入图片描述

三级缓存中返回的对象,放入二级缓存,然后从二级缓存删除。
实例化完成后,从二级缓存删除,放入一级缓存。
在这里插入图片描述
到这里,Spring最核心的原理基本上就差不多了,如果这些都能弄懂的话,可以说是对Spring有一个比较深入的理解。

当然Spring还有一些扩展的东西,比如AOP,事务,还有其他的扩展点。

Spring怎么实现AOP的?

AOP是什么?为什么会有AOP?

AOP的意思就是面向切面编程,所谓切面,可以简单理解为横跨了多个方法,就像一个切面一样,在各方法前(或者后)都要执行的代码。

比如处理请求前打印日志,进行权限校验等,这些代码在每个请求方法前都要执行,但是不可能每个方法都重复写一遍,所以通过AOP,就可以把这些每个地方都要执行的代码,提取到切面中去

AOP的核心组件

先看一下AOP的核心组件
在这里插入图片描述
有了解过AOP的应该对这些都很熟悉。

AOP处理

AOP处理要做的事情,就是在被Pointcut命中的方法前后(具体是前是后,要看Advice类型),织入Advice定义的增强逻辑,织入的方式就是创建一个代理对象在执行方法前(或者后),执行增强逻辑
在这里插入图片描述
经过代理增强之后,目标方法就会被包裹起来,根据不同的Advice(通知)类型,在它的前前后后,会有不同的增强逻辑:
在这里插入图片描述

Spring中的AOP实现

Spring首先实现了一个BeanPostProcessor(bean后置处理器),AnnotationAwareAspectJAutoProxyCreator,它的祖先类AbstractAutoProxyCreator在after方法定义了AOP的入口,在after方法中检查是否要对bean进行AOP处理,如果需要,则进行AOP处理。
在这里插入图片描述
检查一个bean是否需要进行AOP处理,其实就是拿到所有我们定义的Pointcut,看bean里面的方法有没有被Pointcut切中的,有的话,就要进行AOP处理。
Pointcut是通过MethodMatcher方法匹配器的matches方法,与方法进行匹配,匹配结果返回true或者false。

如果需要进行AOP处理,则要进行动态代理,生成代理对象。
代理方式有JDK动态代理和CGLib两种,如果bean实现了接口,则使用JDK动态代理,否则使用CGLib
生成的代理对象里,会保存一个Advisor[] 增强器数组,在方法调用时会触发Advisor里的Advice增强逻辑的调用。
在这里插入图片描述
在进行方法调用时,就会从Advisor中筛选出需要执行的Advisor(一个类中有多个方法,可能被不同的Pointcut切中,就要进行不同的增强逻辑),这个筛选也是通过Pointcut(一个Advisor会有一个Pointcut与之对应) 进行匹配。从筛选出的各个Advisor中取出Advice,与目标方法一起,封装为一个执行器链。然后就是责任链模式的链式调用
在这里插入图片描述

Spring怎么实现事务的?

Spring是通过AOP实现事务的,也就是通过AOP处理,把被@Transactional注解修饰的目标方法包裹在try-catch块里面,在进入try-catch之前,先开启事务,然后在try块中执行目标方法,如果目标方法报错,则在catch块中回滚事务,如果没有报错,则在try-catch块之后提交事务。
在这里插入图片描述

Spring提供了哪些扩展点?

这里就简单罗列一下Spring的扩展点、扩展点的用途、以及例子,具体细节就不展开说明了。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/97228.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java开发】 Staffjoy 01 :项目目标及案例需求

Staffjoy 是 Spring Boot & Kubernetes 云原生微服务实践,是一个贴近生产的微服务云原生教学案例,本文依波波老师的课程。新开了一个坑,希望能搞懂微服务框架,感兴趣的同学也欢迎讨论~ 目录 1 Staffjoy 项目目标 2 Staffjoy…

【Java寒假打卡】Java基础-StringBuilder类

【Java寒假打卡】Java基础-StringBuilder类一、概述二、构造方法三、常用方法四、StringBuilder提高效率的原理五、StringBuilder实现字符串反转一、概述 也就是使用String 拼接字符串每一次都要开辟新的堆内存空间,使用StringBuilder不需要开辟新的内存空间 String…

软件架构设计的七大原则

学习设计原则是学习设计模式的基础。千万不能形成强迫症。当碰到业务复杂的场景时,需要随机应变。 在实际开发过程中,并不是一定要求所有代码都遵循设计原则,而是要在适当的场景遵循设计原则,就可以帮助开发者设计出更加优雅的代…

glibc内存管理那些事儿

Linux内存空间简介 32位Linux平台下进程虚拟地址空间分布如下图: 进程虚拟地址空间分布 图中,0xC0000000开始的最高1G空间是内核地址空间,剩下3G空间是用户态空间。用户态空间从上到下依次为stack栈(向下增长)、mmap(匿名文件映射区)、Heap堆(向上增长…

网络编程之IO多路复用

目录 一. 同步与阻塞 1.1 同步阻塞 1.2 同步非阻塞 1.3 异步阻塞 1.4 异步非阻塞 1.5 I/O多路 二.多路复用的技术 2.1 UNIX I/O Models 2.1.1 blocking I/O 2.1.2 nonblocking I/O 2.1.3 I/O Multiplexing Model 2.1.4 SIGIO 2.1.5 asynchronous I/O 2.2 IO多路复…

【拓扑排序】课程表问题

一、拓扑排序问题描述 给定:一系列任务 (A,B,C …) 任务间的依赖关系 (B 和 C 必须在 A 之前完成, …) 输出:这些任务间的合法执行顺序 (C – B – A – …) 总之&am…

「图文讲解」浏览器原理与页面渲染过程

相信大家在面试的时候面试官总是会问一个问题:“可以说下浏览器从输入url到渲染页面完成过程吗?”,非官方标准答案来啦~ 那我们首先需要先来讲一下浏览器是什么,浏览器其实就是一个应用软件,在学习操作系统…

《数据结构、算法与应用C++语言描述》线性表-数组描述

《数据结构、算法与应用C语言描述》线性表-数组描述 5表示在 数据结构算法与应用C语言描述 书中所在章节。 本文包含了《数据结构、算法与应用C语言描述》第五章主要练习题答案,给出了线性表数组描述完整测试代码。 5.1 数据对象 5.1.1 定义 数据对象(data obj…

java开发的师生评教小程序学生对老师评价老师对班级评价打分题单选题意见框系统选课系统

简介 源码1.0(源码2.0选课功能,请往下看) 师生评教小程序,学生可以对老师进行评价,老师可以对班级行进评级。管理员可以创建不同的评教模板(单选题0分或者10分,打分题0-10分,意见框…

三分钟彻底搞懂paint,repaint,update!

最近总结了一下java中的paint,repaint和updata三者之间的关系,首先咱们都知道用paint方法来绘图,用repaint重绘,用update来写双缓冲。但是他们之间是怎么来调用的呢,咱们来分析一下(想直接看结果,请跳过分析…

2022卡塔尔世界杯感想

一、概述 说说我个人吧!在体育活动上面真是没什么兴趣,篮球,足球...等等竞技运动不敏感! 今年稍微关注了点世界杯比赛!什么原因呢?我有一个爱买彩票/赌球的一个同事!随着世界杯的进行&#xf…

风力发电机组机械功率Pm与转子转速Wm关系(Matlab实现)

目录 1 数学模型 2 代码 3 结果 1 数学模型 风力机空气动力学模型 风力涡轮机的动态输出机械扭矩表示为: 其中是空气密度 , A是叶片扫掠面积 , R是风力涡轮机的转子半径 (m), 是风速 (m/s)。是叶片的功率系数,它是叶片桨距…

linux内网渗透:docker逃逸

Docker逃逸 前言 Docker 逃逸在渗透测试中面向的场景大概是这样,渗透拿到shell后,发现主机是docker环境,要进一步渗透,就必须逃逸到“直接宿主机”。甚至还有物理机运行虚拟机,虚拟机运行Docker容器的情况。那就还要…

基于VUE.JS的招聘系统

开发工具(eclipse/idea/vscode等):idea 数据库(sqlite/mysql/sqlserver等):mysql 功能模块(请用文字描述,至少200字):本文是基于Vue.js对招聘系统的设计与实现,对招聘系统管理员、个人用户、企业用户三个模块功能的完善…

kubernetes学习之路--污点容忍度横向主节点

参考:K8s污点容忍度横向主节点-安全客 - 安全资讯平台 一.kube-scheduler调度 kube-scheduler是Kubernetes 集群的默认调度器,并且是集群控制面(master)的一部分。对每一个新创建的Pod或者是未被调度的Pod,kube-scheduler会选择一个最优的N…

[附源码]Nodejs计算机毕业设计基于图书管理系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置: Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术: Express框架 Node.js Vue 等等组成,B/S模式 Vscode管理前后端分…

43_SDIO基础知识

目录 SDIO协议简介 SDIO设备分类 SD卡物理结构 SD卡寄存器列表 SDIO总线拓扑 SDIO总线 SDIO总线协议 SDIO命令 命令格式 命令类型 响应 SD卡的操作模式 卡识别模式 数据传输模式 STM32 SDIO功能框图 命令通道 命令状态机 数据通道 数据状态机 数据FIFO SDI…

自定义java注解案例

今天与大家分享java注解的基本使用,如有哪里有问题,望大家指教。 目录 1. 什么是java注解 2. java注解的分类 2.1 JDK基本注解 2.2 JDK元注解 3. 自定义注解 3.1 自定义注解语法 4. 注解示例 4.1 定义注解 4.2 拦截器 4.3 使用注解 4.4 测试 …

Kali Linux渗透测试演示——DNS欺骗

目录 一、DNS欺骗 1.介绍 2.原理 二、环境和工具 1.kali Linux和靶机(这里选择windows 7) 2.ettercap 三、攻击过程 1.首先确认一下Kali和靶机的IP地址以及网关 2.进入ettercap的配置文件 3.打开ettercap,进行主机发现 总结 一、DNS…

MR案例:计算学生成绩

计算学生成绩一、提出任务二、完成任务(一)准备数据1、在虚拟机上创建文本文件2、上传文件到HDFS指定目录(二)实现步骤1、创建Maven项目2、添加相关依赖3、创建日志属性文件4、创建成绩映射器类5、创建成绩驱动器类6、启动成绩驱动…