(源码链接:https://pan.baidu.com/s/1i-afXQ2tBGded6t8Psea8g 提取码:bh0r)
一、What is the Spring?
Spring框架是由于软件开发的复杂性而创建的,换句话说:Spring是为简化软件开发而存在的。Spring主要的作用可以概况为:简化开发,最大程度的松耦合。
为什么可以松耦合?这个要从下面的模型讲起:
什么是耦合?比如对象A的在实现其功能时需要用到对象B,那就说A依赖B,A和B之间就有耦合关系。耦合又会带来啥?那就是如果修改其中一方,另一方就需要同步修改,如上图Object A、Object B、Object C就是互相耦合的,3个齿轮环环相扣,缺一不可,若其中一个齿轮出现故障或改动,会直接影响其它2个齿轮的运转。耦合度高的项目很不利于自身的更新、扩展、维护、部署,举个不利于部署的例子:如果客户只需要某个很简单的功能,但因为你的项目耦合度太高无法独立划分出来部署,那你就需要部署维护一整套资源,成本就太高了。其实,松耦合是我们一直追求的目标。
Spring实际上就是一种松耦合的方案,它作为第三方,为其他各自独立的对象建立联系。如上图中间的图,Object A、Object B、Object C彼此独立,他们的联系是由第三方Spring维系的,这种模式的好处就是Object A、Object B、Object C无论哪个改变都不会影响其它的两个。下面从代码的角度来说明:
如果不使用Spring,按照原来的模式,service层调用DAO层,需要在service层new一个peopleDAO对象,再去调用该对象的方法,如下:
public class PeopleDAO {
public void add(People p){}
}
public class PeopleService {
public void insert(People p){
PeopleDAO peopleDAO = new PeopleDAO();
peopleDAO.add(p);
}
}
很显然,若这样处理service层和DAO层就有了很强的耦合关系,且service的每个需要调用DAO层对象的方法都需要new一个这个对象,那你将看到service层到处是new DAO层对象的语句。
若使用了Spring,那么DAO层的对象只需要一次注入即可到处使用,这个对象交由Spring管理,如下:
public class PeopleService {
private PeopleDAO peopleDAO;
public void add(People p){
peopleDAO.add(p);
}
}
这里Spring作为第三方实现了解耦:
若未使用spring进行管理,service层在使用DAO层的对象时就是传统的new这个对象,一旦DAO中的这个对象发生改变,比如构造方法等改变,那service中所有使用new这个构造方法的代码都需要同步修改,这样耦合性很大;若使用了spring,则service中要使用的对象是由spring注入的,即使DAO层对象改变,service层的代码不需要修改,就实现了解耦。
二、Spring 架构
Spring框架是一种一站式框架,封装了Web应用所需的所有层面。Spring帮助开发者解决了开发中基础性的问题,使得开发人员可以专注于应用程序的开发。
Spring大约18个基本模块,大致分为4类,分别是核心模块
、AOP
、数据访问
、Web模块
、测试模块
。
- 核心模块包括:
core
、beans
、context
、context-support
、expression
共5个模块; - AOP模块包括:
aop
、aspects
、instrument
共3个模块; - 数据访问模块包括:
jdbc
、tx
、orm
、oxm
共4个模块; - Web模块包括:
web
、webmvc
、websocket
、webflux
共4个模块; - 集成测试模块:
test
模块。
三、Spring中的核心概念
1、IOC
(Inversion of Control),即控制反转。简单的来说,控制反转就是反转了对象的创建方式,由我们自己new创建反转给了Spring。控制的方式有两种:配置文件或注解,本文后面会详细介绍这两种实现IOC的方式。IOC是Spring的核心,贯穿始终。
2、DI
(Dependency Injection),即依赖注入。IOC时被调用对象交由Spring来创建,被调用对象注入调用者中的过程就是DI。在理解上DI不应该和IOC区别的太远,两者可以理解成一个概念,只是IOC偏重于原理,DI偏重于实现,或者说DI是实现IOC的方式。IOC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象,这一点是通过DI(Dependency Injection,依赖注入)来实现的。
所以这两个概念可以理解成是一回事,IOC是目的,DI是实现方式。
3、AOP
(Aspect Oriented Programming),即面向切面编程。听起来很抽象,但不要被这个吓坏,通俗的讲,比如:一个组件A,不关心其他常用的服务组件B,但是这个组件A使用组件B的时候,不是组件A自身调用,而是通过配置等其他方式,比如Spring中通过xml配置文件。这样就使得A压根就不需要知道服务组件B是怎样的。A只关系自己的业务逻辑,具体A使用B的时候,配置文件去做,与具体的A组件无关。AOP就是实现上面所述过程的方式。
实际上Spring主要有两个层面的动态:一是动态创建所需的对象,二是动态为某个类去动态添加一些方法。第一种就是IOC,而第二种就是AOP。AOP的主要作用就是:不修改目标类的前提下,使用动态代理技术,去增强目标类的功能。
例如:你要在类A的某个方法被执行时添加事务的启动与提交功能。若不使用AOP方式,唯一的实现方式就是在这个方法中加入事务功能代码,这样肯定能实现功能,但如果项目中有很多类似的方法都要添加一样的事务功能,你就需要每个方法中都加上类似的事务相关代码,项目中会出现大量重复代码并且增加了开发工作量。
这时你会想,要是像添加事务、添加日志这类频繁出现的功能不要一遍一遍重复的在业务代码里面写就好了,而是某个类的某个方法需要的时候就动态自动添加,这样这个业务类就和事务等实现了解耦,业务类只关心业务。
AOP就是来干这样的事的,可以不用修改这个类,就能为这个类增加额外的功能。AOP专门用于处理系统中分布于各个模块中的交叉关注问题,或者说来处理具有横切性质的系统级服务,如事务管理、安全检查、日志记录、缓存、对象池管理等。利用AOP可以对业务逻辑的各个部分进行隔离,使得业务逻辑各个部分之间的耦合度降低。
本文后面会详细介绍AOP是如何实现这样的过程的,这里只需要知道AOP能干什么就行了。
以上IOC和AOP可以通过xml配置文件实现,也可以通过注解实现。
四、Spring创建bean的过程
Spring中有两种容器对象:ApplicationContext和BeanFactory
BeanFactory已经过时,且ApplicationContext已经覆盖BeanFactory所有接口,因此实际开发时,ApplicationContext来创建bean对象。
下面通过xml配置文件的形式演示下流程:
- 将要创建bean对象的实体类写进.xml文件
- 根据配置创建Spring容器对象,从容器对象中取得需要的bean对象
- 结果
Spring装配Bean有三种方式:
1.在XML中显示配置()
2.在Java的接口和类中实现配置
3.隐式Bean的发现机制和自动装配原则
往往是三种自由配合使用,优先使用隐式Bean的发现机制和自动装配原则,因为基于约定优于配置的原则,第三种可以减少繁重的配置;其次可以选择在Java的接口和类中实现配置,目的也是减少XML配置文件;在无法前面两种时可以选择XML去配置Ioc,比如你要配置的类的bean不是自己工程内的就可以使用XML配置。
4.1 Bean的作用域
1.单例(singleton):默认选项,整个过程中Spring只为其生成一个Bean实例;
2.原型(prototype):每次注入或者获取Bean时,Spring都会为其创建新的Bean实例;
3.会话(session):在Web应用中使用,在会话中只创建一个实例;
4.请求(request):在Web应用中使用,每请求一次就创建一个实例。
五、DI(依赖注入)的方式
Sping的注入方式可以分为三种:
1.set注入
- 基本数据类型
- 引用类型(对象)
- 复杂类型(数组,List,Map,Properties)
2.构造方法注入
- 基本数据类型
- 引用类型(对象)
- 复杂类型(数组,List,Map,Properties)
3.接口注入
在类中,对于依赖的接口,通过具体的实现类名,动态的加载并强制转换成其接口类型。接口注入不常用,只有在有些特定环境下使用,比如数据库链接资源不在项目内,比如配置在Tomcat中时,就可以通过JNDI的形式去获取。接口注入模式因为历史较为悠久,在很多容器中都已经得到应用。但由于其在灵活性、易用性上不如其他两种注入模式,因而在 IOC 的专题世界内并不被看好。不是重点,不需去研究。
注意:构造器注入和setter注入都是通过java的反射技术得以实现的。
1、set注入
set注入是spring中最主流的注入方式, setter注入是通过setter方法注入,首先将构造方法设置为无参的构造方法,然后利用setter注入为其设置新的值,实际就是利用java的反射技术实现的。
1)基本类型注入
ApplicationContext.xml,如
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
name属性: 填写你要注入的字段名称
value属性:你要注入的字段名称对应的值
-->
<bean name="people" class="com.wo.domain.People">
<property name="name" value="熊大"></property>
<property name="age" value="18"></property>
</bean>
</beans>
public class DemoTest {
public void test2() {
// 1、默认从src下加载.xml,根据配置文件创建 容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、从容器对象中取得 需要的 bean对象
People people = (People) context.getBean("people");
System.out.println(people);
}
}
People{age=18, name='熊大'}
注意:set注入要求这个属性set方法要存在不然会。报错,如下把People类中name属性的set方法注释之后,ApplicationContext.xml就会报错:0
public class People {
private int age;
private String name;
private Ball ball;
public String getName() {
return name;
}
// public void setName(String name) {
// this.name = name;
// }
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Ball getBall() {
return ball;
}
public void setBall(Ball ball) {
this.ball = ball;
}
public String toString() {
return "People{" +
"age=" + age +
", name='" + name + '\'' +
", ball=" + ball +
'}';
}
}
2)引用类型注入
总体来说分为两步:
- 需要先将要注入的引用对象使用Spring容器创建出来;
- 再将该引用对象注入进来,使用ref
public class Ball {
private String name;
private int size;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public String toString() {
return "Ball{" + "name='" + name + '\'' + ", size=" + size + '}';
}
}
public class People {
private int age;
private String name;
private Ball ball;
public Ball getBall() {
return ball;
}
public void setBall(Ball ball) {
this.ball = ball;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "People{" + "age=" + age + ", name='" + name + '\'' + '}';
}
}
3)复杂类型注入(数组、List、Map、Properties)
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Feng, Ge 2020/2/19 19:59
*/
public class CollectionDemo {
private Object[] arr;
private List list;
private Map map;
private Properties properties;
public Object[] getArr() {
return arr;
}
public void setArr(Object[] arr) {
this.arr = arr;
}
public List getList() {
return list;
}
public void setList(List list) {
this.list = list;
}
public Map getMap() {
return map;
}
public void setMap(Map map) {
this.map = map;
}
public Properties getProperties() {
return properties;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
public String toString() {
return "CollectionDemo{" +
"arr=" + Arrays.toString(arr) +
", list=" + list +
", map=" + map +
", properties=" + properties +
'}';
}
}
xml文件:
<bean name="collection" class="com.wo.domain.CollectionDemo">
<property name="arr">
<array>
<value>你大爷</value>
<value>你二大爷</value>
</array>
</property>
<property name="list">
<list>
<value>DOTA</value>
<value>war</value>
<ref bean="myBall"></ref>
</list>
</property>
<property name="map">
<map>
<entry key="price" value="9.9"></entry>
<entry key="address" value="地球"></entry>
</map>
</property>
<property name="properties">
<props>
<prop key="id">ksodsodkosdosodko</prop>
</props>
</property>
</bean>
public void test() {
// 1、默认从src下加载.xml,根据配置文件创建 容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、从容器对象中取得 需要的 bean对象
CollectionDemo collectionDemo = (CollectionDemo) context.getBean("collection");
System.out.println(collectionDemo);
}
CollectionDemo{arr=[你大爷, 你二大爷], list=[DOTA, war, Ball{name='FootBall', size=12}], map={price=9.9, address=地球}, properties={id=ksodsodkosdosodko}}
注意:Properties本质就是Map
2、构造方法注入
构造器注入是通过构造方法注入。不需要被注入类实现set方法,只需要有构造方法,下面展示基本类型、引用类型、复杂类型的构造器注入过程:
import java.util.List;
/**
* Feng, Ge 2020/2/19 20:36
*/
public class ConStructorDemo {
private String name;
private List list;
private Ball ball;
public ConStructorDemo(String name, List list, Ball ball) {
this.name = name;
this.list = list;
this.ball = ball;
}
public String toString() {
return "ConStructorDemo{" +
"name='" + name + '\'' +
", list=" + list +
", ball=" + ball +
'}';
}
}
以上并未实现set方法,只有构造方法。
<bean name="construction" class="com.wo.domain.ConStructorDemo">
<constructor-arg index="0" type="java.lang.String" value="构造器注入"></constructor-arg>
<constructor-arg index="1" type="java.util.List">
<list>
<value>1</value>
<value>2</value>
<value>3</value>
</list>
</constructor-arg>
<constructor-arg index="2" type="com.wo.domain.Ball" ref="myBall"></constructor-arg>
</bean>
public void testConstruction() {
// 1、默认从src下加载.xml,根据配置文件创建 容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、从容器对象中取得 需要的 bean对象
ConStructorDemo conStructorDemo = (ConStructorDemo) context.getBean("construction");
System.out.println(conStructorDemo);
}
ConStructorDemo{name='构造器注入', list=[1, 2, 3], ball=Ball{name='FootBall', size=12}}
注意:这里index指定构造方法参数顺序,要和构造方法定义一致。
set注入和构造方法注入的优缺点比较:
- 使用set注入的理由:
- 1.若Bean有很多的依赖,那么构造方法的参数列表会变的很长
- 2.若一个对象有多种构造方法,构造方法会造成代码量增加
- 3.若构造方法中有两个以上的参数类型相同,那么将很难确定参数的用途
- 使用构造方法注入的理由:
- 1.构造方法注入使用强依赖规定,如果不给足够的参数,对象则无法创建
- 2.对于依赖关系无须变化的Bean,构造注入更有用处;因为没有setter方法,所有的依赖关系全部在构造器内设定,因此,不用担心后续代码对依赖关系的破坏。
建议:
这两种依赖注入的方式,并没有绝对的好坏,只是适应的场景有所不同。但Spring官方更推荐使用set注入,即采用以设值注入为主,构造注入为辅的注入策略。对于依赖关系无需变化的注入,尽量采用构造注入;而其他的依赖关系的注入,则考虑采用设值注入。
六、AOP的7个专业术语
前面已经介绍过AOP的概念,AOP的主要作用就是在不修改目标类的基础上,使用动态代理技术,去增强目标类的功能 。
这样直接说可能不能很快的去理解,下面看一个需求场景:
假如现在有一个类,要求你在这个执行这个类里的每个方法的前后都加上事务开启与事务提交。例如下面的类:
/**
* Feng, Ge 2020/2/19 21:32
*/
public class AopDemo {
public void add(){
System.out.println("执行增加!");
}
}
现在要求你在执行add()方法前开启事务,执行add()后提交事务,当然你可以如下面的方式来实现:
public class AopDemo {
public void add(){
System.out.println("开启事务!");
System.out.println("执行增加!");
System.out.println("提交事务!");
}
}
以上虽然可以完成我们的需求,但是仔细考虑就会有问题:(1)现在AopDemo类只有一个add()方法,假如AopDemo类有很多方法或者别的类的很多方法也要实现同样的添加事务的功能,像这样在每个方法里面写"开启事务!"和"提交事务!"的方式会造成大量重复代码,带来很大的工作量;(2)直接在AopDemo中编写代码破坏了AopDemo的独立性,使得这个类与事务相关的类产生耦合,一方改动会影响另一方。
基于以上分析,我们想要是在不修改AopDemo类代码的前提下,就能给AopDemo类增加事务功能就好了,既不影响AopDemo类的业务,使其保持独立,又能添加想要的功能。AOP就是来干这件事的,AOP把上面的场景分成三方:目标类、增强(通知)、代理类
上面场景中的AopDemo类就是目标类(Target),这里事务相关的代码就是增强,代理类由Spring提供。AOP中重要的专业术语如下:
- 目标类(Target):增强逻辑的织入目标类,例如:AopDemo类
- 连接点(Joinpoint):可能被Spring拦截的点(方法),例如:AopDemo类中的update()方法
- 切入点(Pointcut):已经被增强的连接点(方法),例如:AopDemo类中的add()方法
- 增强/通知(Advice):那些增强的代码,例如:AdviceDemo类中的方法
- 织入(Weaving):把增强advice应用到目标类target来创建新的代理对象procxy的过程
- 代理(Proxy):一个类被AOP织入增强后,就产出了一个结果类,这个结果类就是融合了原类和增强逻辑的代理类
- 切面(Aspect):切入点和增强的结合。通知说明了干什么和什么时候干(什么时候通过方法名中的befor,after,around等就能知道),切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。
七、Spring实现AOP的过程
1、 编写目标类
/**
- 目标类
- Feng, Ge 2020/2/19 21:32
*/
public class AopDemo {
public void add() {
System.out.println("执行增加!");
}
public void update() {
System.out.println("执行修改!");
}
public void delete() {
System.out.println("删除!");
}
}
2、 编写增强
/**
- 增强
- Feng, Ge 2020/2/19 21:57
*/
public class AdviceDemo {
public void before(){
System.out.println("开启事务!");
}
public void after(){
System.out.println("提交事务!");
}
}
3、配置xml
结果:
开启事务!
执行增加!
提交事务!
注意:
1、使用AOP功能时,ApplicationContext.xml头文件要增加Aop相关规范及资源
- xmlns XML NameSpace的缩写,初始化bean的格式文件地址,来区分此xml文件不同于别的xml
- xmlns:xsi 指定了xml所使用的Schema(模式)定义的语言及需要遵循的规范
- xmlns:aop 使用spring框架的aop 面向切面编程时,在xml文件中引入aop资源
- xsi:context:关于spring上下文,包括加载资源文件
- xmlns:tx spring 事务配置
- xsi:schemaLocation 引入所有xml本文档需要遵循的规范 解析器在需要的情况下对该属性引入的文档进行校验,第一个值表示命名空间,第二个值表示该命名空间的模式文档的具体位置,其中命名空间和对应的 xsd 文件的路径,必须成对出现。比如用到的 、、 等命名空间,都需要在这里声明其 xsd 文件地址。
2、需要导入aspectjweaver.jar包
3、AOP的切入点表达式
<aop:pointcut expression=“execution(表达式)” id=“pCut”/>
- public void com.wo.domain.AopDemo.add() ==》具体的
- public void com.wo.domain.AopDemo.add(…) ==》切入点的方法参数不固定
- public void com.wo.domain.AopDemo.*add(…) ==》切入点的方法开头不确定
在Spring中有4种实现AOP的方式:
1.使用ProxyFactoryBean和对应的接口实现AOP
2.使用XML配置AOP
3.使用@AspectJ注解驱动切面
4.使用AspectJ注入切面
八、AOP增强/通知的五种类型
- before 前置通知
目标方法运行之前调用 - after-returning 后置通知
目标方法运行之后调用,但出现异常则不会调用 - around 环绕通知
在目标方法之前和之后都调用 - after-throwing 异常拦截通知
出现异常就会调用 - after 最终通知
目标方法运行之后调用,无论有无异常都会调用
/**
* 目标类
* Feng, Ge 2020/2/19 21:32
*/
public class AopDemo {
public void add() {
// 手动产生异常
int i = 6/0;
System.out.println("执行增加!");
}
public void update() {
System.out.println("执行修改!");
}
public void delete() {
System.out.println("删除!");
}
}
/**
* 增强
* Feng, Ge 2020/2/19 21:57
*/
public class AdviceDemo {
public void before() {
System.out.println("开启事务!");
}
public void after() {
System.out.println("提交事务!");
}
public void afterExp() {
System.out.println("出现异常会执行我!");
}
}
public void testAop() {
// 1、默认从src下加载.xml,根据配置文件创建 容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、从容器对象中取得 需要的 bean对象
AopDemo aopDemo = (AopDemo) context.getBean("aopDemo");
aopDemo.add();
}
结果:
jionpoint参数:
在每个增强方法中,都可以接收一个Joinpoint类型参数,主要包含两个方法:
- getTarget():获得被代理的目标对象
- getSignature():获取被代理目标类中的目标方法
/**
* 增强
* Feng, Ge 2020/2/19 21:57
*/
public class AdviceDemo {
public void before(JoinPoint joinPoint) {
System.out.println(joinPoint.getTarget());
System.out.println(joinPoint.getSignature());
System.out.println(joinPoint.getSignature().getName());
System.out.println("开启事务!");
}
}
com.wo.domain.AopDemo
void com.wo.domain.AopDemo.add()
add
开启事务!
执行增加!
提交事务!
九、Spring中的注解
利用注解也可以实现一下目标:
- IOC
- DI
- AOP
就是利用注解取代xml配置。
但是并不是xml文件就不需要了,使用注解时需要先在xml文件中添加注解相关约束加载相关资源,并开启注解扫描,即告诉Spring要以注解的方式创建bean。
一般不推荐XML的方式装配Bean,更多时候会使用注解的方式去装配bean,注解不仅提供了XML的功能,也提供了自动装配功能,采用注解遵循了“约定优于配置”的思想,开发者要做的决断更少。
1、注解实现IOC
具体配置说明如下:
value = "people")(
public class People {
private int age;
private String name;
private Ball ball;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Ball getBall() {
return ball;
}
public void setBall(Ball ball) {
this.ball = ball;
}
public String toString() {
return "People{" +
"age=" + age +
", name='" + name + '\'' +
", ball=" + ball +
'}';
}
}
public void test2() {
// 1、默认从src下加载.xml,根据配置文件创建 容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、从容器对象中取得 需要的 bean对象
People people = (People) context.getBean("people");
System.out.println(people);
}
People{age=0, name='null', ball=null}
这里xml中并没有配置标签,但是依然可以实现bean的创建,这就是注解方式。@Componment就是把普通pojo实例化到spring容器中,相当于配置文件中的 < bean id="" class=""/>
注意:
Spring中的@Componment有三个衍生注解:
- @Controller ----> Web层
- @Service ----> 业务层
- @Repository ----> 持久层(DAO)
这3个和@Componment的作用是一样的,这4个注解都可以完成bean的创建,但是后3个可以更清晰的展示各层级,利于项目的机构清晰和代码可读性,项目开发中各层最好使用各层专有的注解形式。
2、注解实现DI
注解实现DI就可以不再提供set或者构造器,分为以下几种:
- 普通类型注入,使用@Value
- 对象注入,使用@AutoWired(按类型自动装配)或者@Qualifier(强制使用名称注入);还可以使用@Resource,它是Java提供的,但是Spring也支持这种注解形式,相当于@AutoWired和@Qualifier一起使用
value = "people")(
public class People {
("16")
private int age;
("ohou")
private String name;
private Ball ball;
// 不在需要set()方法
// public String getName() {
// return name;
// }
//
// public void setName(String name) {
// this.name = name;
// }
//
// public int getAge() {
// return age;
// }
//
// public void setAge(int age) {
// this.age = age;
// }
//
// public Ball getBall() {
// return ball;
// }
//
// public void setBall(Ball ball) {
// this.ball = ball;
// }
public String toString() {
return "People{" +
"age=" + age +
", name='" + name + '\'' +
", ball=" + ball +
'}';
}
}
public class Ball {
("好球")
private String name;
private int size;
public String toString() {
return "Ball{" + "name='" + name + '\'' + ", size=" + size + '}';
}
}
public void test2() {
// 1、默认从src下加载.xml,根据配置文件创建 容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、从容器对象中取得 需要的 bean对象
People people = (People) context.getBean("people");
System.out.println(people);
}
结果:
People{age=16, name='ohou', ball=Ball{name='好球', size=0}}
3、注解实现AOP
实例一:
1、首先是开启AOP扫描:
aop:aspectj-autoproxy</aop:aspectj-autoproxy>
2、目标类添加注解(创建bean)
public class AopDemo {
public void add() {
System.out.println("执行增加!");
}
public void update() {
System.out.println("执行修改!");
}
public void delete() {
System.out.println("删除!");
}
}
3、增强添加注解(创建bean、配置切面(配置切入点、通知类型))
public class AdviceDemo {
(value = "execution(public void add())")
public void before(JoinPoint joinPoint) {
System.out.println(joinPoint.getTarget());
System.out.println(joinPoint.getSignature());
System.out.println(joinPoint.getSignature().getName());
System.out.println("开启事务!");
}
(value = "execution(public void add())")
public void after() {
System.out.println("提交事务!");
}
public void afterExp() {
System.out.println("出现异常会执行我!");
}
}
public void testAop() {
// 1、默认从src下加载.xml,根据配置文件创建 容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2、从容器对象中取得 需要的 bean对象
AopDemo aopDemo = (AopDemo) context.getBean("aopDemo");
aopDemo.add();
}
com.wo.domain.AopDemo
void com.wo.domain.AopDemo.add()
add
开启事务!
执行增加!
提交事务!
实例二:
切面:
public class LogAspect {
//这个方法定义了切入点
("@annotation(com.example.demo.aop.anno.MyLog)")
public void pointCut() {}
//这个方法定义了具体的通知
("pointCut()")
public void recordRequestParam(JoinPoint joinPoint) {
for (Object s : joinPoint.getArgs()) {
//打印所有参数,实际中就是记录日志了
System.out.println("after advice : " + s);
}
}
//这个方法定义了具体的通知
("pointCut()")
public void startRecord(JoinPoint joinPoint) {
for (Object s : joinPoint.getArgs()) {
//打印所有参数
System.out.println("before advice : " + s);
}
}
//这个方法定义了具体的通知
("pointCut()")
public Object aroundRecord(ProceedingJoinPoint pjp) throws Throwable {
for (Object s : pjp.getArgs()) {
//打印所有参数
System.out.println("around advice : " + s);
}
return pjp.proceed();
}
}
上面的切点是借助自定义注解实现的,自定义注解:
(RetentionPolicy.RUNTIME)
({ElementType.METHOD, ElementType.TYPE})
public @interface MyLog {
}
目标类:
public class MockController {
("/hello")
public String helloAop( String key) {
System.out.println("do something...");
return "hello world";
}
}
最后是测试类:
class MockControllerTest {
MockController mockController;
void helloAop() {
mockController.helloAop("aop");
}
}
控制台结果:
around advice : aop before advice : aop do something... after advice : aop
实例三:
上面使用的自定义注解是结合pointCut注解使用的,自定义注解还有一种使用方法:
public class VerifyValidAspect {
private VerifyCodeService verifyCodeService;
/**
* 设置切点为 SMSValid 注解
*
* @param joinPoint
* @throws VerifyCodeValidException
*/
("@within(com.zkml.dc.annotation.VerifyCodeValid)")
public void validateSms(JoinPoint joinPoint) throws VerifyCodeValidException {
// 验证缓存中的 验证码验证结果标识,验证失败抛出异常
if (!verifyCodeService.verifyCodeValid()) {
throw new VerifyCodeValidException();
}
log.info("短信验证码验证成功,可以继续访问 ==> {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
}
}
使用“@within(注解类型)”匹配所有持有指定注解类型内的方法。
十、Spring中的事务管理
1、事务的特性
先回顾一下事务的特性(ACID):
- 原子性(Atomicity):操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。
- 一致性(Consistency):事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。
拿转账来说,
假设用户A和用户B两者的钱加起来一共是20000,
那么不管A和B之间如何转账,转几次账,
事务结束后两个用户的钱相加起来应该还得是20000,
这就是事务的一致性。
- 隔离性(Isolation):隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。 - 持久性(Durability):当事务正确完成后,它对于数据的改变是永久性的。
例如我们在使用JDBC操作数据库时,在提交事务方法后,
提示用户事务操作完成,当我们程序执行完成直到看到提示后,
就可以认定事务已经正确提交,即使这时候数据库出现了问题,
也必须要将我们的事务完全执行完成,
否则就会造成我们看到提示事务处理完毕,
但是数据库因为故障而没有执行事务的重大错误。
如果违背以上原则就很可能引起下面的问题:
- 脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
小明的银行卡余额里有100元。现在他打算用手机点一个外卖饮料,
需要付款10元。但是这个时候,他的女朋友看中了一件衣服95元,
她正在使用小明的银行卡付款。于是小明在付款的时候,
程序后台读取到他的余额只有5块钱了,根本不够10元,
所以系统拒绝了他的交易,告诉余额不足。
但是小明的女朋友最后因为密码错误,无法进行交易。
小明非常郁闷,明明银行卡里还有100元,怎么会余额不足呢?
- 幻读也叫虚读:一个事务执行两次查询,第二次结果集包含第一次中没有或某些行已经被删除的数据,造成两次结果不一致,只是另一个事务在这两次查询中间插入或删除了数据造成的。幻读是事务非独立执行时发生的一种现象。
例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,
这时事务T2又对这个表中插入了一行数据项,
而这个数据项的数值还是为“1”并且提交给数据库。
而操作事务T1的用户如果再查看刚刚修改的数据,
会发现还有一行没有修改,其实这行是从事务T2中添加的,
就好像产生幻觉一样,这就是发生了幻读。
- 不可重复读:一个事务两次读取同一行的数据,结果得到不同状态的结果,中间正好另一个事务更新了该数据,两次结果相异,不可被信任。
例如事务T1在读取某一数据,
而事务T2立马修改了这个数据并且提交事务给数据库,
事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
注意:
1.不可重复读和脏读的区别:脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
2.幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
2、事务的操作步骤
1.开启事务
2.提交事务
3.回滚事务(发生异常时)
3、事务隔离级别
1.脏读
2.读/写提交
3.可重复读
4.序列化
在互联网应用中不仅要考虑数据库的一致性,还要考虑系统的性能,由1到4,数据一致性是逐渐增强的,但是性能却是下降的,所以在选择隔离级别时要综合考虑数据一致性和性能。一般会选择“读/写提交”,可以防止脏读又能兼顾性能。在实际工作中,使用 @Transactional注解,它在不同的数据库中,隔离级别也是不同的,Mysql中支持这4种级别,且默认是第三种“可重复读”级别,在Oracle中只支持读/写提交和序列化两种,默认是读/写提交。
4、事务举例
传统处理事务时都是利用try…catch…finally进行分步处理:
public void saveRecord() {
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(new Configuration());
SqlSession sqlSession = null;
try {
sqlSession = defaultSqlSessionFactory.openSession();
System.out.println("处理完第一个事务");
System.out.println("处理完第二个事务");
// 多件事完成后,开始提交
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
// 遇到异常事务回滚
sqlSession.rollback();
} finally {
// 关闭资源
if (!(sqlSession == null)) {
sqlSession.close();
}
}
}
若有大量的try…catch必定会使人眼花缭乱,于是Spring给我们提供了一个处理事务的注解@Transactional,上面的代码可以用下面的代码替换:
public void saveRecord() {
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(new Configuration());
SqlSession sqlSession = defaultSqlSessionFactory.openSession();
System.out.println("处理完第一个事务");
System.out.println("处理完第二个事务");
}
这段代码没有任何开启关闭提交回滚等操作,但是却达到了上面一样的效果,且更加简洁易维护。实际上@Transactional就是Spring AOP的典型应用。
5、事务失效的原因
1、没有被 Spring 管理
// @Service
public class OrderServiceImpl implements OrderService {
public void update(Object object) {
}
}
如果此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。
另外,如果采用spring+spring mvc,则context:component-scan重复扫描问题可能会引起事务失败。如果spring和mvc的配置文件中都扫描了service层,那么事务就会失效。
原因:因为按照spring配置文件的加载顺序来讲,先加载springmvc配置文件,再加载spring配置文件,我们的事物一般都在spring配置文件中进行配置,在加载springMVC配置文件的时候,已经把servlet给注册,但是此时spring配置文件中配置的事物还没加载,导致事物无法成功注入到service中。所以应该把对service的扫描放在spring配置文件中或是其他配置文件中。
2、注解未添加在 public 方法上
@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
3、类内部自身调用问题
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
updateOrder(order);
}
public void updateOrder(Order order) {
// update order;
}
}
update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务管用吗?
再来看下面这个例子:
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
updateOrder(order);
}
(propagation = Propagation.REQUIRES_NEW)
public void updateOrder(Order order) {
// update order;
}
}
这次在 update 方法上加了 @Transactional,updateOrder 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?
这两个例子的答案是:不管用!
因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。
4、数据源没有配置事务管理器
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
如上面所示,当前数据源若没有配置事务管理器,那也是白搭!
5、不支持事务
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
updateOrder(order);
}
(propagation = Propagation.NOT_SUPPORTED)
public void updateOrder(Order order) {
// update order;
}
}
Propagation.NOT_SUPPORTED: 表示不以事务运行。
6、异常被吃了
这个也是出现比较多的场景:
public class OrderServiceImpl implements OrderService {
public void updateOrder(Order order) {
try {
// update order;
}catch (Exception e){
//do something;
}
}
}
把异常吃了,然后又不抛出来,事务就不生效了。
7、异常类型错误或格式配置错误
上面的例子再抛出一个异常:
public class OrderServiceImpl implements OrderService {
// @Transactional(rollbackFor = SQLException.class)
public void updateOrder(Order order) {
try { // update order
}catch (Exception e){
throw new Exception("更新错误");
}
}
}
这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:
rollbackFor = Exception.class)(
这个配置仅限于 Throwable 异常类及其子类。
在业务代码中如果抛出RuntimeException异常,事务回滚;但是抛出Exception,事务不回滚。
8、搜索引擎不支持事务
如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB引擎
Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
十一、事务传播行为
详情参见: 事务传播详解 Spring的事务,也就是数据库的事务操作,符合ACID标准,也具有标准的事务隔离级别。但是Spring事务有自己的特点,也就是事务传播机制。
所谓事务传播机制,也就是在事务在多个方法的调用中是如何传递的,是重新创建事务还是使用父方法的事务?父方法的回滚对子方法的事务是否有影响?这些都是可以通过事务传播机制来决定的。
1、REQUIRED
如果有事务则加入事务,如果没有事务,则创建一个新的(默认值)。
public class EmployeeServiceImpl implements EmployeeService {
/**
* 添加员工的同时添加部门
* REQUIRED 传播
* @param name 员工姓名
*/
(propagation = Propagation.REQUIRED)
public void addEmp(String name) {
Employee employee = new Employee();
employee.setDeptId(1);
employee.setName(name);
employeeMapper.insertSelective(employee);
departmentService.addDept("jishubu");
int i = 1/0;
}
}
public class DepartmentServiceImpl implements DepartmentService {
public void addDept(String name) {
Department department = new Department();
department.setName(name);
departmentMapper.insertSelective(department);
// int i = 1/0;
}
}
- 上述代码中,无论int i =1/0 这个异常出现在哪里,添加员工和添加部门都会回滚。 因为 REQUIRED 会让添加员工和添加部门变为一个事务。,
- 值得一提的是,如果异常在addDept中,但是在addEmp把 addDept方法 try,catch了,则会抛出异常:Transaction rolled back because it has been marked as rollback-only 。
- 如果在addDept上添加@Transactional(propagation = Propagation.REQUIRED),在addEmp不添加事务,则addDept是一个事务,addEmp并不是一个事务。因为addDept开启了一个事务,但是addEmp并不存在一个事务中。
结论:
在外围方法开启事务的情况下REQUIRED
修饰的内部方法会加入到外围方法的事务中,所有REQUIRED
修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。
在外围方法未开启事务的情况下Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
2、SUPPORTS
支持当前事务,如果当前没有事务,就以非事务方式执行。
propagation = Propagation.REQUIRED)(
public void addEmp(String name) {
Employee employee = new Employee();
employee.setDeptId(2);
employee.setName(name);
employeeMapper.insertSelective(employee);
departmentService.addDeptBySupports("jishubu");
// int i = 1/0;
}
propagation = Propagation.SUPPORTS)(
public void addDept(String name) {
Department department = new Department();
department.setName(name);
departmentMapper.insertSelective(department);
int i = 1/0;
}
这个属性主要是添加到addDept上的,也就是被调用方法上。因为添加到addEmp就不以事务的方式运行了。然后,如果addEmp为事务,则addDept也为事务。如果addEmp不是事务,则addDept也不是事务。
3、MANDATORY( 强制的)
使用当前的事务,如果当前没有事务,就抛出异常。
public void addEmp(String name) {
Employee employee = new Employee();
employee.setDeptId(2);
employee.setName(name);
employeeMapper.insertSelective(employee);
departmentService.addDeptBySupports("jishubu");
int i = 1/0;
}
propagation = Propagation.MANDATORY)(
public void addDept(String name) {
Department department = new Department();
department.setName(name);
departmentMapper.insertSelective(department);
int i = 1/0;
}
这个属性也是添加到addDept(被调用者) 上的。如果添加到addEmp(调用者)上,则直接抛出异常。该属性添加到addDept上, 如果addEmp有事务,则addDept加入到addEmp的事务中,如果addEmp没有事务,则直接抛出异常。
4、REQUIRES_NEW
不管是否存在事务,都创建一个新的事务,原来的方法挂起,新的方法执行完毕后,继续执行老的事务。
propagation = Propagation.REQUIRES_NEW)(
public void addEmp(String name) {
Employee employee = new Employee();
employee.setDeptId(2);
employee.setName(name);
employeeMapper.insertSelective(employee);
departmentService.addDeptBySupports("jishubu");
int i = 1/0;
}
propagation = Propagation.REQUIRES_NEW)(
public void addDept(String name) {
Department department = new Department();
department.setName(name);
departmentMapper.insertSelective(department);
// int i = 1/0;
}
这个属性应该是除了REQUIRED用的最多的。这个属性也是针对被调用者的。
不管调用者(addEmp)是否存在事务,被调用者(addDept)都会新开一个事务,相当于被调用者都存在于自己的事务中和调用者没有关系。如上述代码,addEmp会回滚,但addDept不会回滚。因为他们是两个事务。
结论:
在外围方法未开启事务的情况下REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
在外围方法开启事务的情况下REQUIRES_NEW
修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
5、NOT_SUPPORTED
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
propagation = Propagation.REQUIRED)(
public void addEmp(String name) {
Employee employee = new Employee();
employee.setDeptId(2);
employee.setName(name);
employeeMapper.insertSelective(employee);
departmentService.addDeptBySupports("jishubu");
int i = 1/0;
}
propagation = Propagation.NOT_SUPPORTED)(
public void addDept(String name) {
Department department = new Department();
department.setName(name);
departmentMapper.insertSelective(department);
// int i = 1/0;
}
这个属性如果放在调用者(addEmp)上,则是以非事务方式运行。
如果放在被调用者(addDept)上,该方法(addDept)以非事务运行,调用者如果有事务,则运行单独的事务(挂起)。上述代码,会出现添加员工回滚,添加部门不回滚。
6、NEVER
以非事务方式执行,如果当前存在事务,则抛出异常。
propagation = Propagation.REQUIRED)(
public void addEmp(String name) {
Employee employee = new Employee();
employee.setDeptId(2);
employee.setName(name);
employeeMapper.insertSelective(employee);
departmentService.addDeptBySupports("jishubu");
int i = 1/0;
}
propagation = Propagation.NEVER)(
public void addDept(String name) {
Department department = new Department();
department.setName(name);
departmentMapper.insertSelective(department);
// int i = 1/0;
}
这个属性如果在调用者上,则直接以非事务运行。
如果作用在被调用者上,则看调用者是否有事务,如果调用者有事务,则抛出异常,如果没有事务,则以非事务运行。上述代码中,则会抛出异常。(并不是除0异常,而是:Existing transaction found for transaction marked with propagation ‘never’)
7、NESTED
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。(这个和REQUIRED区别在于一个是加入到一个事务,一个是在嵌套事务运行)
propagation = Propagation.REQUIRED)(
public void addEmp(String name) {
Employee employee = new Employee();
employee.setDeptId(2);
employee.setName(name);
employeeMapper.insertSelective(employee);
departmentService.addDeptBySupports("jishubu");
// int i = 1/0;
}
propagation = Propagation.NESTED)(
public void addDept(String name) {
Department department = new Department();
department.setName(name);
departmentMapper.insertSelective(department);
int i = 1/0;
}
可以这么理解,大多数情况下,效果和REQUIRED一样。但是有一种情况,就是上述代码中,被调用者事务传播属性为NESTED,当被调用者出现异常时, ddEmp方法不会回滚,只有addDept回滚了。
结论:
在外围方法未开启事务的情况下NESTED
和REQUIRED
作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
在外围方法开启事务的情况下NESTED
修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务。
注意事项:
- REQUIRED:当两个方法的传播机制都是REQUIRED时,如果一旦发生回滚,两个方法都会回滚
- REQUIRES_NEW:当被调用方法传播机制为REQUIRES_NEW,会开启一个新的事务,并单独提交方法,所以调用方法的回滚并不影响被调用方法事务提交
- NESTED:当调用方法为REQUIRED,被调用方法方法为NESTED时,被调用方法方法开启一个嵌套事务; 当调用方法回滚时,被调用方法方法也会回滚;反之,如果被调用方法方法回滚,则并不影响调用方法的提交(外层会影响内层事务,但内层事务是内层自己的,不影响外层事务)
REQUIRED,REQUIRES_NEW,NESTED异同:
NESTED
和REQUIRED
修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是REQUIRED
是加入外围方法事务,所以和外围事务同属于一个事务,一旦REQUIRED
事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED
是外围方法的子事务,有单独的保存点,所以NESTED
方法抛出异常被回滚,不会影响到外围方法的事务。
NESTED
和REQUIRES_NEW
都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED
是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW
是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。
补充一、Spring 如何解决循环依赖♻️
通常来说,如果问Spring内部如何解决循环依赖,一定是单默认的单例Bean中,属性互相引用的场景。
比如几个Bean之间的互相引用:
甚至自己“循环”依赖自己:
先说明前提:原型(Prototype)
的场景是不支持循环依赖的,通常会走到AbstractBeanFactory
类中下面的判断,抛出异常。
if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName);}
原因很好理解,创建新的A时,发现要注入原型字段B,又创建新的B发现要注入原型字段A…
这就套娃了, 你猜是先StackOverflow
还是OutOfMemory
?Spring怕你不好猜,就先抛出了BeanCurrentlyInCreationException
。换句话说,原型方式的循环依赖无法解决。
另外除了不支持原型方式的循环依赖外,基于构造器的循环依赖,就更不用说了,官方文档都摊牌了,你想让构造器注入支持循环依赖,是不存在的,不如把代码改了。
Spring IOC 容器中获取 bean 实例的简化版流程如下(排除了各种包装和检查的过程):
那么默认单例的属性注入场景,Spring是如何支持循环依赖的?
首先,Spring内部维护了三个Map
,也就是我们通常说的三级缓存
。
在Spring的DefaultSingletonBeanRegistry
类中挂着这三个Map
:
-
singletonObjects
:完成初始化的单例对象的 cache,这里的 bean 经历过实例化->属性填充->初始化
以及各种后置处理
(一级缓存) -
earlySingletonObjects
:存放原始的 bean 对象(完成实例化
但是尚未填充属性和初始化),仅仅能作为指针提前曝光,被其他 bean 所引用,用于解决循环依赖的 (二级缓存) -
singletonFactories
:在 bean实例化完之后
,属性填充以及初始化之前
,如果允许提前曝光,Spring 会将实例化后的 bean 提前曝光,也就是把该 bean 转换成 beanFactory 并加入到 singletonFactories(三级缓存)
后两个Map其实是“垫脚石”级别的,只是创建Bean的时候,用来借助了一下,创建完成就清掉了。
为什么成为后两个Map为垫脚石? 假设最终放在singletonObjects
的Bean是你想要的一杯“凉白开”。
那么Spring准备了两个杯子,即singletonFactories
和earlySingletonObjects
来回“倒腾”几番,把热水晾成“凉白开”放到singletonObjects
中。
Spring中有三个缓存,用于存储单例的Bean实例,这三个缓存是彼此互斥的,不会针对同一个Bean的实例同时存储。如果调用getBean,则需要从三个缓存中依次获取指定的Bean实例。 读取顺序依次是一级缓存–>二级缓存–>三级缓存。
第一级缓存的作用?
- 用于存储单例模式下创建的Bean实例(已经创建完毕)
- 该缓存是对外使用的,指的就是使用Spring框架的程序员
- 存储什么数据?K:bean的名称;V:bean的实例对象(有代理对象则指的是代理对象,已经创建完毕)
第二级缓存的作用?
- 用于存储单例模式下创建的Bean实例(该Bean被
提前暴露的引用
,该Bean还在创建中)- 该缓存是对内使用的,指的就是Spring框架内部逻辑使用该缓存
- 存储什么数据?K:bean的名称;V:bean的实例对象(有代理对象则指的是代理对象,该Bean还在创建中)
第三级缓存的作用?
- 通过ObjectFactory对象来存储单例模式下提前暴露的Bean实例的引用(正在创建中)
- 该缓存是对内使用的,指的就是Spring框架内部逻辑使用该缓存。此缓存是解决循环依赖最大的功臣
- 存储什么数据?K:bean的名称;V:ObjectFactory,该对象持有提前暴露的bean的引用
如果缓存没有的话,我们就要创建了,接着我们以单例对象为例,再看下创建 bean 的逻辑:
上面的流程描述如下:
- Spring 创建 bean 主要分为两个步骤,创建原始 bean 对象,接着去填充对象属性和初始化
- 每次创建 bean 之前,都会从缓存中查下有没有该 bean,因为是单例,只能有一个
- 当创建 beanA 的原始对象后,并把它放到
三级缓存
中,接下来就该填充对象属性了,这时候发现依赖了 beanB,接着就又去创建 beanB,同样的流程,创建完 beanB 填充属性时又发现它依赖了 beanA,又是同样的流程,不同的是,这时候可以在三级缓存中查到刚放进去的原始对象 beanA,所以不需要继续创建,用它注入 beanB,完成 beanB 的创建 - 既然 beanB 创建好了,所以 beanA 就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成
但是这个地方,看源码会有个小疑惑,为什么需要三级缓存呢,二级也够了?
跟源码的时候,发现在创建 beanB 需要引用 beanA 这个“半成品”的时候,就会触发"前期引用":
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 三级缓存有的话,就把他移动到二级缓存
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
singletonFactory.getObject()
是一个接口方法,这里具体的实现方法在:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
// 这么一大段就这句话是核心,也就是当bean要进行提前曝光时,
// 给一个机会,通过重写后置处理器的getEarlyBeanReference方法,来自定义操作bean
// 值得注意的是,如果提前曝光了,但是没有被提前引用,则该后置处理器并不生效!!!
// 这也正式三级缓存存在的意义,否则二级缓存就可以解决循环依赖的问题
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
这个方法就是 Spring 为什么使用三级缓存,而不是二级缓存的原因,它的目的是为了后置处理。如果没有 AOP 后置处理,就不会走进 if 语句,直接返回了 exposedObject ,相当于啥都没干,二级缓存就够用了。
所以又得出结论,这个三级缓存应该和 AOP 有关系。
在 Spring 的源码中getEarlyBeanReference
是 SmartInstantiationAwareBeanPostProcessor
接口的默认方法,真正实现这个方法的只有 AbstractAutoProxyCreator
这个类,用于提前曝光的 AOP 代理。
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
// 对bean进行提前Spring AOP代理
return wrapIfNecessary(bean, beanName, cacheKey);
}
以上就是Spring解决循环依赖的关键点!总结来说,就是要搞清楚以下几点:
- 搞清楚Spring三级缓存的作用?
- 搞清楚第三级缓存中ObjectFactory的作用?
- 搞清楚为什么需要第二级缓存?
- 搞清楚什么时候使用三级缓存(添加和查询操作)?
- 搞清楚什么时候使用二级缓存(添加和查询操作)?
- 当目标对象产生代理对象时,Spring容器中(第一级缓存)到底存储的是谁?
面试中:
B 中提前注入了一个没有经过初始化的 A 类型对象不会有问题吗?
虽然在创建 B 时会提前给 B 注入了一个还未初始化的 A 对象,但是在创建 A 的流程中一直使用的是注入到 B 中的 A 对象的引用,之后会根据这个引用对 A 进行初始化,所以这是没有问题的。
Spring 是如何解决的循环依赖?
Spring 为了解决单例的循环依赖问题,使用了三级缓存。其中一级缓存为单例池(singletonObjects),二级缓存为提前曝光对象(earlySingletonObjects),三级缓存为提前曝光对象工厂(singletonFactories)。
假设A、B循环引用,实例化 A 的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了 B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖 A,这时候从缓存中查找到早期暴露的 A,没有 AOP 代理的话,直接将 A 的原始对象注入 B,完成 B 的初始化后,进行属性填充和初始化,这时候 B 完成后,就去完成剩下的 A 的步骤,如果有 AOP 代理,就进行 AOP 处理获取代理后的对象 A,注入 B,走剩下的流程。
为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?
所有的单例 bean 在实例化后都会被进行提前曝光到三级缓存中,但是并不是所有的 bean 都存在循环依赖,也就是三级缓存到二级缓存的步骤不一定都会被执行,有可能曝光后直接创建完成,没被提前引用过,就直接被加入到一级缓存中。因此可以确保只有提前曝光且被引用的 bean 才会进行该后置处理。
如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。
这个又涉及到了 Spring 中动态代理的实现,不管是cglib代理还是jdk动态代理生成的代理类,代理时,会将目标对象 target 保存在最后生成的代理 $proxy 中,当调用 proxy 中保存着 traget 也就是是 原始bean 的引用,因此后续 原始bean 的完善,也就相当于Spring AOP中的 target 的完善,这样就保证了 AOP 的属性填充与初始化了!
补充二、Spring提供几种配置方式来设置元数据?
将Spring配置到应用开发中有以下三种方式:
(1)基于XML的配置。
(2)基于注解的配置。
(3)基于Java对象的配置。