Spring 回顾
本文最后更新于 2025年9月18日 下午
这是对目前所了解到的Spring和SpringBoot知识点的回顾。
IoC
- IoC(控制反转)是一个程序设计概念,目的是为了将一个组件所依赖的其他组件交给外部组件进行管理,而不是交给组件自己进行创建和管理。相当于出现了以下变化:
graph LR
1[A create B and Manage B] --> 2[C create B, if A needs B, A get B from C]
这种变化带来的好处:
- 组件不需要关新依赖组件的创建流程和管理方式,这些东西都不是自己需要考虑的事情,组件本身的变更也不会对自己的核心逻辑产生影响。这减少了代码的耦合度
- 这种方法类似于工厂模式或者抽象工厂模式,这点可以同样可以看出来组件无需在意依赖组件的创建,管理流程。
- 更方便进行统一的对象管理,简化代码开发
例子:
1 | |
DI (Dependency Injection)
依赖注入是Spring实现IoC的方法。Spring会根据给定的设置(Configuration),也许是用注解标记的类或者方法返回值,也许是XML文件,来创建IoC容器。程序可以很轻松地从这个IoC容器中拿到想要的实例对象(Bean,后面都说是Bean)。实际上,IoC容器的底层也就是一个Map,key就是对象唯一标识(名称),value就是对象本身。
由此可以看出,如果想要对同一个类创建多种不同的Bean,要么在配置Bean的时候就给一些不同的命名,要么就不要出现重复命名的Bean设计。
Spring的依赖注入既可以是构造器注入(Constructor),也可以是Setter注入。其实顾名思义了,正常来说也就这两方法可以用于注入了。
AOP
AOP(Aspect Orientation? Oriented? Program),面向切面编程。这也是一个编程的设计理念。它让程序中具有切点和切面以让一些其他的系统或者程序流程在不侵入原业务代码的情况下产生效果。这能够让系统业务更加专注于自己的实现逻辑而不用管系统性业务流程的实现流程,让业务代码具有更高的内聚性。
给个例子,Java的切面应用就是用注解标记一下方法就行了:
1 | |
例子中给出的两个情况都会给出相同的结果,但是显而易见,第二个情况能够让methodA更加专注于自己要干的事情而不用再管log和preProcess和其他任务的实现。这里写一下如何自定义一个AOP
1 | |
@Pointcut(...):这里面的参数有value和argname,但我们常用的是value。其中@execution: 拆解一下里面的每一个内容:- 第一个
*代表的是返回值,用于限制该切点只会在指定返回值的方法下设别 com.test.service.*.*(..):表对应类,包下的方法下的对应参数。这里的写法代表切点在com.test.service包下所有类的所有方法都会存在,不论参数如何。
- 第一个
@annotation:表示当类,方法等内容上标有这个注解时便会被视为切点。
Around环绕通知模式下可以自由地控对原方法进行控制,当然也可以操作方法的返回值。如果要这样做的话,需要给该通知定义一个返回类型。
AOP的实现方式
Spring中,AOP的实现方式有两种:
- JDK Proxy:仅当代理对象是一个或多个接口的实现类时会采用,这是因为JDK Proxy刚需interface接口参数。
- 在创建代理对象的时候会实现传入的接口,并以此作为参照生成代理对象。
- CGLIB:更通用,是如今Spring进行AOP实现的默认方法。如果想要使用JDK Proxy,可以去改配置,但是一般不改。
- 直接根据原对象创建一个新的代理对象。
Bean
在Spring出现之前,最常见的理论的应用是JavaBean,与Spring Bean还是有不少区别的。
JavaBean
JavaBean是一种类的范式,当一个类满足以下条件时,可以被视为是一个JavaBean:
- 字段均为私有
- 具有一个无参的构造方法
- 拥有
getter/setter,一般来说,拥有getter/setter方法的字段可以被视为一种属性,一个只有getter的属性被称为只读属性,一个只有setter的属性被称为只写属性,但很少见且很少用。
JavaBean主要是为了规范一个类的创建,以能够让系统框架对这种类进行更方便的序列化,自动注入和非侵入移植操作。我们可以发现,在使用Swing这类Java的图形化技术时,在IDE中通常会看到有JavaBean的出现,这也是其优势之一,因为JavaBean有着更方便的自动序列化和注入行为。
Spring Bean
Spring Bean的涵盖范围则比JavaBean要大得多,但是在某些情况下,我们依然可以将JavaBean视为是一种Spring Bean,因为它们在应用层面上有着很多相似之处。
Spring Bean的定义为:由Spring IoC容器自动生成,配置,管理的类实例或对象。这类对象需要用Spring Bean配置显式定义。配置方式由如下几种:
- 基于注解的Bean配置和声明:注解有很多
@Component:加在声明类上,用于声明该类被视为一个Spring Bean@Bean:与@Configuration同用。加在声明方法上,用于声明该方法的返回值是一个Spring Bean
- 基于XML文件的Bean配置和声明:
- 使用XML文件进行配置
不论是用注解还是用XML文件,都需要在程序中进行读取,扫描,以得到一个IoC容器。在Spring Boot中,据我所知,现在用XML文件进行配置的比较少见,因为使用@SpringBootApplication这个注解和运行基本程序时所需的具体步骤中会自动将使用注解装饰过的类或方法进行扫描,以生成Spring Bean,IoC容器。
接下来讲讲与Spring Bean中共存的,比较重要的几个知识点:
@Component 和 @Bean 的区别
用代码表示一遍:
1 | |
不难看出,两者都可以用于创建Spring Bean。
@Component用于修饰类,当Spring程序在扫描到一个类上有该注解的时候,会根据注解的定义域,自动创建一个该类的实例(不一定什么时候都会创建哈,也许是使用时创建,这里只是说没有给任何参数的情况下的默认情况)。
@Bean用于修饰方法,该方法必须在一个SpringBean中才能生效,因为Spring只会对是SpringBean的对象(类)进行自动操作,如果该类没有被标记为一个Spring Bean,则内部方法也不会被自动扫描。
很多人都说@Bean的类必须使用@Configuration注解,但实际上@Configuration注解也是使用@Component注解修饰的一个接口,并且Spring是否对一个类中的内容进行操作主要是基于该类是否是一个Spring Bean,所以理论上来说,你往上面加的是@Component,或者任何其他继承了@Component(Spring中用@Component注解修饰且公认是一个用于声明Spring Bean)的注解,应该都能用。
事实上,我后面自己试了一下把@Configuration改成@Component,结果告诉我确实没什么区别,但是@Service一类的我就没试了。
Spring Bean的作用域
这里说的作用域就是表示一个Spring Bean的创建,销毁和引用性质。通常来说,有六种不同的作用域,总的来说可以分为两类:与web相关,和与web无关。常用的就是那两个与web无关的作用域。这里先介绍这两个:
Singleton:要是会些英语或者学过Java模式的朋友们应该就知道这个词的意思。这个词表示单例,很容易让我们想到,这个定义域的Bean只会被Spring创建一次并且受到持续维护,往后在其他程序对其进行引用时,拿到的也只是最开始创建的那个单例实例。因此,它有如下特性:- 单例性质
- 理论上的线程不安全
Prototype:中文意思是原型,从词面意思上来说可以理解成为类对象生成了一个原型,在往后要对该对象进行引用的时候,则会根据这个原型来创建一个新的对象。因此,该定义域下的Bean并不是单例的,每一个地方使用的对象都是独立的。
1 | |
此时假如你使用Web对singleton和prototype中的num各自进行操作,你会发现,调用任意一个singletonUser的addNum之后,都会使另一个singletonUser增加。实际上就是因为两个实例中的singletonBean指向的是同一个对象,所以操作的也是同一个对象。而prototypeUser的addNum则不会对另一个prototypeUser产生影响,因为他们的@Autowire会给他们创建一个新的PrototypeUser实例对象,相互独立,没有冲突。
Bean的生命周期
我原本对这个概念是嗤之以鼻的,想着为什么我需要记这个东西?人家都给我创建好了,我用不就行了?知道有一天我遇到了一个问题,分享给大家看看。
1 | |
假设现在有一个Controller,分别用于读取user和user2这两个Bean中的name属性。大家想一下,user的name是多少,user2的name是多少?
诚然,这个案例只是我在尝试创建另一个user2 Bean,其name属性不同于正常user Bean中的Harry Potter,而是Hermine。但是输出的内容告诉我,user 和 user2中的name都是Harry Potter,这是为什么?
这便是基于Spring创建Bean时的流程而导致的现象。接下来,我讲一下Spring Bean的声明周期:
-
实例化:创建一个Bean的实例,其可以给予XML,注解等来决定创建哪些Bean实例。默认通过反射调用无参构造方法。
-
依赖注入,属性赋值:根据配置(
@Autowired,@Value)注入Bean的每个字段,如果字段没有指定配置并且类中也没有定义值,则值为None。如果有声明依赖注入类但没有找到指定类Bean,则会报错(如:用@Autowired注解修饰了的字段没有找到对应Bean) -
初始化Bean:初始化Bean会涉及到很多内容:
-
Aware 接口回调(让 Bean 感知容器环境)
BeanNameAware→ 注入当前 Bean 的名字BeanClassLoaderAware→ 注入类加载器BeanFactoryAware→ 注入 BeanFactoryApplicationContextAware→ 注入 ApplicationContext
BeanPostProcessor 前置处理
- 执行容器中所有
BeanPostProcessor.postProcessBeforeInitialization()
初始化方法调用
InitializingBean.afterPropertiesSet()- 自定义
init-method或@PostConstruct
BeanPostProcessor 后置处理
- 执行容器中所有
BeanPostProcessor.postProcessAfterInitialization() - 常见场景:生成 AOP 代理对象
-
-
使用阶段:
- Bean实例已准备好,随之等待被程序使用。此时Bean处于被IoC容器托管的状态,只要IoC容器还在(通常是程序没挂时),这个Bean都会存在。
-
销毁阶段:
- 程序关闭(认为操作关闭,挂了等情况),IoC容器被关闭时,Bean实例会被自动销毁,释放内存。此时会调用
DisposableBean.destroy()方法。 - 如果修饰了
@PreDestroy或有destroy-method,则会先执行这些方法。
- 程序关闭(认为操作关闭,挂了等情况),IoC容器被关闭时,Bean实例会被自动销毁,释放内存。此时会调用
从步骤1和2中可以看到,Bean会先创建实例,然后进行依赖注入。而在例子中,由于@Configuration中的user2的可视流程是步骤1中执行的操作。而用@Value将配置注入到属性中时步骤2的操作。因此,Hermine会在步骤2时被HarryPotter覆盖掉。
Transaction
Transaction,事务。所谓事务,就是一连串的操作。
事务这一概念,最基本的是ACID四大特征:
- **A(Atomic)原子性:**一个事务是从一而终的,不可分割的,要么内部操作都执行,要么就都不执行。
- C(Consistency)一致性: 一个事务的执行会将一个具有一致性的数据转移到另一个具有一致性的数据状态。比如转账的情况下,A有200,B有0,A转100给B,那么A有100,B有100。转账前后,两人的金额总值不变。
- **I(Isolation)隔离性:**事务与事务之间是隔离的,独立的。一个事务的操作不会对另一个事务产生影响。(一个事务的中间状态不会对其他事务产生影响)
- **D(Duration)持久性:**事务执行的结果是有持久性的,不会因为数据库的崩溃或其他什么问题而导致事务操作带来的结果丢失。
在我们需要学习的数据库中,不论是什么数据库,都需要学习这个概念,比如说,MySQL便对完成事务系统做了很多:
- **保证原子性:**MySQL中具有undo log,用于记录事务中的操作流程,以便在最后决定是否都执行,或者是否回滚,以确保原子性。
- 一致性与原子性通常是共存的,保证了原子性,很难不保证一致性,除非在分布式事务中。(这是我的主观意见)
- **隔离性:**不论在那种数据库或者关于数据库的框架中都会遇到如下事务的隔离性问题:
- **脏读:**事务B更改了该数据 -> 事务A读取了该数据 -> 事务B因错误发生回滚 此时事务A便持有一个脏数据,因为它持有的是一个预期之外的数据。
- **幻读:**事务A对一组数据进行了统计操作 -> 事务B更改了这组数据(也许删除了其中一条数据) 此时事务A便正在进行幻读,因为A认为的数据组与实际不符,产生出来的效果似乎像幻影一样莫名其妙地出现。
- **不可重复度:**事务A读取了一个数据 -> 事务B更改了该数据(更改后发生回滚,或正常更新了该数据) -> 事务A重新读取了该数据用于做另外的计算或检查。此时由于事务A前后对同一个数据进行了多次读取且结果不同,这也许会造成两次的统计结果(或其他与该数据有关的操作)产生不一致的错误,最终导致输出的结果不符合预期而造成严重问题。
- **持久性:**MySQL中具有redo log(Innodb和mysql两步提交),确保一个事务的完成在最终一定会对数据库产生持久化影响,或者一个未完成或有问题的事务在最后不会对数据库产生影响。
Spring中,我们主要需要考虑的是隔离性和事务传播的问题
隔离性
之前提到了脏读,幻读,不可重复度三种问题。Spring提供了多种隔离等级来确保不同程度的隔离性,用户可以根据需要决定用哪个(这些隔离等级在大多数数据库系统中都是同一个概念):
READ_UNCOMMIT:未提交读,意思是对一个数据的读取允许在另一个事务正在对该数据进行改动且未提交状态时进行。显而易见,这会造成脏读,幻读,和不可重复读问题,必将你读取的内容可能在你的这一个事物周期中发生改变。READ_COMMIT:提交读,意思是仅可以读取已提交的数据。这能够保证事务在读取数据的时候,数据的版本是最新的(不管是否有正在进行修改且没有提交的其他事务),因此可以保证避免脏读问题。但幻读和不可重复读问题依然无法解决。- 不可重复度问题:假设A读取了数据a -> B修改了数据a且提交 -> A再读取数据a。A先后两次读取的数据结果不同,造成了不可重复读问题。
- 幻读问题:与不可重复度问题相同。
REPEATABLE_READ:可重复度,意思是破解了不可重复读的问题,通过保证同一事务中多次对统一数据读取的数据结果相同这一方法,确保了消除不可重复读的问题。- 幻读问题:假设A读取了一系列数据[a1, an] -> B插入了一条新数据ak在[a1,an]中 -> A再次读取这一系列数据[a1,an]。虽然由于可重复度的特性,原本[a1,an]中的值没有发生改变,但是由于新加入的ak,导致最终多了一条数据而发生幻读。
- Mysql中的Innodb在可重复度中通过MVCC和next-key lock解决了幻读问题,但是一般定义中,依然会把幻读问题视作可重复度机制中存在的问题。
SERIALIZABLE:串行,意思是事务之间的执行是串行的,一个事务的执行需要等当前正在执行的事务执行完毕且提交之后才被允许。这可以解决所有问题,但是明显对性能极度不友好。
事务传播
这一概念指的是一个事务中调用了另一个标记了事务操作的方法时的操作逻辑。Spring主要提供了以下几个方案:
REQUIRED:内部事务创建时会加入外部事务组成一个更大的事务。不论是内部事务还是外部事务因错误发生的任何回滚都会让所有牵扯到的事务发生回滚。REQUIRED_NEW:内部事务创建时会被视为一个独立的事务运行。内部事务发生错误导致回滚不会影响到外部事务,除非内部事务的错误没有被它自己处理,被外部事务感知到了。外部事物发生错误导致的回滚也不会影响到内部事务。NESTED:内部事务创建时会被视为外部事务的一个嵌套事务。此时,内部事务因错误导致回滚不会影响到外部事物,同样的,除非内部事务的错误没有被它自己处理,被外部事务感知到了。但是,外部事务发生错误导致的回滚会同样牵连到内部事务,都会发生回滚。
还有一个用的很少的MANDATORY,用于非常特殊的时期,这类事务一定会加入它所属的外部事务,如果没有外部事物,则程序报错。
对于事物传播带来的事务回滚问题有着极为广泛的争论与交流。我认为有一个很好理解的方法判断回滚逻辑:算一下一共有多少事务和事务的隶属关系即可。
- 事务回滚都是由于事务中抛出了错误且没有得到解决才发生的,如果两个事物相互独立且没有任何隶属关系,则不论如何都不会影响到彼此。
REQUIRED中,事务会发生全部回滚是因为这些事务被整合成了一个单一事务,因此牵一发而动全身。REQUIRED和REQUIRED_NEW和用的情况,比如A是Required,B是Required_new,外部事务为Required。那么,B的错误回滚不会影响到A和外部事务,外部事务和A任意一个的错误都会导致外部事务和A共同发生回滚且不影响B。NESTED则是在此之上有加一层隶属关系,父事务的错误会影响其所有子事务,而子事务的错误与父事务无关。
Spring声明事务的方法
通常来说,在主入口类上修饰一个@EnableTransactionManagement注解后,在方法,类,接口上给一个@Transactional注解都可以实现事务声明。
- 方法上加就是表示这个方法被事务管理。
- 类上加就是表示这个类中的所有方法都会被事务管理。
- 接口上加就是表示所有实现了这个接口中方法的方法都会被事务管理(不论是实现了相关继承接口的类,还是直接实现了接口的类)。
@Transactional()参数:
propagation:传播策略isolation:隔离等级timeout:超时时长,定义该时长后,如果事务没有在规定时间内完成,则自动回滚readOnly:只读,事务标记为只读后,程序会认为该事务中的所有操作都是读操作,程序会给一些优化方案rollbackFor / noRollbackFor:回滚控制,事务回滚一般是在出现了运行时错误异常或Error错误时进行回滚,预期外的错误异常不会发生回滚。该参数可以让用户自行决定事务在什么情况(遇到什么错误,异常)时触发回滚。