hello,我是小索奇,这里把Spring全套笔记分享出来哈,便于大家查看~一起加油
Spring
1、Spring简介
1.1、Spring概述
官网地址:Spring | Home
Spring 是最受欢迎的企业级 Java 应用程序开发框架,数以百万的来自世界各地的开发人员使用
Spring 框架来创建性能好、易于测试、可重用的代码。
Spring 框架是一个开源的 Java 平台,它最初是由 Rod Johnson 编写的,并且于 2003 年 6 月首
次在 Apache 2.0 许可下发布。
Spring 是轻量级的框架,其基础版本只有 2 MB 左右的大小。
Spring 框架的核心特性是可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应
用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于 POJO
编程模型来促进良好的编程实践。
框架主要就是为了增加扩展性
1.2、Spring家族
Spring是一个庞大的家族,其中有实现各种功能的框架
项目列表:Spring | Projects
1.3、Spring Framework
Spring 基础框架,可以视为 Spring 基础设施,基本上任何其他 Spring 项目、框架都是以 Spring Framework为基础的。
1.3.1、Spring Framework特性
-
非侵入式:使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常
小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会
破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序
时结构清晰、简洁优雅。
-
控制反转:IOC——Inversion of Control,翻转资源获取方向。把自己创建资源、向环境索取资源
变成环境将资源准备好,我们享受资源注入。把对象的控制权翻转给程序本身(教给spring来管理我们的对象)
本来是主动创建对象,现在是被动接收对象,框架为我们提供对象-配置在xml中,降低耦合性,降低对象之间的依赖关系
-
面向切面编程(横向抽取):AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码功
能。
-
容器:Spring IOC 是一个容器,因为它包含并且管理组件对象的生命周期(帮我们管理对象)。组件享受到了容器化
的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率。
-
对我们创建对象过程进行了屏蔽,我们只需要关心如何使用即可,不需要关心如何创建的对象
-
组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML
我们给ioc所管理的对象,它在ioc中就叫做组件
和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭
建超大型复杂应用系统。
-
声明式:很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现。
编程式具体功能什么的由我们自己去实现
-
一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(可利用我们spring的两大核心去整合我们的第三方框架MyBatis)。而且
Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基础上全部使用 Spring 来实现。
1.3.2、Spring Framework五大功能模块
功能模块 | 功能介绍 |
---|---|
Core Container | IOC核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。 |
AOP&Aspects | 第二个核心:面向切面编程 |
Testing | 提供了对 junit 或 TestNG 测试框架的整合。 |
Data Access/Integration | 提供了对数据访问/集成的功能。 |
Spring MVC | 提供了面向Web应用程序的集成功能。 |
2、IOC
2.1、IOC容器
2.1.1、IOC思想
IOC:Inversion of Control,翻译过来是反转控制。
①获取资源的传统方式
自己做饭:买菜、洗菜、择菜、改刀、炒菜,全过程参与,费时费力,必须清楚了解资源创建整个过程中的全部细节且熟练掌握。
在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的
模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。
②反转控制方式获取资源
点外卖:下单、等、吃,省时省力,不必关心资源创建过程的所有细节。
反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。
③DI
DI:Dependency Injection,翻译过来是依赖注入。就是为我们当前所管理的依赖赋值
DI 是 IOC 的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器
的资源注入。相对于IOC而言,这种表述更直接。
所以结论是:IOC 就是一种反转控制的思想, 而 DI 是对 IOC 的一种具体实现。
2.1.2、IOC容器在Spring中的实现
-
实际上就是spring一系列的api
-
我们把对象教给IOC管理之后,我们要想获取IOC容器所管理的对象,我们要先获取IOC容器
Spring 的 IOC 容器就是 IOC 思想的一个落地的产品实现。IOC 容器中管理的组件也叫做 bean。在创建bean 之前,首先需要创建 IOC 容器。Spring 提供了 IOC 容器的两种实现方式:
①BeanFactory
-
既然叫Factory,那么用的就是Factory模式
这是 IOC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。我们程序媛用ApplicationContext
②ApplicationContext
BeanFactory 的子接口,提供了更多高级特性(子接口肯定要比父接口的功能多)。面向 Spring 的使用者,几乎所有场合都使用
ApplicationContext 而不是底层的 BeanFactory。
-
接口:需要向上转型,创建IOC对象
③ApplicationContext的主要实现类
类型名 | 简介 |
---|---|
ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 |
FileSystemXmlApplicationContext | 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象 |
ConfigurableApplicationContext | ApplicationContext 的子接口,包含一些扩展方法refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。 |
WebApplicationContext | 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。 |
-
配置文件一般都写到类目录下(java下的resources中),resources会被加载到类目录下,ClassPathXmlApplicationContext用的多
-
FileSystemXml是文件系统,从磁盘上某一个地方的xml,通过这个xml读取IOC容器
2.2、基于XML管理bean
-
spring来管理对象,管理的对象叫做主键,也叫做bean
-
管理方式共有两种:
-
基于xml
-
基于注解方式
-
2.2.1、实验一:入门案例
①创建Maven Module
②引入依赖
spring-context:spring上下文,依赖具有传递性,spring-conntext所依赖的jar包也会被传递进来
<dependencies> <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies>
③创建类HelloWorld
public class HelloWorld { public void sayHello(){ System.out.println("helloworld"); } }
-
现在可以指定某个xml来获取IOC容器,我们获取ioc容器时候是可以指定的,这里xml名字可以是不固定的
-
整合SSM时候就是固定了
⑤在Spring的配置文件中配置bean
-
spring默认提供的对象是单例的,所以我们通过获取这个bean就可以来使用了
<!-- 配置HelloWorld所对应的bean,即将HelloWorld的对象交给Spring的IOC容器管理 通过bean标签配置IOC容器所管理的bean 属性: id:设置bean的唯一标识 class:设置bean所对应类型的全类名 --> <bean id="helloworld" class="com.atguigu.spring.bean.HelloWorld"></bean>
⑥创建测试类测试
-
ioc.getBean(name)这个name就是bean的唯一标识:id,
-
通过IOC获取的,我们不知道类型,我们需要强转
@Test public void testHelloWorld(){ ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); HelloWorld helloworld = (HelloWorld) ioc.getBean("helloworld"); helloworld.sayHello(); }
⑦思路
在IOC容器中它就是通过反射+工厂模式来帮助我们创建对象,管理对象的
它默认使用的就是无参构造,不知道我们的有参构造是什么,当把无参构造删除时候,执行会发现出现错误(NoSuchMethodException)
以后设置类的时候别忘记写无参构造,我们没写有参构造,无参构造是默认的
⑧注意
Spring 底层默认通过反射技术调用组件类的无参构造器来创建组件对象,这一点需要注意。如果在需要无参构造器时,没有无参构造器,则会抛出下面的异常:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'helloworld' defined in class path resource [applicationContext.xml]: Instantiation of bean
failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed
to instantiate [com.atguigu.spring.bean.HelloWorld]: No default constructor found; nested
exception is java.lang.NoSuchMethodException: com.atguigu.spring.bean.HelloWorld.
<init>()
2.2.2、实验二:获取bean
①方式一:根据id获取
由于 id 属性指定了 bean 的唯一标识,所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。
上个实验中我们使用的就是这种方式。
②方式二:根据类型获取
ac也就是ioc
@Test public void testHelloWorld(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); HelloWorld bean = ac.getBean(HelloWorld.class); bean.sayHello(); }
③方式三:根据id和类型
-
进一步筛选,可以放在IOC容器有两个相同的class路径
@Test public void testHelloWorld(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); HelloWorld bean = ac.getBean("helloworldOne", HelloWorld.class); bean.sayHello(); }
④注意
当根据类型获取bean时,要求IOC容器中指定类型的bean有且只能有一个,否则报错,但是这种情况很少出现,我们用的做多的还是方式二:根据类型获取
class里面只能写的是一个具体类型,接口啥的是没有构造方法的,不能newInstance(但是扩展到这个类上的接口是可以用的)
当IOC容器中一共配置了两个:
<bean id="helloworldOne" class="com.atguigu.spring.bean.HelloWorld"></bean> <bean id="helloworldTwo" class="com.atguigu.spring.bean.HelloWorld"></bean>
根据类型获取时会抛出异常:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean
of type 'com.atguigu.spring.bean.HelloWorld' available: expected single matching bean but
found 2: helloworldOne,helloworldTwo
⑤扩展
如果组件类实现了接口,根据接口类型可以获取 bean 吗?
可以,前提是bean唯一
只要是这个对象实现的接口,和这个对象有关就可以获取到这个bean(管理的对象)
Class Student implements Person
接口拓展:
boolean result = obj instanceof Class;
其中,obj指的是对象,class为类或者接口,该表达式的含义为:当obj是class的对象,或者是其直接/间接子类,或者是其接口的实现类时,result为true,否则为false。
如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗?
不行,因为bean不唯一
⑥结论
根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类
型』的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到(即通过bean继承的,实现的类型都可以获取到bean)。
2.2.3、实验三:依赖注入(DI)之setter注入
-
IOC是从我们当前获取资源的角度来说,我们以前需要主动获取,现在被动接受,
比如我们Student里面有id、name、age、sex,那么我们就是Student是依赖于id、name、age、sex属性,既然这个对象依赖于这些属性,那么我们就可以在IOC中为它所依赖的属性赋值
-
依赖注入:就是对我们当前类中的属性赋值的过程就叫做依赖注入
-
把方法名中的get、set去掉,剩余部分的首字母变为小写就是属性,严格来说属性指的就是我们的get和set方法
①创建学生类Student
public class Student { private Integer id; private String name; private Integer age; private String sex; public Student() { } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", sex='" + sex + '\'' + '}'; } }
②配置bean时为属性赋值
-
DI依赖注入第一种方式set注入(它底层用的是set注入)
<bean id="studentTwo" class="com.atguigu.spring.pojo.Student"> <!-- property标签:通过组件类的setXxx()方法给组件对象设置属性 --> <!-- name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关)--> <!-- value属性:指定属性值 --> <property name="id" value="1001"></property> <property name="name" value="张三"></property> <property name="age" value="23"></property> <property name="sex" value="男"></property> </bean>
③测试
@Test public void testDIBySet(){ ApplicationContext ac = new ClassPathXmlApplicationContext("spring-ioc.xml"); Student studentTwo = ac.getBean("studentTwo", Student.class);//这里测试用id和类型,用的多的还是根据类型获取 System.out.println(studentTwo); }
2.2.4、实验四:依赖注入之构造器注入
①在Student类中添加有参构造
public Student(Integer id, String name, Integer age, String sex) { this.id = id; this.name = name; this.age = age; this.sex = sex; }
②配置bean
<bean id="studentTwo" class="com.atguigu.spring.bean.Student"> <constructor-arg value="1002"></constructor-arg> <constructor-arg value="李四"></constructor-arg> <constructor-arg value="33"></constructor-arg> <constructor-arg value="女"></constructor-arg> </bean>
注意:
constructor-arg标签还有两个属性可以进一步描述构造器参数:
-
index属性:指定参数所在位置的索引(从0开始)
-
name属性:指定参数名
③测试
@Test public void testDIBySet(){ ApplicationContext ac = new ClassPathXmlApplicationContext("springdi.xml"); Student studentOne = ac.getBean("studentTwo", Student.class); System.out.println(studentOne); }
问题:如果有两个相同的有参构造匹配哪一个?
这里默认匹配的是第二个score,如果要指定类型一定设置name给想要给的属性
2.2.5、实验五:特殊值处理
①字面量赋值
什么是字面量?
int a = 10;
声明一个变量a,初始化为10,此时a就不代表字母a了,而是作为一个变量的名字。当我们引用a
的时候,我们实际上拿到的值是10。
而如果a是带引号的:'a',那么它现在不是一个变量,它就是代表a这个字母本身,这就是字面
量。所以字面量没有引申含义,就是我们看到的这个数据本身。
-
基本数据类型及其包装类,和String都是字面量
-
value是专门给字面量赋值的,直接在里面写个null?不可能的,她在这里表示字符串null
<!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 --> <property name="name" value="张三"/>
②null值
<property name="name"> <null /> </property>
注意:
<property name="name" value="null"></property>
以上写法,为name所赋的值是字符串null
如何知道控制台输出的是空字符串还是null?
-
我们在输出时候.toString即可,不报错就是字符串
如何给属性设置null?
-
在里面设置null标签即可(单双标签都可:<null />),也可以在里面设置子标签value,不写在property里面也可以
③xml实体
-
xml是可扩展标识语言
-
它和html相似之处就是不能用特殊字符,比如value="<王五>" 这个标签就不对,这个尖括号就是特殊字符,如何解决这个问题?
用这个特殊字符所对应的实体,<是< ,>是>
可以百度搜索 实体
<!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 --> <!-- 解决方案一:使用XML实体来代替 --> <property name="expression" value="a < b"/>
④CDATA节
放在CDATA节里面的内容都会被原样解析,不用实体也可
-
它是xml中的一个特殊的标签,不能直接写到property中
-
<![CDATA[]]>本身就是一个特殊的标签,只能以标签方式写,所以是不能在属性里面直接写的,可以在子标签里面写
-
在idea中直接输入大写CD就可以快速生成
<property name="expression"> <!-- 解决方案二:使用CDATA节 --> <!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 --> <!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 --> <!-- 所以CDATA节中写什么符号都随意 --> <value><![CDATA[a < b]]></value> </property>
2.2.6、实验六:为类类型属性赋值
①创建班级类Clazz(因为Class是关键字,所以Clazz)
public class Clazz { private Integer clazzId; private String clazzName; public Integer getClazzId() { return clazzId; } public void setClazzId(Integer clazzId) { this.clazzId = clazzId; } public String getClazzName() { return clazzName; } public void setClazzName(String clazzName) { this.clazzName = clazzName; } @Override public String toString() { return "Clazz{" + "clazzId=" + clazzId + ", clazzName='" + clazzName + '\'' + '}'; } public Clazz() { } public Clazz(Integer clazzId, String clazzName) { this.clazzId = clazzId; this.clazzName = clazzName; } }
②修改Student类
在Student类中添加以下代码:
private Clazz clazz; public Clazz getClazz() { return clazz; } public void setClazz(Clazz clazz) { this.clazz = clazz; }
③方式一:引用外部已声明的bean
-
对1对对象,对多对集合
-
这里我把配置Clazz类型的bean:"com.atguigu.spring.bean.Clazz"改为了"com.atguigu.spring.pojo.Clazz" ,和视频老师的一样
<bean id="clazzOne" class="com.atguigu.spring.pojo.Clazz"> <property name="clazz.cid" value="1111"></property> <property name="clazz.cname" value="财源滚滚班"></property> </bean>
为Student中的clazz属性赋值:
<bean id="studentFour" class="com.atguigu.spring.pojo.Student"> <property name="id" value="1004"></property> <property name="name" value="赵六"></property> <property name="age" value="26"></property> <property name="sex" value="女"></property> <!-- ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值 --> <property name="clazz" ref="clazzOne"></property> </bean>
错误演示:
value是给字面量赋值的
<bean id="studentFour" class="com.atguigu.spring.pojo.Student"> <property name="id" value="1004"></property> <property name="name" value="赵六"></property> <property name="age" value="26"></property> <property name="sex" value="女"></property> <property name="clazz" value="clazzOne"></property> </bean>
如果错把ref属性写成了value属性,会抛出异常: Caused by: java.lang.IllegalStateException:
Cannot convert value of type 'java.lang.String' to required type
'com.atguigu.spring.bean.Clazz' for property 'clazz': no matching editors or conversion
strategy found
意思是不能把String类型转换成我们要的Clazz类型,说明我们使用value属性时,Spring只把这个
属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值
④方式二:内部bean
<bean id="studentFour" class="com.atguigu.spring.pojo.Student"> <property name="id" value="1004"></property> <property name="name" value="赵六"></property> <property name="age" value="26"></property> <property name="sex" value="女"></property> <property name="clazz"> <!-- 在一个bean中再声明一个bean就是内部bean --> <!-- 内部bean只能用于给属性赋值,不能在外部通过创建IOC容器获取,因此可以省略id属性 --> <bean id="clazzInner" class="com.atguigu.spring.pojo.Clazz"> <property name="cid" value="2222"></property> <property name="cname" value="远大前程班"></property> </bean> </property> </bean>
③方式三:级联属性赋值
-
一般不用这种,麻烦
和MyBatis不一样的地方:一定先引用某个bean为属性赋值或者实例化,才可以使用级联方式更新属性,使用点.
<bean id="studentFour" class="com.atguigu.spring.pojo.Student"> <property name="id" value="1004"></property> <property name="name" value="赵六"></property> <property name="age" value="26"></property> <property name="sex" value="女"></property> <!-- 一定先引用某个bean为属性赋值或者实例化,才可以使用级联方式更新属性 --> <property name="clazz" ref="clazzOne"></property> <property name="clazz.cid" value="3333"></property> <property name="clazz.cname" value="最强王者班"></property> </bean>
2.2.7、实验七:为数组类型属性赋值
①修改Student类
在Student类中添加以下代码:
private String[] hobbies; public String[] getHobbies() { return hobbies; } public void setHobbies(String[] hobbies) { this.hobbies = hobbies; }
②配置bean
-
<array>里面使用的标签取决于类类型还是字面量(前者用ref表示引用,后者用value)
<bean id="studentFour" class="com.atguigu.spring.bean.Student"> <property name="id" value="1004"></property> <property name="name" value="赵六"></property> <property name="age" value="26"></property> <property name="sex" value="女"></property> <!-- ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值 --> <property name="clazz" ref="clazzOne"></property> <property name="hobbies"> <array> <value>抽烟</value> <value>喝酒</value> <value>烫头</value> </array> </property> </bean>
2.2.8、实验八:为集合类型属性赋值
①为List集合类型属性赋值
在Clazz类中添加以下代码:内部bean
private List<Student> students; public List<Student> getStudents() { return students; } public void setStudents(List<Student> students) { this.students = students; }
配置bean:
<bean id="clazzTwo" class="com.atguigu.spring.bean.Clazz"> <property name="cid" value="4444"></property> <property name="cname" value="Javaee0222"></property> <property name="students"> <list> <ref bean="studentOne"></ref> <ref bean="studentTwo"></ref> <ref bean="studentThree"></ref> </list> </property> </bean>
若为Set集合类型属性赋值,只需要将其中的list标签改为set标签即可
集合方式引入约束:
-
utils:list是设置集合类型的bean
-
utils:map设置map集合类型的bean
一开始有beans约束才能用beans,现在加上<utils:list会自动导入utils约束
引用studentList
②为Map集合类型属性赋值
创建教师类Teacher:
public class Teacher { private Integer teacherId; private String teacherName; public Integer getTeacherId() { return teacherId; } public void setTeacherId(Integer teacherId) { this.teacherId = teacherId; } public String getTeacherName() { return teacherName; } public void setTeacherName(String teacherName) { this.teacherName = teacherName; } public Teacher(Integer teacherId, String teacherName) { this.teacherId = teacherId; this.teacherName = teacherName; } public Teacher() { } @Override public String toString() { return "Teacher{" + "teacherId=" + teacherId + ", teacherName='" + teacherName + '\'' + '}'; } }
在Student类中添加以下代码:
private Map<String, Teacher> teacherMap; public Map<String, Teacher> getTeacherMap() { return teacherMap; } public void setTeacherMap(Map<String, Teacher> teacherMap) { this.teacherMap = teacherMap; }
配置bean:
-
entry表示键和值
-
value-ref引用某一个bean作为value值
-
如果键是字面量就用key,类类型用key-ref,value同理
spring-ioc.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" xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> <bean id="studentOne" class="com.atguigu.spring.pojo.Student"></bean> <bean id="studentTwo" class="com.atguigu.spring.pojo.Student"> <!-- property:通过成员变量的set方法进行赋值 name:设置需要赋值的属性名(和set方法有关) value:设置为属性所赋的值 --> <property name="sid" value="1001"></property> <property name="sname" value="张三"></property> <property name="age" value="23"></property> <property name="gender" value="男"></property> </bean> <bean id="studentThree" class="com.atguigu.spring.pojo.Student"> <constructor-arg value="1002"></constructor-arg> <constructor-arg value="李四"></constructor-arg> <constructor-arg value="女"></constructor-arg> <constructor-arg value="24" name="age"></constructor-arg> </bean> <bean id="studentFour" class="com.atguigu.spring.pojo.Student"> <property name="sid" value="1003"></property> <!-- <:< >:> CDATA节其中的内容会原样解析<![CDATA[...]]> CDATA节是xml中一个特殊的标签,因此不能写在一个属性中 --> <!--<property name="sname" value="<王五>"></property>--> <property name="sname"> <value><![CDATA[<王五>]]></value> </property> <property name="gender"> <null /> </property> </bean> <bean id="studentFive" class="com.atguigu.spring.pojo.Student"> <property name="sid" value="1004"></property> <property name="sname" value="赵六"></property> <property name="age" value="26"></property> <property name="gender" value="男"></property> <!--ref:引用IOC容器中的某个bean的id--> <!--<property name="clazz" ref="clazzOne"></property>--> <!--级联的方式,要保证提前为clazz属性赋值或者实例化--> <!--<property name="clazz.cid" value="2222"></property> <property name="clazz.cname" value="远大前程班"></property>--> <property name="clazz"> <!--内部bean,只能在当前bean的内部使用,不能直接通过IOC容器获取--> <bean id="clazzInner" class="com.atguigu.spring.pojo.Clazz"> <property name="cid" value="2222"></property> <property name="cname" value="远大前程班"></property> </bean> </property> <property name="hobby"> <array> <value>抽烟</value> <value>喝酒</value> <value>烫头</value> </array> </property> <property name="teacherMap" ref="teacherMap"></property> <!--<property name="teacherMap"> <map> <entry key="10086" value-ref="teacherOne"></entry> <entry key="10010" value-ref="teacherTwo"></entry> </map> </property>--> </bean> <bean id="clazzOne" class="com.atguigu.spring.pojo.Clazz"> <property name="cid" value="1111"></property> <property name="cname" value="最强王者班"></property> <property name="students" ref="studentList"></property> <!--<property name="students"> <list> <ref bean="studentOne"></ref> <ref bean="studentTwo"></ref> <ref bean="studentThree"></ref> </list> </property>--> </bean> <bean id="teacherOne" class="com.atguigu.spring.pojo.Teacher"> <property name="tid" value="10086"></property> <property name="tname" value="大宝"></property> </bean> <bean id="teacherTwo" class="com.atguigu.spring.pojo.Teacher"> <property name="tid" value="10010"></property> <property name="tname" value="小宝"></property> </bean> <!--配置一个集合类型的bean,需要使用util的约束--> <util:list id="studentList"> <ref bean="studentOne"></ref> <ref bean="studentTwo"></ref> <ref bean="studentThree"></ref> </util:list> <util:map id="teacherMap"> <entry key="10086" value-ref="teacherOne"></entry> <entry key="10010" value-ref="teacherTwo"></entry> </util:map> <bean id="studentSix" class="com.atguigu.spring.pojo.Student" p:sid="1005" p:sname="小明" p:teacherMap-ref="teacherMap"></bean> </beans>
IOCByXMLTest
package com.atguigu.spring.test; import com.atguigu.spring.pojo.Clazz; import com.atguigu.spring.pojo.Person; import com.atguigu.spring.pojo.Student; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * Date:2022/7/1 * Author:ybc * Description: */ public class IOCByXMLTest { /** * 获取bean的三种方式: * 1、根据bean的id获取 * 2、根据bean的类型获取 * 注意:根据类型获取bean时,要求IOC容器中有且只有一个类型匹配的bean * 若没有任何一个类型匹配的bean,此时抛出异常:NoSuchBeanDefinitionException * 若有多个类型匹配的bean,此时抛出异常:NoUniqueBeanDefinitionException * 3、根据bean的id和类型获取 * 结论: * 根据类型来获取bean时,在满足bean唯一性的前提下 * 其实只是看:『对象 instanceof 指定的类型』的返回结果 * 只要返回的是true就可以认定为和类型匹配,能够获取到。 * 即通过bean的类型、bean所继承的类的类型、bean所实现的接口的类型都可以获取bean */ @Test public void testDI(){ //获取IOC容器 ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml"); //获取bean Student student = ioc.getBean("studentSix", Student.class); System.out.println(student); /*Clazz clazz = ioc.getBean("clazzInner", Clazz.class); System.out.println(clazz);*/ /*Clazz clazz = ioc.getBean("clazzOne", Clazz.class); System.out.println(clazz);*/ } @Test public void testIOC(){ //获取IOC容器 ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml"); //获取bean //Student studentOne = (Student) ioc.getBean("studentOne"); //Student student = ioc.getBean(Student.class); //Student student = ioc.getBean("studentOne", Student.class); Person person = ioc.getBean(Person.class); System.out.println(person); } }
使用util:list、util:map标签必须引入相应的命名空间,可以通过idea的提示功能选择
2.2.9、实验九:p命名空间
引入p命名空间后(需要导入相应的p约束-idea自动导入),可以通过以下方式为bean的各个属性赋值
-
和上面一样,如果是类类型则加ref引用bean id,字面量不用加
-
用的不多
-
在Spring中,p代表property,即Bean的属性。在XML配置文件中,使用p命名空间可以更方便地设置Bean的属性,使得XML配置更加简洁和易读。例如,使用p命名空间可以这样设置Bean的属性:
<bean id="studentSix" class="com.atguigu.spring.bean.Student" p:id="1006" p:name="小明" p:clazz-ref="clazzOne" p:teacherMap-ref="teacherMap"></bean>
2.2.10、实验十:引入外部属性文件
①加入依赖
-
数据源就是来统一管理数据库连接的,所以必须有MySQL驱动
-
加入德鲁伊依赖
-
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
还有很多属性可以配置,都有默认值,也可以不设置
-
property中initialSize是设置数据库连接池中初始化为我们连接的个数(默认值是0)
-
maxActive是当前数据库连接池中最大能够存在连接的数量(默认值是8)
如果我们只从这个数据源中获取连接而不关闭,也就是不将这些连接返回,最多只能获取8个,如果获取第九个时会一直出现堵塞状态
-
maxWait等待分配连接的最大等待时间,如果不设置就一直等待
<!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency> <!-- 数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.31</version> </dependency>
②创建外部属性文件
jdbc.user=root jdbc.password=atguigu jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver
③引入属性文件
-
如果导入错了context,一定要删除相应的约束,再导入,不然可能找不到
<!-- 引入外部属性文件 ,每一个新的内容都需要引入新的约束,直接写resources下面的配置文件名-和配置文件一个目录,类路径--> <context:property-placeholder location="classpath:jdbc.properties"/>
④配置bean
-
接口连构造方法都没,是不能写接口的
-
这里class写的是一个德鲁伊druid的实现类
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.user}"/> <property name="password" value="${jdbc.password}"/> </bean>
⑤测试
@Test public void testDataSource() throws SQLException { ApplicationContext ac = new ClassPathXmlApplicationContext("spring-datasource.xml"); DataSource dataSource = ac.getBean(DataSource.class); Connection connection = dataSource.getConnection(); System.out.println(connection); }
2.2.11、实验十一:bean的作用域
作用域(作用范围)
在IOC容器中,我们获取的这个bean的对象始终为单实例模式
①概念
在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表:
取值 | 含义 | 创建对象的时机 |
---|---|---|
singleton(默认) | 在IOC容器中,这个bean的对象始终为单实例 | IOC容器初始化时 |
prototype | 这个bean在IOC容器中有多个实例 | 获取bean时 |
如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用):
取值 | 含义 |
---|---|
request | 在一个请求范围内有效 |
session | 在一个会话范围内有效 |
②创建类User
public class User { private Integer id; private String username; private String password; private Integer age; public User() { } public User(Integer id, String username, String password, Integer age) { this.id = id; this.username = username; this.password = password; this.age = age; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + '}'; } }
③配置bean
<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建 对象 --> <!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 --> <bean class="com.atguigu.bean.User" scope="prototype"></bean>
④测试
-
单例就是这个类有且只会有有一个实例对象,大家用的都是同一个
-
设置了scope="prototype"(原型、多例的意思)
<bean class="com.atguigu.bean.User" scope="prototype"></bean>就不是单例了
@Test public void testBeanScope(){ ApplicationContext ac = new ClassPathXmlApplicationContext("spring-scope.xml"); User user1 = ac.getBean(User.class); User user2 = ac.getBean(User.class); System.out.println(user1==user2);//true--证明默认是单例模式,设置scope="prototype"就是false }
2.2.12、实验十二:bean的生命周期
①具体的生命周期过程
-
bean对象创建(调用无参构造器实例化对象)
-
给bean对象设置属性
-
bean对象初始化之前操作(由bean的后置处理器负责)
-
bean对象初始化(需在配置bean时指定初始化方法:init-method属性)
-
bean对象初始化之后操作(由bean的后置处理器负责)
-
bean对象就绪可以使用
-
bean对象销毁(需在配置bean时指定销毁方法)
-
IOC容器关闭
生命周期
同上:重点摘要
-
实例化(Instantiation):当Spring容器接收到一个Bean的定义时,会创建该Bean的一个实例。
-
属性赋值(Properties Setting):在Bean实例化后,Spring容器会根据Bean定义中的配置,将属性值或依赖注入到Bean中。
-
初始化(Initialization):在Bean属性赋值完成后,Spring容器会调用Bean的初始化方法,例如实现了InitializingBean接口的afterPropertiesSet()方法或者在Bean定义中配置的init-method方法。
-
使用(In Use):Bean初始化完成后,可以被应用程序使用。
-
销毁(Destruction):当应用程序关闭时,Spring容器会调用Bean的销毁方法,例如实现了DisposableBean接口的destroy()方法或者在Bean定义中配置的destroy-method方法。
disposable-disposable,adj.一次性的,用完即丢弃的;可支配的,可自由使用的;(人,观点)可有可无的,可轻易放弃的
n.一次性用品
IOC销毁的时候查到里面的bean还在,就销毁bean,然后把bean全部销毁完后才IOC容器才关闭
②修改类User
public class User { private Integer id; private String username; private String password; private Integer age; public User() { System.out.println("生命周期:1、创建对象"); } public User(Integer id, String username, String password, Integer age) { this.id = id; this.username = username; this.password = password; this.age = age; } public Integer getId() { return id; } public void setId(Integer id) { System.out.println("生命周期:2、依赖注入(也就是赋值-随便找一个属性演示了)"); this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public void initMethod(){ System.out.println("生命周期:3、初始化"); } public void destroyMethod(){ System.out.println("生命周期:5、销毁(4是使用,我们这里demo了)"); } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + '}'; } }
注意其中的initMethod() 和 destroyMethod(),可以通过配置bean指定为初始化和销毁的方法
③配置bean
-
指定之后才可以被调用
-
注意destory用ApplicationContext不能被调用,需要更换IOC容器,有close的方法容器才可以(关闭时候才能摧毁)
-
用 ClassPathXmlApplicationContext或ConfigurableApplicationContext才有ioc.close方法(单例)
<!-- 使用init-method属性指定初始化方法 --> <!-- 使用destroy-method属性指定销毁方法 --> <bean class="com.atguigu.bean.User" scope="prototype" init-method="initMethod" destroy-method="destroyMethod"> <property name="id" value="1001"></property> <property name="username" value="admin"></property> <property name="password" value="123456"></property> <property name="age" value="23"></property> </bean>
④测试
@Test public void testLife(){ ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-lifecycle.xml"); User bean = ac.getBean(User.class); System.out.println("生命周期:4、通过IOC容器获取bean并使用"); ac.close(); }
-
通过IOC容器获取bean时候,获取的永远都是唯一的一个对象,我们没必要在获取它的时候创建,在获取IOC容器时候就已把它给创建好,以后用的都是同一个
实例化,依赖注入,初始化在获取IOC容器时候就执行了,不是在获取bean的时候执行的)
-
IOC容器中配置的bean默认作用域是单例
-
如果设置bean中的scope为prototype多例(多例每一次获取对象都是新的对象,IOC没必要在创建容器时候就把它给创建好)则不会执行生命周期(在获取getBean的时候才会进行初始化一系列操作,输出以上内容-多例不会执行destory)
-
设置为多例的时候,它的destory方法就不由我们的IOC容器管理了(bean里面配置了摧毁也不会destory,会前三个生命周期)
解疑
同学你好,当设置Bean的作用域为prototype时,销毁方法会出现冲突。所以destroy方法没有被执行。
因为当作用域为prototype时,Spring IOC容器不能够对Bean的整个生命周期进行管理,最终对象的销毁和资源回收由使用者负责
-
按我理解的是,当作用域为prototype时,Spring容器不会Bean的整个生命周期进行管理,它只是负责new一个对象出来,当你使用完该对象后会自动销毁或者等待回收,如果作用域换成singleton,它的生命周期就由Spring容器负责
⑤bean的后置处理器
bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,
且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
创建bean的后置处理器:
-
默认方法不是抽象方法,所以不强制被重写。但是可以被重写,重写的时候去掉default关键字
-
idea -ctrl + o重写方法
package com.atguigu.spring.process; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; public class MyBeanProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { //此方法在bean的生命周期初始化之前执行 System.out.println("☆☆☆" + beanName + " = " + bean); //所以这里返回的是bean额外操作之后的对象,不进行额外操作,直接返回 return bean也行,beanName就是bean的id return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // System.out.println("★★★" + beanName + " = " + bean); return bean; } }
在IOC容器中配置后置处理器:
<!-- bean的后置处理器要放入IOC容器才能生效 -->
bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
因为我们直接把bean配置到了IOC容器中,不是针对于某一个bean(所有的都会生命周期都会加上)
<bean id="myBeanProcessor" class="com.atguigu.spring.process.MyBeanProcessor"/>
2.2.13、实验十三:FactoryBean
①简介
前面我们讲过了BeanFactory 是BeanFactory而不是FactoryBean
BeanFactory就是IOC最基本的实现,帮我们管理Bean
FactoryBean是专门作为一个Bean交给IOC容器管理
BeanFactory 是 Spring IoC 容器的基础,用于管理 Bean 对象的创建和生命周期,而 FactoryBean 是一个特殊的 Bean,用于定制 Bean 的创建过程。
BeanFactory 可以管理多种类型的 Bean 对象,而 FactoryBean 只能创建一种特定类型的 Bean 对象。
BeanFactory 可以通过配置文件或注解来创建 Bean 对象,而 FactoryBean 可以在创建 Bean 之前进行一些初始化操作或返回一个代理对象。
CSDN:进一步理解- BeanFactory和FactoryBean的区别_factorybean和beanfactory的区别-CSDN博客
FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个
FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是
getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
-
如果我们配置FactoryBean到IOC容器中,虽然我们配置的是FactoryBean类型,但是我们获取时候,可以直接获取工厂所提供的对象
将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。
/* * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.beans.factory; import org.springframework.lang.Nullable; /** * Interface to be implemented by objects used within a {@link BeanFactory} which * are themselves factories for individual objects. If a bean implements this * interface, it is used as a factory for an object to expose, not directly as a * bean instance that will be exposed itself. * * <p><b>NB: A bean that implements this interface cannot be used as a normal bean.</b> * A FactoryBean is defined in a bean style, but the object exposed for bean * references ({@link #getObject()}) is always the object that it creates. * * <p>FactoryBeans can support singletons and prototypes, and can either create * objects lazily on demand or eagerly on startup. The {@link SmartFactoryBean} * interface allows for exposing more fine-grained behavioral metadata. * * <p>This interface is heavily used within the framework itself, for example for * the AOP {@link org.springframework.aop.framework.ProxyFactoryBean} or the * {@link org.springframework.jndi.JndiObjectFactoryBean}. It can be used for * custom components as well; however, this is only common for infrastructure code. * * <p><b>{@code FactoryBean} is a programmatic contract. Implementations are not * supposed to rely on annotation-driven injection or other reflective facilities.</b> * {@link #getObjectType()} {@link #getObject()} invocations may arrive early in the * bootstrap process, even ahead of any post-processor setup. If you need access to * other beans, implement {@link BeanFactoryAware} and obtain them programmatically. * * <p><b>The container is only responsible for managing the lifecycle of the FactoryBean * instance, not the lifecycle of the objects created by the FactoryBean.</b> Therefore, * a destroy method on an exposed bean object (such as {@link java.io.Closeable#close()} * will <i>not</i> be called automatically. Instead, a FactoryBean should implement * {@link DisposableBean} and delegate any such close call to the underlying object. * * <p>Finally, FactoryBean objects participate in the containing BeanFactory's * synchronization of bean creation. There is usually no need for internal * synchronization other than for purposes of lazy initialization within the * FactoryBean itself (or the like). * * @author Rod Johnson * @author Juergen Hoeller * @since 08.03.2003 * @param <T> the bean type * @see org.springframework.beans.factory.BeanFactory * @see org.springframework.aop.framework.ProxyFactoryBean * @see org.springframework.jndi.JndiObjectFactoryBean */ public interface FactoryBean<T> { /** * The name of an attribute that can be * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a * {@link org.springframework.beans.factory.config.BeanDefinition} so that * factory beans can signal their object type when it can't be deduced from * the factory bean class. * @since 5.2 */ String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType"; /** * Return an instance (possibly shared or independent) of the object * managed by this factory. * <p>As with a {@link BeanFactory}, this allows support for both the * Singleton and Prototype design pattern. * <p>If this FactoryBean is not fully initialized yet at the time of * the call (for example because it is involved in a circular reference), * throw a corresponding {@link FactoryBeanNotInitializedException}. * <p>As of Spring 2.0, FactoryBeans are allowed to return {@code null} * objects. The factory will consider this as normal value to be used; it * will not throw a FactoryBeanNotInitializedException in this case anymore. * FactoryBean implementations are encouraged to throw * FactoryBeanNotInitializedException themselves now, as appropriate. * @return an instance of the bean (can be {@code null}) * @throws Exception in case of creation errors * @see FactoryBeanNotInitializedException */ @Nullable T getObject() throws Exception; /** * Return the type of object that this FactoryBean creates, * or {@code null} if not known in advance. * <p>This allows one to check for specific types of beans without * instantiating objects, for example on autowiring. * <p>In the case of implementations that are creating a singleton object, * this method should try to avoid singleton creation as far as possible; * it should rather estimate the type in advance. * For prototypes, returning a meaningful type here is advisable too. * <p>This method can be called <i>before</i> this FactoryBean has * been fully initialized. It must not rely on state created during * initialization; of course, it can still use such state if available. * <p><b>NOTE:</b> Autowiring will simply ignore FactoryBeans that return * {@code null} here. Therefore it is highly recommended to implement * this method properly, using the current state of the FactoryBean. * @return the type of object that this FactoryBean creates, * or {@code null} if not known at the time of the call * @see ListableBeanFactory#getBeansOfType */ @Nullable Class<?> getObjectType(); /** * Is the object managed by this factory a singleton? That is, * will {@link #getObject()} always return the same object * (a reference that can be cached)? * <p><b>NOTE:</b> If a FactoryBean indicates to hold a singleton object, * the object returned from {@code getObject()} might get cached * by the owning BeanFactory. Hence, do not return {@code true} * unless the FactoryBean always exposes the same reference. * <p>The singleton status of the FactoryBean itself will generally * be provided by the owning BeanFactory; usually, it has to be * defined as singleton there. * <p><b>NOTE:</b> This method returning {@code false} does not * necessarily indicate that returned objects are independent instances. * An implementation of the extended {@link SmartFactoryBean} interface * may explicitly indicate independent instances through its * {@link SmartFactoryBean#isPrototype()} method. Plain {@link FactoryBean} * implementations which do not implement this extended interface are * simply assumed to always return independent instances if the * {@code isSingleton()} implementation returns {@code false}. * <p>The default implementation returns {@code true}, since a * {@code FactoryBean} typically manages a singleton instance. * @return whether the exposed object is a singleton * @see #getObject() * @see SmartFactoryBean#isPrototype() */ //证明FactoryBean是单例 default boolean isSingleton() { return true; } }
②创建类UserFactoryBean
public class UserFactoryBean implements FactoryBean<User> { @Override public User getObject() throws Exception { return new User(); } @Override public Class<?> getObjectType() { return User.class; } }
我们实现FactoryBean时候它默认让我们实现两个方法,证明另外的一定有一个默认方法
③配置bean
-
不需要配置User了
-
不需要配置普通的工厂
-
当我们加载IOC配置文件去获取IOC容器时候,它其实是把UserFactoryBean中的getObject()方法所返回的对象给了IOC容器
-
这样可以达到不配做User,却能够使用User(FactoryBean的getObject方法所返回的对象交给ioc容器)
<bean id="user" class="com.atguigu.bean.UserFactoryBean"></bean>
④测试
-
会发现实例化,并没有依赖注入为我们属性赋值,所以生命周期2初始化并没有
@Test public void testUserFactoryBean(){ //获取IOC容器 ApplicationContext ac = new ClassPathXmlApplicationContext("spring-factorybean.xml"); User user = ac.getBean(User.class); System.out.println(user); }
public class UserFactoryBean implements FactoryBean<User> { @Override public User getObject() throws Exception { return new User(); } @Override public Class<?> getObjectType() { return User.class; } }
FactoryBean是一个接口,需要创建一个类实现该接口
-
其中有三个方法:
-
getObject():通过一个对象交给IOC容器管理
-
getObjectType():设置所提供对象的类型
-
isSingleton():所提供的对象是否单例
-
当把FactoryBean的实现类配置为bean时,会将当前类中getObject()所返回的对象交给IOC容器管理
BeanFactory和FactoryBean的区别
-
BeanFactory是Spring框架的核心接口之一,而FactoryBean是一个接口。
-
BeanFactory负责创建和管理Spring容器中的对象,而FactoryBean是用于创建其他Bean实例的特殊Bean。
-
当我们在配置文件中定义一个FactoryBean时,实际上创建的是FactoryBean本身,而不是它所创建的对象。
使用场景
BeanFactory适用于以下场景:
-
需要轻量级的Spring容器,不需要使用Spring提供的大部分扩展功能,只需要基本的Bean容器功能。
-
需要手动控制Bean的创建和销毁,例如在某些特定的场景下需要动态地创建和销毁Bean实例。
-
需要定制Bean的生命周期,例如需要在Bean实例化之前或销毁之后进行特定的处理。
FactoryBean适用于以下场景:
-
需要创建复杂的Bean实例,例如需要根据不同的条件创建不同的Bean实例。
-
需要在Bean实例化之前或销毁之后进行特定的处理,例如需要在Bean实例化之前进行一些预处理操作。
-
需要对Bean实例进行代理,例如需要为Bean实例添加事务或安全控制等功能。
-
需要在Spring容器中动态地创建Bean实例,例如需要在应用程序运行时根据某些条件创建Bean实例。
在实际开发中,我们可以根据具体的需求选择BeanFactory或FactoryBean来创建和管理Bean实例。
2.2.14、实验十四:基于xml的自动装配
补充命名规则:
-
这里接口就叫什么什么Service(Controller),如UserService,其它地方规则一样
-
实现类的话在后面加上impl
自动装配:
根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类
-
我们不需要自己写property了
型属性赋值
-
给IOC容器赋值的时候,其实只需要找到IOC容器所对应的Bean对象,通过ref来引用这个Bean的id,我们就可以把这个Bean所对应的对象来为类类型或者接口类型的属性赋值
三层架构:表述层、业务层、持久层
-
Service是业务层,Dao是数据访问层,这样的分层是基于MVC架构来说的,分层的主要作用是解耦。 对于Spring这样的框架,(View\Web)表示层调用控制层(Controller),控制层调用业务层(Service),业务层调用数据访问层(Dao)
-
学了框架就不需要用Servlet了,所以说我们控制层就叫做什么什么Controller了
①场景模拟
-
我们后期学SpringMVC时候控制层是SpringMVC所封装的一个servlet来统一访问的,下面是模拟,自己访问UserController
-
学了IOC,我们就可以把类交给IOC管理,解耦合
-
控制层调用Service,Service层调用Dao层实现持久化操作
(这么做的意思就是:三层架构只做自己该做的事,不要越级去干别的层该做的事!!)
-
要把下面这些交给IOC容器管理
也要把它们之间的关系教给IOC容器
-
autowire就是自动装配的意思
-
bean里面的Class不能写接口!一定要写具体类型
-
别忘记创建无参构造!
创建类UserController
public class UserController { private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } public void saveUser(){ userService.saveUser(); } }
创建接口UserService
public interface UserService { void saveUser(); }
创建类UserServiceImpl实现接口UserService
public class UserServiceImpl implements UserService { private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public void saveUser() { userDao.saveUser(); } }
创建接口UserDao
public interface UserDao { void saveUser(); }
创建类UserDaoImpl实现接口UserDao
public class UserDaoImpl implements UserDao { @Override public void saveUser() { System.out.println("保存成功"); } }
测试
public class FactoryBeanTest { @Test public void testFactoryBean(){ ApplicationContext ioc=new ClassPathXmlApplicationContext("spring-autowire-xml.xml"); UserController user = ioc.getBean(UserController.class); user.saveUser(); } }
spring-autowire-xml.xml
-
实现自动装配后下面的property就不用写了
<?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"> <!--把三层架构的三个主键交给IOC管理了,接下来还要管理它们的依赖关系--> <bean id="userController" class="com.atguigu.spring.controller.UserController"> <property name="userService" ref="userService"></property> </bean> <bean id="userService" class="com.atguigu.spring.service.impl.UserServiceImpl"> <!--Service里面还要管理Dao--> <property name="userDao" ref="userDao"></property> </bean> <bean id="userDao" class="com.atguigu.spring.dao.impl.UserDaoImpl"></bean> </beans>
②配置bean
使用bean标签的autowire属性设置自动装配效果
自动装配方式:byType
byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值
若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值
null
若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常
NoUniqueBeanDefinitionException
UserController中有private UserService userService; userService这是一个接口(根据类型会给userService赋值)
UserServiceImp中有private UserDao userDao; userDao这是一个接口(UserServiceImple能和userService匹配-接口和实现类)
UserDaoImpl中有覆盖了UserDao中的saveUser()方法
<bean id="userController" class="com.atguigu.autowire.xml.controller.UserController" autowire="byType"> </bean> <bean id="userService" class="com.atguigu.autowire.xml.service.impl.UserServiceImpl" autowire="byType"> </bean> <bean id="userDao" class="com.atguigu.autowire.xml.dao.impl.UserDaoImpl"></bean>
自动装配方式:byName
byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值
<bean id="userController"class="com.atguigu.autowire.xml.controller.UserController" autowire="byName"> </bean> <bean id="userService"class="com.atguigu.autowire.xml.service.impl.UserServiceImpl" autowire="byName"> </bean> <bean id="userServiceImpl"class="com.atguigu.autowire.xml.service.impl.UserServiceImpl" autowire="byName"> </bean> <bean id="userDao" class="com.atguigu.autowire.xml.dao.impl.UserDaoImpl"> </bean> <bean id="userDaoImpl" class="com.atguigu.autowire.xml.dao.impl.UserDaoImpl"> </bean>
③测试
@Test public void testAutoWireByXML(){ ApplicationContext ac = new ClassPathXmlApplicationContext("autowire-xml.xml"); UserController userController = ac.getBean(UserController.class); userController.saveUser(); }
自动装配总结:
public class AutowireByXMLTest { /** * 自动装配: * 根据指定的策略,在IOC容器中匹配某个bean(是bean,不是属性!),自动为bean中的类类型的属性或接口类型的属性赋值 * 可以通过bean标签中的autowire属性设置自动装配的策略 * 自动装配的策略: * 1、no,default:表示不装配,即bean中的属性不会自动匹配某个bean为属性赋值,此时属性使用默认值成员变量的默认值) * 2、byType:根据要赋值的属性的类型,在IOC容器中匹配某个bean,为属性赋值 * 注意: * a>若通过类型没有找到任何一个类型匹配的bean,此时不装配,属性使用默认值,默认值null则报空指针异常 * b>若通过类型找到了多个类型匹配的bean,此时会抛出异常:NoUniqueBeanDefinitionException * 总结:当使用byType实现自动装配时,IOC容器中有且只有一个类型匹配的bean能够为属性赋值 * 3、byName:将要赋值的属性的属性名作为bean的id在IOC容器中匹配某个bean,为属性赋值(很少用到) * 总结:当类型匹配的bean有多个时,此时可以使用byName实现自动装配(当byType不能用的时候再考虑byName) */ @Test public void testAutowire(){ ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-autowire-xml.xml"); UserController userController = ioc.getBean(UserController.class); userController.saveUser(); } }
自动装配autowire用的很多,但是基于xml的自动装配用的不多,因为配置到xml中是针对于我们所有的bean类型的属性和接口类型的属性
我们如果想要为其中的某些属性赋值是做不到的,它都会通过自动装配来找到bean为其赋值
所以我们延伸了基于注解的方式,想要自动装配在其成员变量上加上注解即可
2.3、基于注解管理bean
2.3.1、实验一:标记与扫描
①注解
和 XML 配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测
到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。
本质上:所有一切的操作都是Java代码来完成的,XML和注解只是告诉框架中的Java代码如何执行。
举例:元旦联欢会要布置教室,蓝色的地方贴上元旦快乐四个字,红色的地方贴上拉花,黄色的地方贴上气球。
班长做了所有标记,同学们来完成具体工作。墙上的标记相当于我们在代码中使用的注解,后面同学们做的工作,相当于框架的具体操作。
②扫描
Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。
-
加了注解就是实现相对应的功能,没加则不执行
-
单单加上注解还不行,还需要扫描
-
比如根据注解自动配置相应的bean标签
③新建Maven Module
<dependencies> <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies>
④创建Spring配置文件
⑤标识组件的常用注解
-
IOC容器中根标签是beans,beans由多个bean组成,每一个bean都是IOC中的一个组件
-
加一个注解就相当于配置了一个bean,不能加载接口上面
@Component:将类标识为普通组件
@Controller:将类标识为控制层组件
@Service:将类标识为业务层组件
@Repository:将类标识为持久层组件
问:以上四个注解有什么关系和区别?
通过查看源码我们得知,@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。(功能都是将一个类表识为一个组件,只不过各种的含义不一样)
对于Spring使用IOC容器管理这些组件来说没有区别。所以@Controller、@Service、@Repository这
三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。
注意:虽然它们本质上一样,但是为了代码的可读性,为了程序结构严谨我们肯定不能随便胡乱标记。
-
注意注解在类上面,而不是接口上面,创建bean时候class对应的不是接口!
⑥创建组件
创建控制层组件
@Controller public class UserController { }
创建接口UserService
public interface UserService { }
创建业务层组件UserServiceImpl
@Service public class UserServiceImpl implements UserService { }
创建接口UserDao
public interface UserDao { }
创建持久层组件UserDaoImpl
@Repository public class UserDaoImpl implements UserDao { }
⑦扫描组件
拓展:spring是要和springMVC一起使用的,SpringMVC扫描的是控制层,Spring扫描的是控制层以外的所有的组件
不能让Spring来扫描控制层,不然会造成多次扫描
情况一:最基本的扫描方式
-
base-package扫描包下面的组件
-
将这些类作为组件交给IOC容器来作为组件进行管理
-
也可以中间加,逗号来指定扫描的组件
<context:component-scan base-package="com.atguigu"> </context:component-scan>
情况二:指定要排除的组件
<context:component-scan >下面有两个子标签 context:exclude-filter,context:include-filter
<context:component-scan base-package="com.atguigu"> <!-- context:exclude-filter标签:指定排除规则 --> <!-- type:设置排除或包含的依据 type="annotation",根据注解排除,expression中设置要排除的【注解】的全类名 下面org开头的 type="assignable",根据类型排除,expression中设置要排除的【类型】的全类名 下面包名开头的 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> <!--<context:exclude-filter type="assignable"expression="com.atguigu.controller.UserController"/>--> </context:component-scan>
情况三:仅扫描指定组件
仅扫描谁
<context:component-scan base-package="com.atguigu" use-default-filters="false"> <!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 --> <!-- use-default-filters属性:取值false表示关闭默认扫描规则 --> <!-- 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类,父标签肯定大于子标签 --> <!-- 用的多的还是exclude-filter 如果仅扫描还不如在context:component-scan上直接写呢--> <!-- type:设置排除或包含的依据 type="annotation",根据注解排除,expression中设置要排除的注解的全类名 type="assignable",根据类型排除,expression中设置要排除的类型的全类名 --> <context:include-filter type="annotation"expression="org.springframework.stereotype.Controller"/> <!--<context:include-filter type="assignable"expression="com.atguigu.controller.UserController"/>--> </context:component-scan>
⑧测试
@Test public void testAutowireByAnnotation(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); UserController userController = ac.getBean(UserController.class); System.out.println(userController); UserService userService = ac.getBean(UserService.class); System.out.println(userService); UserDao userDao = ac.getBean(UserDao.class); System.out.println(userDao); }
⑨组件所对应的bean的id
在我们使用XML方式管理bean的时候,每个bean都有一个唯一标识,便于在其他地方引用。现在使用
注解后,每个组件仍然应该有一个唯一标识。
默认情况
类名首字母小写就是bean的id。例如:UserController类对应的bean的id就是userController(类的小驼峰)。
自定义bean的id
可通过标识组件的注解的value属性设置自定义的bean的id
@Service("userService")
//默认为userServiceImpl public class UserServiceImpl implements
UserService {}
一般不会去指定它的id,我们都是根据类名获取bean的,在SpringMVC中有个文件上传信息中需要加id,不加id和没配置是一样的
2.3.2、实验二:基于注解的自动装配
①场景模拟
参考基于xml的自动装配
在UserController中声明UserService对象
在UserServiceImpl中声明UserDao对象
②@Autowired注解
在成员变量上直接标记@Autowired注解即可完成自动装配,注解装配不需要提供setXxx()方法。以后我们在项
目中的正式用法就是这样。
@Controller public class UserController { @Autowired private UserService userService; public void saveUser(){ userService.saveUser(); } }
public interface UserService { void saveUser(); } @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public void saveUser() { userDao.saveUser(); } }
public interface UserDao { void saveUser(); }
@Repository public class UserDaoImpl implements UserDao { @Override public void saveUser() { System.out.println("保存成功"); } }
③@Autowired注解其他细节
@Autowired注解可以标记在成员变量(添加成员变量上set就不需要创建了)构造器和set方法上
建议使用成员变量这一种即可
@Controller public class UserController { private UserService userService; @Autowired public UserController(UserService userService){ this.userService = userService; } public void saveUser(){ userService.saveUser(); } }
@Controller public class UserController { private UserService userService; @Autowired public void setUserService(UserService userService){ this.userService = userService; } public void saveUser(){ userService.saveUser(); } }
④@Autowired工作流程
-
默认情况注解的类名首字母小写就是bean的id
-
首先根据所需要的组件类型到IOC容器中查找
-
能够找到唯一的bean:直接执行装配
-
如果完全找不到匹配这个类型的bean:装配失败
-
和所需类型匹配的bean不止一个
-
没有@Qualifier注解:根据@Autowired标记位置成员变量的变量名作为bean的id进行匹配
-
能够找到:执行装配
-
找不到:装配失败
-
使用@Qualifier注解:根据@Qualifier注解中指定的名称作为bean的id进行匹配
-
能够找到:执行装配
-
找不到:装配失败
-
-
@Controller public class UserController { @Autowired @Qualifier("userServiceImpl") private UserService userService; public void saveUser(){ userService.saveUser(); } }
@Autowired中有属性required,默认值为true,因此在自动装配无法找到相应的bean时,会装配失败
可以将属性required的值设置为true,则表示能装就装,装不上就不装,此时自动装配的属性为
默认值
但是实际开发时,基本上所有需要装配组件的地方都是必须装配的,用不上这个属性。
拓展:
@Qualifier("userServiceImpl") 是 Spring 框架中的一个注解,用于指定注入 Bean 的名称。当一个接口有多个实现类时,使用 @Qualifier 注解可以指定要注入的 Bean 的名称,避免出现歧义。 在 Spring 中,当一个接口有多个实现类时,使用 @Autowired 注解自动注入 Bean 时会出现歧义,因为 Spring 不知道要注入哪个实现类的 Bean。这时可以使用 @Qualifier 注解指定要注入的 Bean 的名称。
例如,假设有一个 UserService 接口和两个实现类 UserServiceImpl1 和 UserServiceImpl2,如果要在另一个类中注入 UserService 的 Bean,可以使用以下代码:
@Autowired @Qualifier("userServiceImpl1") private UserService userService;这样就可以指定要注入的 UserService 的实现类为 UserServiceImpl1,避免了歧义。
需要注意的是,@Qualifier 注解需要与 @Autowired 注解一起使用,@Qualifier 注解指定要注入的 Bean 的名称,@Autowired 注解实现自动注入。
总之,@Qualifier("userServiceImpl") 注解是 Spring 框架中的一个注解,用于指定注入
IOCByAnnotationTest.java
package com.atguigu.spring.test; import com.atguigu.spring.controller.UserController; import com.atguigu.spring.dao.UserDao; import com.atguigu.spring.service.UserService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class IOCByAnnotationTest { /** * @Component:将类标识为普通组件 * @Controller:将类标识为控制层组件 * @Service:将类标识为业务层组件 * @Repository:将类标识为持久层组件 * * 通过注解+扫描所配置的bean的id,默认值为类的小驼峰,即类名的首字母为小写的结果(使用的是小驼峰命名法,则在查找匹配的Bean时,Spring会自动将小驼峰形式的Bean名称转换为大驼峰形式,为了清晰、便于理解) * 使用@Component注解标记一个类为Bean时,可以使用value属性来指定Bean的名称,如果不指定名称,则默认使用类名的小驼 * 峰形式作为Bean的名称。例如: * @Component(value = "userService") * public class UserServiceImpl implements UserService { ... * } * 可以通过标识组件的注解的value属性值设置bean的自定义的id * * @Autowired:实现自动装配功能的注解 * 1、@Autowired注解能够标识的位置 * a>标识在成员变量上,此时不需要设置成员变量的set方法 * b>标识在set方法上 * c>标识在为当前成员变量赋值的有参构造上 * 2、@Autowired注解的原理 * a>默认通过byType的方式,在IOC容器中通过类型匹配某个bean为属性赋值(想一想通过byName也不可能,注解默认配置的id是类的小驼峰,和类名不匹配) * 已经扫描过了又配置的bean * b>若有多个相同类型匹配的bean(同class),此时会自动转换为byName的方式实现自动装配的效果 * 即将要赋值的属性的属性名作为bean的id匹配IOC容器中的bean为属性赋值 * c>若byType和byName的方式都无妨实现自动装配,即IOC容器中有多个类型匹配的bean * 且这些bean的id和要赋值的属性的属性名都不一致,此时抛异常:NoUniqueBeanDefinitionException * d>此时可以在要赋值的属性上,添加一个注解@Qualifier(以后不会有那么多情况) * 通过该注解的value属性值,指定某个bean的id,将这个bean为属性赋值(这个是通过byName赋值的,因为有多个同类型的bean) * * 注意:若IOC容器中没有任何一个类型匹配的bean,此时抛出异常:NoSuchBeanDefinitionException * 在@Autowired注解中有个属性required,默认值为true,要求必须完成自动装配 * 可以将required设置为false,此时能装配则装配,无法装配则使用属性的默认值 */ @Test public void test(){ ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc-annotation.xml"); UserController userController = ioc.getBean("controller", UserController.class); /*System.out.println(userController); UserService userService = ioc.getBean("userServiceImpl", UserService.class); System.out.println(userService); UserDao userDao = ioc.getBean("userDaoImpl", UserDao.class); System.out.println(userDao);*/ userController.saveUser(); } }
报错分析:如果自动装配没有找到,在类上没有注解的话,会报异常NoSuchBeanDefinitionException(在自动装配过程中每没有找到任何一个能够匹配的bean),因为默认required=true,改为false之后,报空指针异常(因为不装配使用的是成员变量的默认值-这里是NULL)
-
UserServiceImpl实现类没有加注解
IOC基于XML和基于注解管理bean都要熟练掌握
第三方jar包中的类交给IOC容器来管理,这个时候不能用注解,因为jar包里面放的都是class字节码文件,看到的都是idea反编译之后的结果,这是改不了的,是加不了注解的,我们只能通过XML方式
拓展:
说成员变量是属性是可以的,但实际上属性是和get、set方法有关的
3、AOP
AOP也是以IOC容器为基础的-面向方面(切面)编程(Aspect-Oriented Programming)
是OOP的一种补充和完善
spring最重要的核心肯定是IOC,这个也是在IOC基础上实现的
3.1、场景模拟
3.1.1、声明接口
声明计算器接口Calculator,包含加减乘除的抽象方法
public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); }
3.1.2、创建实现类
package com.atguigu.spring.proxy; import javax.sql.rowset.CachedRowSet; /** * @author 即兴小索奇 * @version 1.0 * @date 22/12/31 20:30 * @description */ public class CalculatorImpl implements Calculator { @Override public int add(int i, int j) { System.out.println("日志,方法:add,参数:"+i+","+j); int result = i + j; System.out.println("方法内部 result = " + result); System.out.println("日志,方法:add,结果:"+result); return result; } @Override public int sub(int i, int j) { System.out.println("日志,方法:sub,参数:"+i+","+j); int result = i - j; System.out.println("方法内部 result = " + result); System.out.println("日志,方法:int,结果:"+result); return result; } @Override public int mul(int i, int j) { System.out.println("日志,方法:mul,参数:"+i+","+j); int result = i * j; System.out.println("方法内部 result = " + result); System.out.println("日志,方法:mul,结果:"+result); return result; } @Override public int div(int i, int j) { System.out.println("日志,方法:div,参数:"+i+","+j); int result = i / j; System.out.println("方法内部 result = " + result); System.out.println("日志,方法:div,结果:"+result); return result; } }
3.1.3、创建带日志功能的实现类
public class CalculatorLogImpl implements Calculator { @Override public int add(int i, int j) { System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); int result = i + j; System.out.println("方法内部 result = " + result); System.out.println("[日志] add 方法结束了,结果是:" + result); return result; } @Override public int sub(int i, int j) { System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j); int result = i - j; System.out.println("方法内部 result = " + result); System.out.println("[日志] sub 方法结束了,结果是:" + result); return result; } @Override public int mul(int i, int j) { System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j); int result = i * j; System.out.println("方法内部 result = " + result); System.out.println("[日志] mul 方法结束了,结果是:" + result); return result; } @Override public int div(int i, int j) { System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j); int result = i / j; System.out.println("方法内部 result = " + result); System.out.println("[日志] div 方法结束了,结果是:" + result); return result; } }
3.1.4、提出问题
①现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
-
对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
-
附加功能分散在各个业务功能方法中,不利于统一维护
②解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
③困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决(以前面向对象是纵向继承机制,只能把一段连续的代码进行封装)。所以需要引入新的技术。
3.2、代理模式
3.2.1、概念
①介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦(剥离到了代理类中)。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
使用代理后:
②生活中的代理
-
广告商找大明星拍广告需要经过经纪人
-
合作伙伴找大老板谈合作要约见面时间需要经过秘书
-
房产中介是买卖双方的代理
③相关术语
-
代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
-
目标:被代理“套用”了非核心逻辑代码的类、对象、方法。(这里是Calculator)
代理分为静态代理和动态代理
静态代理就是一对一的,一个目标对象对应一个代理对象
3.2.2、静态代理
创建静态代理类:
public class CalculatorStaticProxy implements Calculator { // 将被代理的目标对象声明为成员变量(声明实现类或者接口都是可以的) private CalculatorImpl target; public CalculatorStaticProxy(CalculatorImpl target) { this.target = target; } @Override public int add(int i, int j) { // 附加功能由代理类中的代理方法来实现 System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); // 通过目标对象来实现核心业务逻辑 int addResult = target.add(i, j); System.out.println("[日志] add 方法结束了,结果是:" + addResult); return addResult; } } public class ProxyTest { @Test public void testProxy(){ CalculatorStaticProxy staticProxy=new CalculatorStaticProxy(new CalculatorImpl()); staticProxy.add(1,3); } }
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来
说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代
码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理
类来实现。这就需要使用动态代理技术了。
3.2.3、动态代理
动态代理有两种:
一种是jdk动态代理(要求必须有接口,生成的代理类和目标类实现相同的接口,最终生成的代理类在com.sun.proxy包下,类名为$proxy加一个数字)
(AOP底层就是动态代理实现的,我们自己写的不多)
一种是cglib动态代理(没有接口时)最终生成的代理类会继承目标类,并且和目标类在相同的包下(这个扫描的时候能扫描到,后面可能会用到)
指的是动态生成目标类对应的代理类
生产代理对象的工厂类:
拓展: - getMethods(): 获得类的public类型的方法
getMethod(String name, Class[] params): 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型(null和不写都调用无参的方法)
getDeclaredMethods(): 获取类中所有的方法(public、protected、default、private)
getDeclaredMethod(String name, Class[] params): 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型
单独的T 代表一个类型(表现形式是一个类名而已) ,而 Class<T>代表这个类型所对应的类(又可以称做类实例、类类型、字节码文件), Class<?>表示类型不确定的类
Class<T>表示T类型的字节码文件,意思是:
Class<T> 相当于Class<T> c=T.class,T t new T() ;
或者Class<T> c= t.getClass();
通过以上可以获取类名为c.getName();
反射补充:
反射的基本概念: Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法(即使是private的),可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键(来自 百度百科)
除了int等基本类型外,Java的其他类型全部都是class(包括interface)。例如:
仔细思考,我们可以得出结论:class(包括interface)的本质是数据类型(Type)。无继承关系的数据类型无法赋值: 而class是由JVM在执行过程中,动态加载的,JVM在第一次读取到class类型时,将其加载入内存 每加载一种class,JVM就为其创建一个Class类型的实例,并关联起来。注意:这里的Class类型是一个名叫Class的class。它长这样:
以String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:
这个Class实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class类的构造方法是private,只有JVM能创建Class实例,我们自己的Java程序是无法创建Class实例的。 所以,JVM持有的每个Class实例都指向一个数据类型(class或interface):
一个Class实例包含了该class的所有完整信息:
由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。 这种通过Class实例获取class信息的方法称为反射(Reflection)。 获取一个class的Class有三种方法:
一、 直接通过class的静态变量class获取:
二、 如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:
三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:
因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例。可以用==比较两个Class实例:
Class实例与instanceof的差别:
public class ProxyFactory { private Object target;//因为是动态嘛,所以必须是Object,类型不确定 public ProxyFactory(Object target) { this.target = target; } public Object getProxy(){ /** * newProxyInstance():创建一个代理实例(通过这个方法创建的动态代理类,共有三个参数在·) * 其中有三个参数: * 1、ClassLoader:加载动态生成的代理类的类加载器 * 2、Class[] interfaces:目标对象实现的所有接口的class对象所组成的数组(动态生成的这个类,必须要和目标类实现相同的接口) * 3、InvocationHandler:调用处理的意思-设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法 */ //(要保证代理对象和目标对象实现的功能是一样的) ClassLoader classLoader = target.getClass().getClassLoader();//target改为this也可-target调用getProxy Class<?>[] interfaces = target.getClass().getInterfaces(); //getClass()返回此 Object的运行时类。 所有的类和接口都能用Class类来接收 InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /** * proxy:代理对象 * method:代理对象需要实现的方法,即其中需要重写的方法 * args:method所对应方法的参数 * 韩顺平回顾反射:method1.invoke(o); //传统方法 对象.方法() , 反射机制 方法.invoke(对象) * Object invoke(Object obj, Object... args) 在具有指定参数的指定对象上调用此 方法对象表示的基础方法。 */ Object result = null; try {//因为是Object类型的,所以要转换为String类型的看的清 System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args)); result = method.invoke(target, args); System.out.println("[动态代理][日志] "+method.getName()+",结 果:"+ result); } catch (Exception e) { e.printStackTrace(); System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage()); } finally { System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕"); } return result; } }; //JDK代理要求必须有接口 return Proxy.newProxyInstance(classLoader, interfaces,invocationHandler); } }
3.2.4、测试
@Test public void testDynamicProxy(){ ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl()); Calculator proxy = (Calculator) factory.getProxy();//这个类是我们执行代码过程中动态创建出来的,我们不知道这个类叫什么,但是它实现了接口(和目标类实现了相同的接口)可以向上转型(都是面向接口编程) proxy.div(1,0); //proxy.div(1,1); }
3.3、AOP概念及相关术语
3.3.1、概述
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程OOP的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术(底层是代理模式)
-
OOP是纵向继承机制,AOP叫做横向抽取机制(抽取非核心代码)
3.3.2、相关术语
①横切关注点
简要来说,就是从核心业务代码中抽取的
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
②通知
切面就是封装横向关注点(非核心业务代码)的--
把横切关注点抽取出来放到一个类中,这个类叫切面(每一个横切关注点都是一个方法,这个方法叫通知)
把横切关注点封装到一个类中,这个类就叫做切面,而这个切面,每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
-
前置通知:在被代理的目标方法前执行
-
返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
-
异常通知:在被代理的目标方法异常结束后执行(死于非命)
-
后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
-
环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所
有位置
③切面
封装通知方法的类。
④目标
我们要抽取非核心代码的这个对象
被代理的目标对象。
⑤代理
AOP帮助我们来创建
向目标对象应用通知之后创建的代理对象。
⑥连接点
抽取横切关注点的位置
这也是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。
⑦切入点
从哪儿抽出来,放到哪儿去
定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条
件。
3.3.3、作用
-
简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
-
代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。
3.4、基于注解的AOP
AOP是一种思想,AspectJ是AOP的具体实现方式,具体实现层就是动态代理
3.4.1、技术说明
-
动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因
为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
-
cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
-
AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
3.4.2、准备工作
IOC加的依赖有:
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包,spring-context是IOC的依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
①添加依赖
在IOC所需依赖基础上再加入下面依赖即可:
<!-- spring-aspects会帮我们传递过来aspectjweaver --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency>
②准备被代理的目标资源
接口:
public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); }
实现类:
@Component public class CalculatorPureImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; System.out.println("方法内部 result = " + result); return result; } @Override public int sub(int i, int j) { int result = i - j; System.out.println("方法内部 result = " + result); return result; } @Override public int mul(int i, int j) { int result = i * j; System.out.println("方法内部 result = " + result); return result; } @Override public int div(int i, int j) { int result = i / j; System.out.println("方法内部 result = " + result); return result; } }
3.4.3、创建切面类并配置
因为AOP是在IOC的基础上实现的,所以也要建立配置文件,把切面类和目标对象交给IOC容器管理(自己配置or注解+扫描)
不是控制层、业务层、持久层,标记为Component普通组件
// @Aspect表示这个类是一个切面类 @Aspect // @Component注解保证这个切面类能够放入IOC容器 @Component public class LoggerAspect { @Before("execution(public int com.atguigu.aop.annotation.CalculatorImpl.*(..))") public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); } @After("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))") public void afterMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->后置通知,方法名:"+methodName); } @AfterReturning(value = "execution(*com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result") public void afterReturningMethod(JoinPoint joinPoint, Object result){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result); } @AfterThrowing(value = "execution(*com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex") public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex); } @Around("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))") public Object aroundMethod(ProceedingJoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { System.out.println("环绕通知-->目标对象方法执行之前"); //目标对象(连接点)方法的执行 result = joinPoint.proceed(); System.out.println("环绕通知-->目标对象方法返回值之后"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->目标对象方法出现异常时"); } finally { System.out.println("环绕通知-->目标对象方法执行完毕"); } return result; } }
在Spring的配置文件中配置:
<!-- 基于注解的AOP的实现: 1、将目标对象和切面交给IOC容器管理(注解+扫描) 2、开启AspectJ的自动代理,为目标对象自动生成代理 3、将切面类通过注解@Aspect标识 --> <context:component-scan base-package="com.atguigu.aop.annotation"> </context:component-scan> <!--开启基于注解的AOP--> <aop:aspectj-autoproxy />
3.4.4、各种通知
必须加上这些注解才能标记为相应的通知
-
前置通知:使用@Before注解标识,在被代理的目标方法前执行 |必须要设置value属性(切入点表达式-通过切入点表达式定位到连接点-连接点就是抽取横切关注点的地方),否则报错
-
返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
-
异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命)
-
后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
-
环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包
括上面四种通知对应的所有位置
补充:
创建了代理对象就不能通过原注解方式直接访问对象了(否则报错),不然创建代理干什么(这个代理类不知道什么类型,但知道它实现了接口,我们可以向上转型)
设置了切面,不能用CalculatorImpl实现了,用代理类实现的接口Calculator(因为创建代理类的时候继承了Calculator的接口,并且是唯一的,所以可以通过接口来获取代理类的对象)
前置通知:@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))") 要作用于目标对象的某一个方法上,public int 而add方法有那么多,怎么知道它是哪一个,所以说加上包名,这里参数可以只写类型(方法重载和参数名没关系,只和参数类型个数有关系)
下图中的半圆m就代表在目标方法执行前执行
切入点表达式语法扩充:
"execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))"这样只能作用于add方法
这个* com代表任意修饰符类型
CalculatorImpl.*表示类中所有的方法
CalculatorImpl.*()表示当前方法的参数列表,这样写表示无参的方法
CalculatorImpl.*(..)所有的方法都加前置通知的话 ..表示任意的参数列表
在类下面每隔地方都能写* ,表示当前类下面的所有的类
可以写"execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))"
各种通知的执行顺序:
Spring版本5.3.x以前:
前置通知
目标操作
后置通知
返回通知或异常通知
Spring版本5.3.x以后:
前置通知
目标操作
返回通知或异常通知
后置通知
3.4.5、切入点表达式语法
①作用
②语法细节
-
用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
-
在包名的部分,一个“星* ”号只能代表包的层次结构中的一层,表示这一层是任意的。*
-
例如:.Hello匹配com.Hello,不匹配com.atguigu.Hello
-
-
在包名的部分,使用“..”表示包名任意、包的层次深度任意
-
*..Service:表示方法名以 Service 结尾,且方法所在的类名中包含任意数量的任意字符,例如 UserService、OrderService、ProductService 等。
UserService:符合条件,因为类名以 Service 结尾。
com.example.service.UserService:符合条件,因为类名以 Service 结尾,且 com.example.service 是 *..Service 的子包。
-
一个星号( *)表示匹配任意数量的任意字符,但至少要有一个字符。它可以出现在包名、类名和方法名中,但只匹配当前层级的包或子包中的类或方法。例如, * Service 表示匹配当前层级的包或子包中以 Service 结尾的类名或方法名。
-
两个星号()表示匹配任意数量的任意字符,包括 0 个字符。它可以出现在包名中,匹配任意层级的包或子包。例如,com.example. * *表示匹配 com.example 包及其所有子包中的所有类和方法。
-
com.example.** 是一个Ant风格的类路径模式。在这种模式下,** 表示任意数量的目录。
-
..:表示任意数量任意类型的子包或子类。例如,com.example..*Service 表示 com.example 包下的所有以 Service 结尾的类,com.example.service.. 表示 com.example.service 包及其子包下的所有类。
-
在Spring MVC中,
**
和..
都是Ant风格的通配符,但是它们的作用有所不同:-
**
代表任意数量的目录,例如com.example.**
表示匹配com.example
包及其所有子包下的类或资源文件。 -
..
代表任意字符的数量,常用于 SQL 查询中的LIKE
操作,例如name LIKE '%example%'
可以使用name like '%example..%'
替代。
-
-
在类名的部分,类名部分整体用号代替,表示类名任意
-
(.., int):表示方法参数包含任意数量任意类型的参数,且最后一个参数为 int 类型。
-
在类名的部分,可以使用* 号代替类名的一部分*
-
例如:Service匹配所有名称以Service结尾的类或接口
-
-
在方法名部分,可以使用号表示方法名任意
-
在方法名部分,可以使用号代替方法名的一部分
-
例如:*Operation匹配所有方法名以Operation结尾的方法
-
-
在方法参数列表部分,使用(..)表示参数列表任意
-
在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
-
在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
-
切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
-
-
在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
-
例如:execution(public int ..Service.* (.., int)) 正确*
-
*例如:execution( * int ..Service.*(.., int)) 错误
-
3.4.6、重用切入点表达式
①声明
视频中路径是execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))
@Pointcut("execution(* com.atguigu.aop.annotation.*.*(..))") public void pointCut(){}
②在同一个切面中使用
-
切入点表达式定位的是哪一个方法
@Before("pointCut()") public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); }
③在不同切面中使用
@Before("com.atguigu.aop.CommonPointCut.pointCut()") public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args); }
3.4.7、获取通知的相关信息
①获取连接点信息
获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参
-
JoinPoint 就是帮助我们获取这个切入点的信息,切入点表达式定位的是哪一个方法,这个定位的就是哪一个方法的信息
@Before("execution(public int com.atguigu.aop.annotation.CalculatorImpl.*(..))") public void beforeMethod(JoinPoint joinPoint){ //获取连接点的签名信息 String methodName = joinPoint.getSignature().getName(); //获取目标方法到的实参信息 String args = Arrays.toString(joinPoint.getArgs()); System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+ Arrays.toString(args)); }
②获取目标方法的返回值
@AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值
-
返回通知,在目标对象方法返回值之后执行(有异常就不可能有返回值)
-
returning = "result"就是设置接收目标对象方法返回值的一个参数的参数名
@AfterReturning(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result") public void afterReturningMethod(JoinPoint joinPoint, Object result){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result); }
③获取目标方法的异常
@AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常
@AfterThrowing(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex") public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){ String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex); }
3.4.8、环绕通知
@Around("execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))") public Object aroundMethod(ProceedingJoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { System.out.println("环绕通知-->目标对象方法执行之前"); //目标方法的执行,目标方法的返回值一定要返回给外界调用者 result = joinPoint.proceed(); System.out.println("环绕通知-->目标对象方法返回值之后"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->目标对象方法出现异常时"); } finally { System.out.println("环绕通知-->目标对象方法执行完毕"); } return result; }
LoggerAspect.java
package com.atguigu.spring.aop.annotation; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; /** * Date:2023/1/1 * Author:ybc * Description: * 1、在切面中,需要通过指定的注解将方法标识为通知方法 * @Before:前置通知,在目标对象方法执行之前执行 * @After:后置通知,在目标对象方法的finally字句中执行(即异常之前) * @AfterReturning:返回通知,在目标对象方法返回值之后执行(有异常就不可能有返回值) * @AfterThrowing:异常通知,在目标对象方法的catch字句中执行 * * * 2、切入点表达式:设置在标识通知的注解的value属性中 * execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int, int) * execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..) * 第一个*表示任意的访问修饰符和返回值类型 * 第二个*表示类中任意的方法 * ..表示任意的参数列表 * 类的地方也可以使用*,表示包下所有的类 * 3、重用切入点表达式 * //@Pointcut声明一个公共的切入点表达式(多次会用到,不然还要重复声明) * @Pointcut("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))") * public void pointCut(){} * 使用方式:@Before("pointCut()") 把它的value属性当做切入点表达式 * 可以在其它类下面访问不同类的已经设置的切入点,因为包都已经扫描了,当然可以 * * 4、获取连接点的信息 * 在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点所对应方法的信息 * //获取连接点所对应方法的签名信息 * Signature signature = joinPoint.getSignature(); * //获取连接点所对应方法的参数 * Object[] args = joinPoint.getArgs(); * * 5、切面的优先级 * 可以通过@Order注解的value属性设置优先级,默认值Integer的最大值 * @Order注解的value属性值越小,优先级越高(可以建立不同的类来测试) * */ @Component @Aspect //将当前组件标识为切面 public class LoggerAspect { @Pointcut("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))") public void pointCut(){} //@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int, int))") //@Before("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))") @Before("pointCut()") public void beforeAdviceMethod(JoinPoint joinPoint) { //获取连接点所对应方法的签名信息 Signature signature = joinPoint.getSignature(); //获取连接点所对应方法的参数 Object[] args = joinPoint.getArgs(); System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+ Arrays.toString(args)); } @After("pointCut()") public void afterAdviceMethod(JoinPoint joinPoint){ //获取连接点所对应方法的签名信息 Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,方法:"+signature.getName()+",执行完毕"); } /** * 在返回通知中若要获取目标对象方法的返回值 * 只需要通过@AfterReturning注解的returning属性 * 就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数 */ @AfterReturning(value = "pointCut()", returning = "result") public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result){ //获取连接点所对应方法的签名信息 Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,方法:"+signature.getName()+",结果:"+result); } /** * 在异常通知中若要获取目标对象方法的异常 * 只需要通过AfterThrowing注解的throwing属性 * 就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数 */ @AfterThrowing(value = "pointCut()", throwing = "ex") public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable ex){//Exception也可 //获取连接点所对应方法的签名信息 Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,方法:"+signature.getName()+",异常:"+ex); } @Around("pointCut()") //环绕通知的方法的返回值一定要和目标对象方法的返回值一致,把前面四个整合的效果 public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint){ Object result = null; try { System.out.println("环绕通知-->前置通知"); //就是为了表示目标对象方法的执行 在执行前后可以加上额外的操作 //相当于动态代理中result=method.invoke(target,args) 目标对象返回值是什么,Object就是什么 result = joinPoint.proceed(); System.out.println("环绕通知-->返回通知"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->异常通知"); } finally { System.out.println("环绕通知-->后置通知"); } return result; } }
切面优先级测试切面优先级测试(设置多个切面的情况下),取决于Order设置,不设置优先级默认是整数的最大值
以后要么对一个地方进行额外的通知,要么用环绕通知
3.4.9、切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
-
优先级高的切面:外面
-
优先级低的切面:里面
使用@Order注解可以控制切面的优先级:
-
@Order(较小的数):优先级高
-
@Order(较大的数):优先级低
3.5,基于XML的AOP(了解)
主要是注解为主
3.5.1、准备工作
参考基于注解的AOP环境
3.5.2、实现
< aspectj-autoproxy>这个开启基于注解的AOP就不用带了
aop:aspect 将组件设置为切面,aop:avdisor设置当前的通知的,aop:pointcut设置切入点表达式
为什么 <aop:aspect ref="loggerAspect">能直接引用?
因为当前的切面用注解+扫描的方式将其配置到了IOC容器中,所以ref引用的id默认就是类名的小驼峰
<context:component-scan base-package="com.atguigu.aop.xml"></context:component-scan> <aop:config> <!--配置切面类--> <aop:aspect ref="loggerAspect"> <!--设置一个公共的切入点表达式--> <aop:pointcut id="pointCut" expression="execution(*com.atguigu.aop.xml.CalculatorImpl.*(..))"/> <aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before> <aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after> <aop:after-returning method="afterReturningMethod" returning="result"pointcut-ref="pointCut"></aop:after-returning> <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing> <aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around> </aop:aspect> <aop:aspect ref="validateAspect" order="1"> <aop:before method="validateBeforeMethod" pointcut-ref="pointCut"> </aop:before> </aop:aspect> </aop:config>
4、声明式事务
4.1、JdbcTemplate
事物是数据库中的内容,现在需要在执行SQL时测试事务的功能的;
spring对JBDC进行了封装,封装之后有个框架springjdbc,它有个类叫JDBCTemplate,有各种CRUD方法
4.1.1、简介
Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
4.1.2、准备工作
①加入依赖
spring-orm对象关系映射(Object Relational Mapping)
spring-tx:带tx的就是操作事务的
spring-test:这个是spring整合junit的一个依赖(可以让测试类在spring的测试环境中执行,这样就不用每一次都获取IOC容器了,可以直接依赖注入获取IOC容器中的bean并使用)
<dependencies> <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!-- Spring 持久化层支持jar包 --> <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 --> <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.1</version> </dependency> <!-- Spring 测试相关 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.1</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency> <!-- 数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.31</version> </dependency> </dependencies>
②创建jdbc.properties
jdbc.user=root jdbc.password=atguigu jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver
③配置Spring的配置文件
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">这是第三方jar包,在源文件中不可能用注解+扫描的方式,我们加bean标签
在web工程下必须设置classpath,在java工程下没有web资源写jdbc.properties也可(最好直接设置classpath)
<!-- 导入外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties" /> <!-- jdbcTemplate需要配置数据源,然后property引用 --> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置 JdbcTemplate,不设置id也可以,通过类型获取 --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 装配数据源 --> <property name="dataSource" ref="druidDataSource"/> </bean>
4.1.3、测试
spring-test:这个是spring整合junit的一个依赖(可以让测试类在spring的测试环境中执行,这样就不用每一次都获取IOC容器了,可以直接依赖注入获取IOC容器中的bean并使用)
需要配置运行环境@RunWith
以后用的多的还是MyBatis,这里了解下
①在测试类装配 JdbcTemplate
-
自动装配就是让应用程序上下文为你找出依赖项的过程。说的通俗一点,就是Spring会在上下文中自动查找,并自动给bean装配与其关联的属性!
//指定当前测试类在Spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中bean @RunWith(SpringJUnit4ClassRunner.class) //设置Spring测试环境的配置文件 @ContextConfiguration("classpath:spring-jdbc.xml") public class JDBCTemplateTest { //通过自动装配的方式为当前属性赋值 @Autowired private JdbcTemplate jdbcTemplate; }
②测试增删改功能
jdbcTemplateTest.java
//指定当前测试类在Spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中bean @RunWith(SpringJUnit4ClassRunner.class) //设置Spring测试环境的配置文件 @ContextConfiguration("classpath:spring-jdbc.xml") public class JdbcTemplateTest { @Autowired private JdbcTemplate jdbcTemplate; @Test public void testInsert(){ String sql = "insert into t_user values(null,?,?,?,?,?)"; jdbcTemplate.update(sql, "root", "123", 23, "女", "123@qq.com"); } @Test public void testGetUserById(){ String sql = "select * from t_user where id = ?"; User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1);//一个占位符写1个数字 System.out.println(user); } @Test public void testGetAllUser(){ String sql = "select * from t_user"; List<User> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class)); list.forEach(System.out::println); } @Test public void testGetCount(){ String sql = "select count(*) from t_user"; //queryForObject(String sql,Class<T> requiredType)requiredType是需要转换的类型 Integer count = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println(count); } }
③查询一条数据为实体类对象
@Test //查询一条数据为一个实体类对象 public void testSelectEmpById(){ String sql = "select * from t_emp where id = ?"; Emp emp = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Emp.class), 1); System.out.println(emp); }
④查询多条数据为一个list集合
new BeanPropertyRowMapper<>(Emp.class)把查询出来的字段和对应实体类中的属性进行映射
默认映射关系-字段名和属性名一致
@Test //查询多条数据为一个list集合 public void testSelectList(){ String sql = "select * from t_emp"; List<Emp> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class)); list.forEach(emp -> System.out.println(emp)); }
⑤查询单行单列的值
@Test public void selectCount(){ String sql = "select count(id) from t_emp"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println(count); }
spring-jdbc.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" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:jdbc.properties" /> <!-- 配置数据源 --> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="druidDataSource"></property> </bean> </beans>
pojo下的User.java
public class User { private Integer id; private String username; private String password; private Integer age; private String gender; private String email; public User() { } public User(Integer id, String username, String password, Integer age, String gender, String email) { this.id = id; this.username = username; this.password = password; this.age = age; this.gender = gender; this.email = email; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + ", gender='" + gender + '\'' + ", email='" + email + '\'' + '}'; } }
4.2、声明式事务概念
4.2.1、编程式事务
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = ...; try { // 开启事务:关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 提交事务 conn.commit(); }catch(Exception e){ // 回滚事务 conn.rollBack(); }finally{ // 释放数据库连接 conn.close(); }
编程式的实现方式存在缺陷:
-
细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
-
代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
4.2.2、声明式事务
spring中有个声明式事务的功能,我们不需要自己写切面和切面的通知
我们只需要把事务管理中切面的通知事务的方法上就可以了
只要和事务相关的都可以通过声明式事务来管理
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
-
好处1:提高开发效率
-
好处2:消除了冗余的代码
-
好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性
能等各个方面的优化
所以,我们可以总结下面两个概念:
-
编程式:自己写代码实现功能
-
声明式:通过配置让框架实现功能,切面的通知直接定位到连接点上(也就是定位到需要被管理的方法上)
4.3、基于注解的声明式事务
4.3.1、准备工作
①加入依赖
<dependencies> <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!-- Spring 持久化层支持jar包 --> <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 --> <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.1</version> </dependency> <!-- Spring 测试相关 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.1</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency> <!-- 数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.31</version> </dependency> </dependencies>
②创建jdbc.properties
jdbc.user=root jdbc.password=atguigu jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver
③配置Spring的配置文件
事务需要在JdbcTmplate操作执行SQL语句的过程中来测试,所以还是需要配置JdbcTemplate的
<!--扫描组件--> <context:component-scan base-package="com.atguigu.spring.tx.annotation"> </context:component-scan> <!-- 导入外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties" /> <!-- 配置数据源 --> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置 JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 装配数据源 --> <property name="dataSource" ref="druidDataSource"/> </bean>
④创建表
unsigned无符号就是无负号,有符号整数范围是-2^31 - 2^31 - 1,无符号整数范围0 - 2^32 - 1(
模拟用户买书,查询价格,更新库存,更新余额
CREATE TABLE `t_book` ( `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称', `price` int(11) DEFAULT NULL COMMENT '价格', `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)', PRIMARY KEY (`book_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100); CREATE TABLE `t_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(20) DEFAULT NULL COMMENT '用户名', `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
⑤创建组件
创建BookController:
@Controller public class BookController { @Autowired private BookService bookService; public void buyBook(Integer bookId, Integer userId){ bookService.buyBook(bookId, userId); } }
创建接口BookService:
public interface BookService { void buyBook(Integer bookId, Integer userId); }
创建实现类BookServiceImpl:
@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao;//这里就要访问数据库了,所以创建持久层对象 @Override public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); } }
创建接口BookDao:
public interface BookDao { Integer getPriceByBookId(Integer bookId); void updateStock(Integer bookId); void updateBalance(Integer userId, Integer price); }
创建实现类BookDaoImpl:
@Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public Integer getPriceByBookId(Integer bookId) { String sql = "select price from t_book where book_id = ?"; return jdbcTemplate.queryForObject(sql, Integer.class, bookId); } @Override public void updateStock(Integer bookId) { String sql = "update t_book set stock = stock - 1 where book_id = ?";//MySQL里面是没有+=,-=这种java语法的 jdbcTemplate.update(sql, bookId);//填充占位符 } @Override public void updateBalance(Integer userId, Integer price) { String sql = "update t_user set balance = balance - ? where user_id =?"; jdbcTemplate.update(sql, price, userId);//对应两个?price,userId } }
4.3.2、测试无事务情况
在MySQL中一个sql独占一个事务且自动提交,一定要关闭自动提交,不然可能导致更新了图书库存,但是余额那里除了问题
我们设置了unsigned,数据是不能为负号的,
①创建测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:tx-annotation.xml") public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook(){ bookController.buyBook(1, 1); } }
②模拟场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
此时执行sql语句会抛出SQLException
③观察结果
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
4.3.3、加入事务
①添加事务配置
tx:annotation-driven是将事务管理器里面的切面通知作用到当前的连接点上的(我理解为桥梁),@Transactional加在了哪一个方法上,哪一个方法就是连接点,如果加在类上,类中所有的方法都是连接点;
切面的通知直接定位到连接点上(也就是定位到需要被管理的方法上-这里的通知指的是事务中的东西-commit等)
事务管理器是TransactionManager,而它是个接口,我们需要找它的实现类,它的实现类就是DataSourceTransactionManager数据源事务管理器
要想处理事务,必须要有数据源,数据源是来管理连接的,事务相关的所有代码都是通过连接对象来设置的,都是Connection点.什么什么,
所以事务管理是需要依赖于数据源的
在Spring的配置文件中添加配置:
<!--这就是一个切面transactionManager,这里面才是通知,它就可以将通知作用到加注解的地方--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务(哪里加了这个注解,哪里就是连接点);这个tx:annotation-driven的作用就是把通知作用到连接点上 --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就 是这个默认值,则可以省略这个属性 (写的就是事务管理器的id)--> <tx:annotation-driven transaction-manager="transactionManager"/>
注意:导入的名称空间需要 tx 结尾的那个。
②添加事务注解
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理
在BookServiceImpl的buybook()添加注解@Transactional
③观察结果
由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
4.3.4、@Transactional注解标识的位置
@Transactional标识在方法上,咋只会影响该方法
@Transactional标识的类上,咋会影响类中所有的方法
4.3.5、事务属性:只读
事务中都是查询的时候才能设置事物的只读,有任意一个增删改都不能用只读
①介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
②使用方式
@Transactional(readOnly = true) public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); }
③注意
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification
are not allowed
4.3.6、事务属性:超时
①介绍
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
②使用方式
5>3 ,3秒之后就会回滚,在规定时间内没有执行完就会强制回滚
@Transactional(timeout = 3) public void buyBook(Integer bookId, Integer userId) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); }
/** * Date:2022/1/2 * Author:即兴小索奇 * Description: * 声明式事务的配置步骤: * 1、在Spring的配置文件中配置事务管理器 * 2、开启事务的注解驱动 * 在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理 * @Transactional注解标识的位置: * 1、标识在方法上 * 2、标识在类上,则类中所有的方法都会被事务管理 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:tx-annotation.xml") public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook(){ //bookController.buyBook(1, 1); bookController.checkout(1, new Integer[]{1,2}); } }
③观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out:
deadline was Fri Jun 04 16:25:39 CST 2022
4.3.7、事务属性:回滚策略
①介绍
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
-
rollbackFor属性:需要设置一个Class类型的对象(因为什么而回滚,写异常所对应的class对象)
-
rollbackForClassName属性:需要设置一个字符串类型的全类名(因为什么而回滚,写全类名)
-
noRollbackFor属性:需要设置一个Class类型的对象(不因为什么而回滚)
-
norollbackForClassName属性:需要设置一个字符串类型的全类名
一般只设置no开头的两个,因为因为什么而回滚,我们声明式事务中默认情况下都是针对于运行时异常而进行回滚的,默认就是只要是运行时异常都进行回滚
noRollbackForClassName后面是{}写数组的,如果写一个可以省略{}
②使用方式
@Transactional(noRollbackFor = ArithmeticException.class) //@Transactional(noRollbackForClassName = "java.lang.ArithmeticException") public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); System.out.println(1/0); }
③观察结果
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当
出现ArithmeticException不发生回滚,因此购买图书的操作正常执行
4.3.8、事务属性:事务隔离级别
①介绍
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事
务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同
的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
-
读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
-
读已提交:READ COMMITTED、
要求Transaction01只能读取Transaction02已提交的修改。
比如02开启添加了新的事务,添加了一个用户信息,01事务是读取不到的,只有02提交了事务才能读取到
-
可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值(加锁),即Transaction01执行期间禁止其它事务对这个字段进行更新。(如果操作就会一直处于堵塞状态,直到01提交事务)
但是01可以读取这个事务被其它事务修改的数据,可能造成Transaction01已加锁的数据间接被修改,01能读到这些数据,这就造成了幻读
-
幻读是指在一个事务中,由于其他事务对数据的修改,导致同一个事务中多次读取同一数据时,读取结果不一致的现象。
MySQL的可重复读就已经避免了幻读(这是MySQL中最理想的隔离级别),01事务读取不到02事务修改的数据
弹幕:MySQL的innodb存储引擎已经通过多版本并发机制解决了幻读的问题
-
串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它
事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
各种数据库产品对事务隔离级别的支持程度:
隔离级别 | Oracle | MySQL |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
②使用方式
isolation(Isolation isolation()是枚举类型)
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别 @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交 @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交 @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读 @Transactional(isolation = Isolation.SERIALIZABLE)//串行化
4.3.9、事务属性:事务传播行为
面试经常问!
①介绍
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
-
A事务调用B事务的时候,A就会把它的事务传播到B中,B如何使用这个事务,这就叫做事务传播行为
比如两本书,比如用结账的事务,两本书有一本买不了,都买不了,如果用买书本身的事务,能买几本就买几本
②测试
BookController.java
@Controller public class BookController { @Autowired private BookService bookService; @Autowired private CheckoutService checkoutService; public void buyBook(Integer userId, Integer bookId){ bookService.buyBook(userId, bookId); } public void checkout(Integer userId, Integer[] bookIds){ checkoutService.checkout(userId, bookIds); } }
创建接口CheckoutService:
public interface CheckoutService { void checkout(Integer[] bookIds, Integer userId ); }
创建实现类CheckoutServiceImpl:
注意这里自动装配的是CheckoutServiceImpl,调用的是这个类里面的checkout方法
@Service public class CheckoutServiceImpl implements CheckoutService { @Autowired private BookService bookService; @Override //@Transactional public void checkout(Integer userId, Integer[] bookIds) { for (Integer bookId : bookIds) { bookService.buyBook(userId, bookId); } } }
在BookController中添加方法:
@Autowired private CheckoutService checkoutService; public void checkout(Integer[] bookIds, Integer userId){ checkoutService.checkout(bookIds, userId); }
BookDao
public interface BookDao{ void updateStock(Integer bookId); void updateBalance(Integer userId, Integer price); Integer getPriceByBookId(Integer bookId); }
BookDaoImpl
@Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public Integer getPriceByBookId(Integer bookId) { String sql = "select price from t_book where book_id = ?"; return jdbcTemplate.queryForObject(sql, Integer.class, bookId); } @Override public void updateStock(Integer bookId) { String sql = "update t_book set stock = stock - 1 where book_id = ?"; jdbcTemplate.update(sql, bookId); } @Override public void updateBalance(Integer userId, Integer price) { String sql = "update t_user set balance = balance - ? where user_id = ?"; jdbcTemplate.update(sql, price, userId); } }
在数据库中将用户的余额修改为100元
tx-annotation.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" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.atguigu.spring"></context:component-scan> <context:property-placeholder location="classpath:jdbc.properties" /> <!-- 配置数据源 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务 --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就 是这个默认值,则可以省略这个属性 --> <tx:annotation-driven transaction-manager="transactionManager" /> </beans>
测试方法
注意balance不能低于书本的价格,不然报错,设置的是unsigned
/** * Date:2022/7/6 * Author:ybc * Description: * 声明式事务的配置步骤: * 1、在Spring的配置文件中配置事务管理器 * 2、开启事务的注解驱动 * 在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理 * @Transactional注解标识的位置: * 1、标识在方法上 * 2、标识在类上,则类中所有的方法都会被事务管理 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:tx-annotation.xml") public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook(){ //bookController.buyBook(1, 1); bookController.checkout(1, new Integer[]{1,2}); } }
③观察结果
买书和结账都设置了transactional,默认使用的是结账的事务(只要有一本买不了整个事务回滚)
可以通过@Transactional中的propagation属性设置事务传播行为
修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性
@Transactional(propagation = Propagation.REQUIRED)使用的是调用的事务,默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行(这里用的是结账的事务)。经过观察,购买图书的方法buyBook()在checkout()中被调
用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了
@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本
Propagation设置事物的传播行为的(动植物等的)繁殖,增殖,;(观点、理论等的)传播;(运动、光线、声音等的)传送
4.4、基于XML的声明式事务
4.3.1、场景模拟
参考基于注解的声明式事务
4.3.2、修改Spring配置文件
将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置:
<aop:config> <!-- 配置事务通知和切入点表达式 --> <aop:advisor advice-ref="txAdvice" pointcut="execution(*com.atguigu.spring.tx.xml.service.impl.*.*(..))"></aop:advisor> </aop:config> <!-- tx:advice标签:配置事务通知 --> <!-- id属性:给事务通知标签设置唯一标识,便于引用 --> <!-- transaction-manager属性:关联事务管理器 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- tx:method标签:配置具体的事务方法 --> <!-- name属性:指定方法名,可以使用星号代表多个字符,get*包括所有以get开头的方法名,自己设置,按照自己命名规则,下面任选 --> <!--name也可以直接写一个*,代表所有切入点表达式--> <tx:method name="get*" read-only="true"/> <tx:method name="query*" read-only="true"/> <tx:method name="find*" read-only="true"/> <!-- read-only属性:设置只读属性 --> <!-- rollback-for属性:设置回滚的异常 --> <!-- no-rollback-for属性:设置不回滚的异常 --> <!-- isolation属性:设置事务的隔离级别 --> <!-- timeout属性:设置事务的超时属性 --> <!-- propagation属性:设置事务的传播行为 --> <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> </tx:attributes> </tx:advice>
注意:基于xml实现的声明式事务,必须引入aspectJ的依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency>