一、Spring 家族
1. Spring Framework 的特性
① 非侵入式:对应用程序本身的结构影响非常小
② 控制反转:IOC,翻转资源获取方向
③ 面向切面编程:AOP,在不修改源代码的基础上增强代码功能
④ 容器:包含并管理组件对象的生命周期
⑤ 组件化:实现了使用简单的组件配置组合成一个复杂的应用
⑥ 声明式:只需要声明需求即可实现以前需要编写代码才能实现的功能
⑦ 一站式
2. Spring Framework 五大功能模块
功能模块
|
功能介绍
|
Core Container
|
核心容器,在
Spring
环境下使用任何功能
都必须基于
IOC
容器。
|
AOP&Aspects
|
面向切面编程
|
Testing
|
提供了对
junit
或
TestNG
测试框架的整合
|
Data Access/Integration
|
提供了对数据访问
/
集成的功能。
|
Spring MVC
|
提供了面向
Web
应用程序的集成功能。
|
二、IOC
1. IOC 容器
IOC:Inversion of Control,反转控制
优点:不必关心资源创建过程的所有细节
DI:Dependency injection,依赖注入
组件以一些预先定义好的方式接受来自于容器的资源注入
IOC 是一种反转控制思想,而 DI 是对 IOC 的一种具体实现
(2) IOC 容器在 Spring 中的实现
IOC 容器中管理地组件也叫做 bean
在创建 bean 之前,需要创建 IOC 容器
Spring 提供了 IOC 容器的两种实现方式:
① BeanFactory
是 IOC 容器的基本实现,是 Spring 内部使用的接口
面向 Spring 本身,不提供给开发人员使用
② ApplicationContext
BeanFactory 的子接口,提供了更多高级特性
面向 Spring 的使用者,几乎所有场合都使用
ApplicationContext,而不是底层的 BeanFactory
③ ApplicationContext 的主要实现类
类型名 | 简介 |
ClassPathXmlApplicationContext
|
通过读取
类路径
下的
XML
格式的
配置文件创建
IOC
容器 对象
|
FileSystemXmlApplicationContext
|
通过文件
系统路径
读取
XML
格式
的配置文件创建
IOC
容 器对象
|
ConfigurableApplicationContext
|
ApplicationContext
的子接口,包含
一些扩展方法
refresh()
和
close()
,
让
ApplicationContext
具有
启动、
关闭
和
刷新
上下文的能力
|
WebApplicationContext
|
专门为
Web
应用准备,基于
Web
环境创建
IOC
容器对 象,并将对象
引入存入
ServletContext
域中
|
2. 基于 XML 管理 bean
(1) 实验一
① 创建 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>
③ 创建类 HelloWorld
public class HelloWorld {
public void sayHello(){
System.out.println("helloworld");
}
}
④ 创建 Spring 的配置文件
⑤ 在 Spring 的配置文件中配置 bean
<!--
配置HelloWorld所对应的bean,即将HelloWorld的对象交给Spring的IOC容器管理
通过bean标签配置IOC容器所管理的bean
属性:
id:设置bean的唯一标识
class:设置bean所对应类型的全类名
-->
<bean id="helloworld" class="com.zh.spring.bean.HelloWorld"></bean>
⑥ 创建测试类测试
@Test
public void testHelloWorld(){
ApplicationContext ac = new
ClassPathXmlApplicationContext("applicationContext.xml");
HelloWorld helloworld = (HelloWorld) ac.getBean("helloworld");
helloworld.sayHello();
}
⑦ 思路
⑧ 注意:
Spring 底层默认通过反射技术调用组件类
的无参构造器来创建组件对象,如果在需要
无参构造器时,没有无参构造器,则会抛出
异常
org.springframework.beans.factory. BeanCreationException : Error creating bean with name'helloworld' defined in class path resource [applicationContext.xml]: Instantiation of beanfailed; nested exception is org.springframework.beans.BeanInstantiationException: Failedto instantiate [com.zh.spring.bean.HelloWorld]: No default constructor found; nestedexception is java.lang.NoSuchMethodException: com.zh.spring.bean.HelloWorld.<init>()
(2) 实验二:获取bean
① 方式一:根据 id 获取
由于 id 属性指定了 bean 的唯一标识,所以根据
bean 标签的 id 属性可以精确获取到一个组件对象
② 方式二:根据类型获取
@Test
public void testHelloWorld(){
ApplicationContext ac = new
ClassPathXmlApplicationContext("applicationContext.xml");
HelloWorld bean = ac.getBean(HelloWorld.class);
bean.sayHello();
}
③ 方式三:根据 id 和类型
@Test
public void testHelloWorld(){
ApplicationContext ac = new
ClassPathXmlApplicationContext("applicationContext.xml");
HelloWorld bean = ac.getBean("helloworld", HelloWorld.class);
bean.sayHello();
}
④ 注意
当根据类型获取 bean 时,要求 IOC 容器中
指定类型的 bean 有且只能有一个
当 IOC 容器中一共配置了两个:
<bean id = "helloworldOne" class = "com.zh.spring.bean.HelloWorld" ></bean><bean id = "helloworldTwo" class = "com.zh.spring.bean.HelloWorld" ></bean>
根据类型获取时会抛出异常
org.springframework.beans.factory. NoUniqueBeanDefinitionException : No qualifying beanof type 'com.atguigu.spring.bean.HelloWorld' available: expected single matching bean butfound 2: helloworldOne,helloworldTwo
没有任何一个匹配的 bean 则抛出:NoSuchBeanDefinitionException
⑤ 扩展
如果组件类实现了接口,根据接口类型
可以获取 bean,前提是 bean 唯一
如果一个接口有多个实现类,这些实现类
都配置了 bean,根据接口类型不可以获
取 bean,因为 bean 不唯一
⑥ 结论
根据类型来获取 bean 时,在满足 bean 唯一
性的前提下,其实只是看:
【对象 instanof 指定的类型】的返回结果,
只要返回的是 true 就可以认定为和类型匹配,
能够获取到
(3) 实验三:依赖注入之 setter 注入
① 创建学生类 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 时为属性赋值
<bean id="studentOne" class="com.zh.spring.bean.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-di.xml");
Student studentOne = ac.getBean("studentOne", Student.class);
System.out.println(studentOne);
}
实验四:依赖注入之构造器注入
① 在 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.zh.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>
注意:
construor-arg 标签还有两个属性可以进一步
描述构造器参数
index 属性:指定参数所在位置的索引 (从0开始)
name 属性:指定参数名
③ 测试
@Test
public void testDIBySet(){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring-di.xml");
Student studentOne = ac.getBean("studentTwo", Student.class);
System.out.println(studentOne);
}
(5) 实验五:特殊值处理
① 字面量赋值
什么是字面量?
int a = 10;
声明一个变量 a,初始化为 10,此时 a 就不
代表字母 a 了,而是作为一个变量的名字。
当我们引用 a 的时候,我们实际上拿到的值
是 10
而如果 a 是带引号的:'a',那么它现在不是
一个变量,它就是代表 a 这个字母本身,这
就是字面量。所以字面量没有引申含义,就是
我们看到的这个数据本身
<!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 -->
<property name="name" value="张三"/>
② null 值
<property name="name">
<null />
</property>
null 值得用标签表示
注意:
<property name="name" value="null"></property
以上写法,为 name 所赋的值是字符串 null
③ xml 实体
<!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 -->
<!-- 解决方案一:使用XML实体来代替 -->
<property name="expression" value="a < b"/>
< :<;
>:>
④ CDATA 节
<property name="expression">
<!-- 解决方案二:使用CDATA节 -->
<!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 -->
<!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 -->
<!-- 所以CDATA节中写什么符号都随意 -->
<value><![CDATA[a < b]]></value>
</property>
CDATA节其中的内容会原样解析<![CDATA[...]]>
CDATA节是 xml 中的一个特殊的标签,因此不能
写在一个属性中
(6) 实验六:为类类型属性赋值
① 创建班级类 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
配置 Clazz 类型的 bean:
<bean id="clazzOne" class="com.zh.spring.bean.Clazz">
<property name="clazzId" value="1111"></property>
<property name="clazzName" value="财源滚滚班"></property>
</bean>
为 Student 中的 clazz 属性赋值:
<bean id="studentFour" class="com.zh.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>
</bean>
错误演示:
<bean id="studentFour" class="com.zh.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>
<property name="clazz" value="clazzOne"></property>
</bean>
如果错把 ref 属性 写成了 value 属性,会抛出异常:Caused by: java.lang. IllegalStateException :Cannot convert value of type 'java.lang.String' torequired type 'com.atguigu.spring.bean.Clazz' forproperty 'clazz': no matching editors or conversionstrategy found意思是不能把 String 类型转换成我们要的 Clazz 类型,说明我们使用 value 属性时, Spring 只把这个属性看做一个普通的字符串,不会认为这是一个bean 的 id ,更不会根据它去找到 bean 来赋值
方式二:内部 bean
<bean id="studentFour" class="com.zh.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>
<property name="clazz">
<!-- 在一个bean中再声明一个bean就是内部bean -->
<!-- 内部bean只能用于给属性赋值,不能在外部通过IOC容器获取,因此可以省略id属性 -->
<bean id="clazzInner" class="com.zh.spring.bean.Clazz">
<property name="clazzId" value="2222"></property>
<property name="clazzName" value="远大前程班"></property>
</bean>
</property>
</bean>
方式三:级联属性赋值
<bean id="studentFour" class="com.zh.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>
<!-- 一定先引用某个bean为属性赋值,才可以使用级联方式更新属性 -->
<property name="clazz" ref="clazzOne"></property>
<property name="clazz.clazzId" value="3333"></property>
<property name="clazz.clazzName" value="最强王者班"></property>
</bean>
(7) 实验七:为数组类型属性赋值
① 修改 Student 类
在 Student 类中添加以下代码:
private String[] hobbies;
public String[] getHobbies() {
return hobbies;
}
public void setHobbies(String[] hobbies) {
this.hobbies = hobbies;
}
② 配置 bean
<bean id="studentFour" class="com.zh.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>
(8) 实验八:为集合类型属性赋值
① 为 list 集合类型属性赋值
在 Clazz 类中添加以下代码:
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.zh.spring.bean.Clazz">
<property name="clazzId" value="4444"></property>
<property name="clazzName" value="Javaee0222"></property>
<property name="students">
<list>
<ref bean="studentOne"></ref>
<ref bean="studentTwo"></ref>
<ref bean="studentThree"></ref>
</list>
</property>
</bean>
第二种方法:
<!--配置一个集合类型的 bean,需要使用 util 的约束-->
<util:list id="studentList">
<ref bean="studentOne"></ref>
<ref bean="studentTwo"></ref>
<ref bean="studentThree"></ref>
</util:list>
若为 Set 集合类型属性赋值,只需要将其中的 list 标签改为 set 标签即可
② 为 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:
<bean id="teacherOne" class="com.zh.spring.bean.Teacher">
<property name="teacherId" value="10010"></property>
<property name="teacherName" value="大宝"></property>
</bean>
<bean id="teacherTwo" class="com.zh.spring.bean.Teacher">
<property name="teacherId" value="10086"></property>
<property name="teacherName" value="二宝"></property>
</bean>
<bean id="studentFour" class="com.zh.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>
<property name="teacherMap">
<map>
<entry>
<key>
<value>10010</value>
</key>
<ref bean="teacherOne"></ref>
</entry>
<entry>
<key>
<value>10086</value>
</key>
<ref bean="teacherTwo"></ref>
</entry>
</map>
</property>
</bean>
③ 引用集合类型的 bean
<!--list集合类型的bean-->
<util:list id="students">
<ref bean="studentOne"></ref>
<ref bean="studentTwo"></ref>
<ref bean="studentThree"></ref>
</util:list>
<!--map集合类型的bean-->
<util:map id="teacherMap">
<entry>
<key>
<value>10010</value>
</key>
<ref bean="teacherOne"></ref>
</entry>
<entry>
<key>
<value>10086</value>
</key>
<ref bean="teacherTwo"></ref>
</entry>
</util:map>
<bean id="clazzTwo" class="com.zh.spring.bean.Clazz">
<property name="clazzId" value="4444"></property>
<property name="clazzName" value="Javaee0222"></property>
<property name="students" ref="students"></property>
</bean>
<bean id="studentFour" class="com.zh.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>
<property name="teacherMap" ref="teacherMap"></property>
</bean>
使用 util:list、util:map 标签必须引入相应
的命名空间,可以通过 idea 的提示功能选择
(9) 实验九:p 命名空间
引入 p 命名空间后,可以通过以下方式为 bean 的各个属性赋值
<bean id="studentSix" class="com.zh.spring.bean.Student"
p:id="1006" p:name="小明" p:clazz-ref="clazzOne"
p:teacherMap-ref="teacherMap">
</bean>
(10) 实验十:引入外部属性文件
① 加入依赖
<!-- 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 = rootjdbc.password =jdbc.url = jdbc : mysql : //localhost : 3306/ssm?serverTimezone = UTCjdbc.driver = com.mysql.cj.jdbc.Driver
③ 引入属性文件
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
④ 配置 bean
<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("springdatasource.xml");
DataSource dataSource = ac.getBean(DataSource.class);
Connection connection = dataSource.getConnection();
System.out.println(connection);
}
(11) 实验十一: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.zh.bean.User" scope="prototype"></bean>
④ 测试
@Test
public void testBeanScope(){
ApplicationContext ac = new
ClassPathXmlApplicationContext("springscope.xml");
User user1 = ac.getBean(User.class);
User user2 = ac.getBean(User.class);
System.out.println(user1==user2);
}
(12) 实验十二:bean 的生命周期
① 具体的生命周期过程
bean 对象创建 (调用无参构造器)
给 bean 对象设置属性
bean 对象初始化之前操作 (由 bean 的后置处理器负责)
bean 对象初始化 (需在配置 bean 时指定初始化方法)
bean 对象初始化之后操作 (由 bean 的后置处理器负责)
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、销毁");
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
注意其中的 initMethod() 和 destroyMethod(),
可以通过配置 bean 指定为初始化和销毁的方法
③ 配置 bean
<!-- 使用init-method属性指定初始化方法 -->
<!-- 使用destroy-method属性指定销毁方法 -->
<bean class="com.zh.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();
}
⑤ bean 的后置处理器
bean 的 后置处理器 会在生命周期的初始化前后 添加额外的操作 ,需要 实现BeanPostProcessor 接口 , 且配置到 IOC容器中,需要注意的是, bean 后置处理器不是单独针对某一个 bean 生效,而是针对IOC 容器中 所有bean都会执行
创建 bean 的后置处理器:
package com.zh.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 {
System.out.println("☆☆☆" + beanName + " = " + bean);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
System.out.println("★★★" + beanName + " = " + bean);
return bean;
}
}
在 IOC 容器中配置后置处理器:
<!-- bean的后置处理器要放入IOC容器才能生效 -->
<bean id="myBeanProcessor" class="com.zh.spring.process.MyBeanProcessor"/>
(13) 实验十三:FactoryBean
FactoryBean 是 Spring 提供的一种整合第三方框架的常用机制。和普通的 bean 不同,配置一个FactoryBean 类型的 bean ,在获取 bean的时候得到的并 不是 class 属性中配置的这个 类的对象 ,而是 getObject() 方法的返回值 。通过这种机制, Spring 可以帮我们把复杂组件创建的详细过程和繁琐细节都 屏蔽起来,只把最简洁的使用界面展示给我们
将来我们整合 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.
②创建类UserFactoryBean
* 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()
*/
default boolean isSingleton() {
return true;
}
}
② 创建类 UserFactoryBean
public class UserFactoryBean implements FactoryBean < User > {@Overridepublic User getObject () throws Exception {return new User ();}@Overridepublic Class <?> getObjectType () {return User . class ;}}
③ 配置 bean
<bean id="user" class="com.zh.bean.UserFactoryBean"></bean>
④ 测试
@Test
public void testUserFactoryBean(){
//获取IOC容器
ApplicationContext ac = new ClassPathXmlApplicationContext("springfactorybean.xml");
User user = (User) ac.getBean("user");
System.out.println(user);
}
(14) 实验十四:基于 xml 的自动装配
自动装配:根据指定的策略,在 IOC 容器中匹配某一个 bean ,自动为指定的 bean 中所依赖的 类 类型或 接口 类型属性 赋值
① 场景模拟
创建类 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("保存成功");
}
}
② 配置 bean
使用 bean 标签的 autowire 属性设置自动装配效果自动装配方式: byTypebyType :根据类型匹配 IOC 容器中的某个兼容类型的 bean ,为属性 自动赋值若在 IOC 中, 没有任何一个兼容 类型的 bean 能够为属性赋值,则该属 性不装配 , no/default (表示)即值为默认值 null若在 IOC 中,有 多个 兼容类型的 bean 能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException
<bean id="userController"
class="com.zh.autowire.xml.controller.UserController" autowire="byType">
</bean>
<bean id="userService"
class="com.zh.autowire.xml.service.impl.UserServiceImpl" autowire="byType">
</bean>
<bean id="userDao" class="com.zh.autowire.xml.dao.impl.UserDaoImpl"></bean>
自动装配方式: byNamebyName :将自动装配的属性的属性名,作为bean 的 id 在 IOC 容器中匹配相对应的 bean进行 赋值 (类型匹配bean有 多个 时使用)
<bean id="userController"
class="com.zh.autowire.xml.controller.UserController" autowire="byName">
</bean>
<bean id="userService"
class="com.zh.autowire.xml.service.impl.UserServiceImpl" autowire="byName">
</bean>
<bean id="userServiceImpl"
class="com.zh.autowire.xml.service.impl.UserServiceImpl" autowire="byName">
</bean>
<bean id="userDao" class="com.zh.autowire.xml.dao.impl.UserDaoImpl"></bean>
<bean id="userDaoImpl" class="com.zh.autowire.xml.dao.impl.UserDaoImpl">
</bean>
③ 测试
@Test
public void testAutoWireByXML(){
ApplicationContext ac = new
ClassPathXmlApplicationContext("autowirexml.xml");
UserController userController = ac.getBean(UserController.class);
userController.saveUser();
}
3. 基于注解管理 bean
(1) 实验一:标记与扫描
① 注解
和 XML 配置文件一样, 注解本身 并 不能执行 ,注解本身仅仅只是做一个 标记 ,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。本质上:所有一切的操作都是 Java 代码来完成的,XML 和注解只是告诉框架中的 J ava 代码如何执行。
② 扫描
Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过 扫描 的方式,来进行检测。然后根据注解进行后续操作。
③ 新建 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 配置文件
applicationContext.xml
⑤ 标识组件的常用注解
@Component:将类标识为普通组件
@Controller:将类标识为控制层组件
@Service:将类标识为业务层组件
@Repository:将类标识为持久层组件
@Controller、@Service、@Repository
这三个注解只是在@Component注解的基础
上起了三个新的名字
它们本质上一样,但为了代码的可读性,为
了程序结构严谨我们肯定不能随便胡乱标记
⑥ 创建组件
创建控制层组件
@Controllerpublic class UserController {}
创建接口 UserService
public interface UserService {}
创建业务层组件 UserServiceImpl
@Servicepublic class UserServiceImpl implements UserService {}
创建接口 UserDao
public interface UserDao {}
创建持久层组件 UserDaoImpl
@Repositorypublic class UserDaoImpl implements UserDao {}
⑦ 扫描组件
情况一:最基本的扫描方式
<context:component-scan base-package="com.atguigu">
</context:component-scan>
情况二:指定要排除的组件
<context:component-scan base-package="com.atguigu">
<!-- context:exclude-filter标签:指定排除规则 -->
<!--
type:设置排除或包含的依据
type="annotation",根据注解排除,expression中设置要排除的注解的全类名
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.zh" use-default-filters="false">
<!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<!-- 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类 -->
<!--
type:设置排除或包含的依据
type="annotation",根据注解排除,expression中设置要排除的注解的全类名
type="assignable",根据类型排除,expression中设置要排除的类型的全类名
-->
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
<!--<context:include-filter type="assignable"
expression="com.zh.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 classUserServiceImpl implements UserService {}
(2) 实验二:基于注解的自动装配
① 场景模拟
参考基于 xml 的自动装配
在 UserController 中声明 UserService 对象
在 UserServiceImpl 中声明 UserDao 对象
② @Autowired 注解
在成员变量上直接标记@Autowired注解即可
完成自动装配,不需要提供 setXxx() 方法
@Controllerpublic class UserController {@Autowiredprivate UserService userService ;public void saveUser (){userService . saveUser ();}}
public interface UserService {void saveUser ();}
@Servicepublic class UserServiceImpl implements UserService {@Autowiredprivate UserDao userDao ;@Overridepublic void saveUser () {userDao . saveUser ();}}
public interface UserDao {void saveUser ();}
@Repositorypublic class UserDaoImpl implements UserDao {@Overridepublic void saveUser () {System . out . println ( " 保存成功 " );}}
③ @Autowired注解其他细节
@Autowired注解可以标记在构造器和 set 方法上
@Controllerpublic class UserController {private UserService userService ;@Autowiredpublic UserController ( UserService userService ){this . userService = userService ;}public void saveUser (){userService . saveUser ();}}
@Controllerpublic class UserController {private UserService userService ;@Autowiredpublic void setUserService ( UserService userService ){this . userService = userService ;}public void saveUser (){userService . saveUser ();}}
④ @Autowired 工作流程
首先根据所需要的组件类型到 IOC 容器中查找
能够找到唯一的 bean:直接执行装配
如果完全找不到匹配这个类型的 bean:装配失败
和所需类型匹配的 bean 不止一个
没有@Qualifier注解:根据@Autowired标记
位置成员变量的变量名作为 bean 的 id 进行
匹配
能够找到:执行装配
找不到:装配失败
使用@Qualifier注解:根据@Qualifier注解中
指定的名称作为 bean 的 id 进行匹配
能够找到:执行装配
找不到:装配失败
@Controllerpublic class UserController {@Autowired@Qualifier ( "userServiceImpl" )private UserService userService ;public void saveUser (){userService . saveUser ();}}
@Autowired 中有属性 required ,默认值为 true ,因此在自动装配无法找到相应的 bean 时,会装配失败可以将属性 required 的值设置为 true ,则表示能装就装,装不上就不装,此时自动装配的属性为默认值但是实际开发时,基本上所有需要装配组件的地方都是必须装配的,用不上这个属性。
三、AOP
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 );}
(2) 创建实现类
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) 创建带日志功能的实现类
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;
}
}
(4) 提出问题
① 现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
对核心业务功能有干扰,导致程序员在开发
核心业务功能时分散了精力
附加功能分散在各个业务功能方法中,不利
于统一维护
② 解决思路
解决这两个问题,核心就是:解耦
我们需要把附加功能从业务功能代码中抽取出来
③ 困难
解决问题的困难:要抽取的代码在方法内部,靠
以前把子类中的重复代码抽取到父类的方式没法
解决,所以需要引入新的技术
2. 代理模式
(1) 概念
二十三种设计模式中的一种,属于结构型模式
间接调用、解耦,调用目标方法时先调用代理
对象的方法,减少对目标方法的调用和打扰,
同时让附加功能能够集中在一起也有利于统一
维护
代理:将非核心逻辑剥离出来以后,封装这
些非核心逻辑的类、对象、方法
目标:被代理"套用"了非核心逻辑代码的类、
对象、方法
(2) 静态代理
创建静态代理类:
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorStaticProxy(Calculator 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;
}
}
静态代理实现了解耦,但是代码都 写死 了,完全不具备任何的灵活性,拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。提出进一步的需求:将 日志功能集中 到一个 代理类中,将来有任何日志需求,都通过这一个代理类来实现。需要使用 动态代理技术
(3) 动态代理
生产代理对象的工厂类:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy(){
/**
* newProxyInstance():创建一个代理实例
* 其中有三个参数:
* 1、classLoader:加载动态生成的代理类的类加载器
* 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重
写接口中的抽象方法
*/
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
/**
* proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
*/
Object result = null;
try {
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;
}
};
return Proxy.newProxyInstance(classLoader, interfaces,
invocationHandler);
}
}
(4) 测试
@Test
public void testDynamicProxy(){
ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl());
Calculator proxy = (Calculator) factory.getProxy();
proxy.div(1,0);
//proxy.div(1,1);
}
动态代理有两种:
① jdk 动态代理,要求必须有接口,最终生成的
代理类和目标类实现相同的接口,
在 com.sun.proxy 包下,类名为 $proxy2
② cglib 动态代理,最终生成的代理类会继承目
标类,并且和目标类在相同的包下
3. AOP 概念及相关术语
(1) 概述
AOP 是一种设计思想,是软件设计领域中的面向
切面编程的一种补充和完善,它以通过预编译方式
和运行期动态代理方式实现在不修改源代码的情况
下给程序动态统一添加额外功能的一种技术
(2) 相关术语
① 横切关注点
从每个方法中抽取出来的同一类非核心业务,在同
一个项目中,我们可以使用多个横切关注点对相关
方法进行多个不同方面的增强
有多少个附加功能就有多少个横切关注点
② 通知
每一个横切关注点上要做的事情都需要写一个方法
来实现,这样的方法就叫通知方法
前置通知:在被代理的目标方法前执行
返回通知:在被代理的目标方法成功结束后执行
(寿终正寝)
异常通知:在被代理的目标方法异常结束后执行
(死于非命)
后置通知:在被代理的目标方法最终结束后执行
(盖棺定论)
环绕通知:使用 try...catch...finally 结构围绕整个
被代理的目标方法,包括上面四种通
知对应的所有位置
③ 切面
封装通知方法的类
④ 目标
被代理的目标对象
⑤ 代理
向目标对象应用通知之后创建的代理对象
⑥ 连接点
把方法排成一排,每一个横切位置看成 x 轴
方向,把方法从上到下执行的顺序看成 y 轴,
x 轴和 y 轴的交叉点就是连接点
⑦ 切入点
定位连接点的方式
每个类的方法中都包含多个连接点,所以连接点
是类中客观存在的事物 (从逻辑上来说)
如果把连接点看作数据库中的记录,那么切入点
就是查询记录的 SQL 语句
Spring 的 AOP 技术可以通过切入点定位到特定
的连接点
切点通过 org.springframework.aop.Pointcut 接
口进行描述,它使用类和方法作为连接点的查询
条件
(3) 作用
简化代码:把方法中固定位置的重复代码抽取出
来,让被抽取的方法更专注自己的核
心功能,提高内聚性
代码增强:把特定的功能封装到切面类中,看哪
里有需要,就往上套,被套用了切面
逻辑的方法就被切面给增强了
4. 基于注解的 AOP
(1) 动态代理:代理对象和目标对象实现同样的接口
(拜把子模式)
cglib:通过继承被代理的目标类(认干爹模式)
AspectJ:本质上是静态代理,将代理逻辑"织入"被
代理的目标类编译得到的字节码文件,
weaver 是织入器
(2) 准备工作
① 添加依赖
在 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) 创建切面类并配置
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {
@Before("execution(public int com.zh.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.zh.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.zh.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.zh.aop.annotation">
</context:component-scan>
<aop:aspectj-autoproxy />
(4) 各种通知
前置通知:@Before,在目标对象方法执行之
前执行
返回通知:@AfterReturning,在目标对象方法
返回值之后执行
异常通知:@AfterThrowing,在目标对象方法
的 catch 字句中执行
后置通知:@After,在目标对象方法的 finally
字句中执行
环绕通知:@Around
各种通知的执行顺序:
Spring版本5.3.x以前:
前置、目标操作、后置、返回或异常
Spring版本5.3.x以后:
前置、目标操作、返回或异常、后置
(5) 切入点表达式语法
① 作用
② 语法细节
用 * 号 代替 “ 权限修饰符 ” 和 “ 返回值 ” 部分表示 “ 权限修饰符 ” 和 “ 返回值 ” 不限在 包名 的部分, 一个 “*”号 只能代表 包的层次结构中的 一层 ,表示这一层是任意的。例如: *.Hello 匹配 com.Hello,不匹配 com.zh.Hello在 包名 的部分,使用 “ *.. ” 表示 包名任意 、包的层次深度任意在 类名 的部分,类名部分整体用 *号 代替,表示类名任意在类名的部分,可以使用 * 号 代替类名的一部分例如 :*Service匹配所有名称以Service结尾的类或接口在 方法名 部分,可以使用 *号 表示 方法名任意在方法名部分,可以使用 * 号代替方法名的一部分例如: *Operation匹配所有方法名以Operation结尾的方法在 方法参数列表 部分,使用 ( .. ) 表示 参数列表任意在方法参数列表部分,使用 (int,..) 表示参数列表以一个 int 类型的参数开头在方法参数列表部分, 基本数据类型 和对应的 包装类型 是 不一样 的切入点表达式中使用 int 和实际方法中 Integer 是 不匹配 的在 方法返回值 部分,如果想要明确指定一个返回值类型,那么 必须同时写明 权限修饰符例如: execution(public int ..Service.*(.., int)) 正确例如: execution(* int ..Service.*(.., int)) 错误
总结:
切入点表达式:设置在标识通知的注解的 value 属
性中
execution(public int com.zh.spring.aop.annotation
.CalculatorImpl.add(int,int))
execution(* com.zh.spring.aop.annotation
.CalculatorImpl.*(..)
第一个 * 表示任意的访问修饰符和返回值类型
第二个 * 表示类中任意的方法
.. 表示任意的参数列表
类的地方也可以使用 *,表示包下所有的类
(6) 重用切入点表达式
① 声明
@Pointcut("execution(* com.zh.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.zh.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);
}
(7) 获取通知的相关信息
① 获取连接点信息
获取连接点信息可以在通知方法的参数位置
设置 JoinPoint 类型的形参
@Before("execution(public int com.zh.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);
}
② 获取目标方法的返回值
@AfterReturning 中的属性 returning,用来
通知方法的某个形参,接收目标方法的返回值
@AfterReturning(value = "execution(* com.zh.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.zh.aop.annotation.CalculatorImpl.*
(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
(8) 环绕通知
环绕通知的方法一定要和目标对象方法的返回值一致
@Around("execution(* com.zh.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;
}
(9) 切面的优先级
相同目标方法上同时存在多个切面时,切面的
优先级控制切面的内外嵌套顺序
优先级高的切面:外面
优先级低的切面:里面
使用@Order注解可以控制切面的优先级:
默认值:Integer 的最大值
@Order(较小的数):优先级高
@Order(较大的数):优先级低
5. 基于 XML 的 AOP
(1) 准备工作
(2) 实现
<context:component-scan base-package="com.zh.aop.xml"></context:componentscan>
<aop:config>
<!--配置切面类-->
<aop:aspect ref="loggerAspect">
<aop:pointcut id="pointCut" expression="execution(*
com.zh.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>
四、声明式事务
1. JdbcTemplate
(1) 简介
Spring 框架对 JDBC 进行封装,使用 JdbcTemplate
方便实现对数据库操作
(2) 准备工作
① 加入依赖
<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=
jdbc.url=jdbc:mysql://localhost:3306/ssm
jdbc.driver=com.mysql.cj.jdbc.Driver
③ 配置 Spring 的配置文件
<!-- 导入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${atguigu.url}"/>
<property name="driverClassName" value="${atguigu.driver}"/>
<property name="username" value="${atguigu.username}"/>
<property name="password" value="${atguigu.password}"/>
</bean>
<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 装配数据源 -->
<property name="dataSource" ref="druidDataSource"/>
</bean>
(3) 测试
① 在测试类装配 JdbcTemplate
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-jdbc.xml")
public class JDBCTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
}
② 测试增删改功能
@Test
//测试增删改功能
public void testUpdate(){
String sql = "insert into t_emp values(null,?,?,?)";
int result = jdbcTemplate.update(sql, "张三", 23, "男");
System.out.println(result);
}
③ 查询一条数据为实体类对象
@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 集合
@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);
}
2. 声明式事务概念
(1) 编程式事务
事务功能的相关操作全部通过自己编写代码
来实现:
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
编程式的实现方式存在缺陷:
细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较 繁琐代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码, 代码就没有得到复用
(2) 声明式事务
编程式:自己写代码实现功能
声明式:通过配置让框架实现功能
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=
jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
jdbc.driver=com.mysql.cj.jdbc.Driver
③ 配置 Spring 的配置文件
<!--扫描组件-->
<context:component-scan base-package="com.zh.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>
④ 创建表
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 = ?";
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);
}
}
(2) 测试无事务情况
① 创建测试类
@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
③ 观察结果
因为没有添加事务,图书的 库存更新 了,但是用户的 余额没有更新显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
(3) 加入事务
① 添加事务配置
在 Spring 的配置文件中添加配置:
<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" />
注:导入的名称空间需要 tx 结尾的那个
② 添加事务注解
因为 service 层 表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在 service层处理在 BookServiceImpl 的 buybook() 添加注解@Transactional
③ 观察结果
由于使用了 Spring 的声明式事务,更新库存和更新
余额都没有执行
(4) @Transactional注解标识的位置
标识在 方法 上标识的 类 上,则类中所有的方法都会被事务管理
(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 isread-only . Queries leading to data modificationare not allowed
(6) 事务属性:超时
① 介绍
超时回滚,释放资源
② 使用方式
@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);
}
③ 观察结果
执行过程中抛出异常:
org.springframework.transaction. TransactionTimedOutException : Transaction timed out:deadline was Fri Jun 04 16:25:39 CST 2022
(7) 事务属性:回滚策略
① 介绍
声明式事务默认只针对运行时异常回滚,
编译时异常不回滚
可以通过 @Transactional 中相关属性设置
回滚策略
rollbackFor 属性:需要设置一个 Class类型的对象rollbackForClassName 属性:需要设置一个 字符串类型 的全类名noRollbackFor 属性:需要设置一个 Class类型的对象rollbackFor 属性:需要设置一个字符串类型的全类名
② 使用方式
@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 不发生回滚,因此购买图书的操作正常执行
(8) 事务属性:事务隔离级别
① 介绍
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL 标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
读 未 提交: READ UNCOMMITTED允许 Transaction01 读取 Transaction02 未提交的修改。读 已 提交: READ COMMITTED 、要求 Transaction01 只能读取 Transaction02已提交的修改。可重复读 : REPEATABLE READ确保 Transaction01 可以多次从一个字段中读取到相同的值,即 Transaction01 执行期间禁止其它事务对这个字段进行更新。串行化 : SERIALIZABLE确保 Transaction01 可以多次从一个表中读取到相同的行,在 Transaction01 执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别
|
脏读
|
不可重复读
|
幻读
|
READ UNCOMMITTED
|
有
|
有
|
有
|
READ COMMITTED
|
无
|
有
|
有
|
REPEATABLE READ
|
无
|
无
|
有
|
SERIALIZABLE
|
无
|
无
|
无
|
各种数据库产品对事务隔离级别的支持程度:
隔离级别
|
Oracle
|
MySQL
|
READ UNCOMMITTED
|
×
|
√
|
READ COMMITTED
|
√
(
默认
)
|
√
|
REPEATABLE READ
|
×
|
√
(
默认
)
|
SERIALIZABLE
|
√
|
√
|
② 使用方式
@Transactional ( isolation = Isolation . DEFAULT )// 使用数据库默认的隔离级别@Transactional ( isolation = Isolation . READ_UNCOMMITTED ) // 读未提交@Transactional ( isolation = Isolation . READ_COMMITTED ) // 读已提交@Transactional ( isolation = Isolation.REPEATABLE_READ ) // 可重复读@Transactional ( isolation = Isolation.SERIALIZABLE ) // 串行化
(9) 事务属性:事务传播行为
① 介绍
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
② 测试
创建接口 CheckoutService:
public interface CheckoutService {
void checkout(Integer[] bookIds, Integer userId);
}
创建实现类 CheckoutServiceImpl:
@Service
public class CheckoutServiceImpl implements CheckoutService {
@Autowired
private BookService bookService;
@Override
@Transactional
//一次购买多本图书
public void checkout(Integer[] bookIds, Integer userId) {
for (Integer bookId : bookIds) {
bookService.buyBook(bookId, userId);
}
}
}
在 BookController 中添加方法:
@Autowired
private CheckoutService checkoutService;
public void checkout(Integer[] bookIds, Integer userId){
checkoutService.checkout(bookIds, userId);
}
在数据库中将用户的余额修改为100元
③ 观察结果
可以通过 @Transactional 中的 propagation属性 设置事务传播行为修改 BookServiceImpl 中 buyBook() 上,注解@Transactional 的 propagation 属性@Transactional(propagation = Propagation. REQUIRED ) , 默认 情况,表示 如果当前线程上有已经开启的事务可用,那么就在这个事务中运行 。经过观察,购买图书的方法 buyBook() 在checkout() 中被调用,checkout() 上有事务注解,因此在此事务中执行。所购买的两本图书的价格为 80 和 50 ,而用户的余额为100 ,因此在购买第二本图书时余额不足失败,导致整个 checkout() 回滚,即只要有一本书买不了,就都买不了@Transactional(propagation = Propagation. REQUIRES_NEW ) ,表示 不管当前线程上是否有已经开启 的事务,都要开启新事务 。同样的场景,每次购买图书都是在 buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook() 中回滚,购买第一本图书不受影响,即能买几本就买几本
4. 基于 XML 的声明事务
(1) 场景模拟
参考基于注解的声明式事务
(2) 修改 Spring 配置文件
将 Spring 配置文件中去掉 tx:annotation-driven
标签,并添加配置:
<aop:config>
<!-- 配置事务通知和切入点表达式 -->
<aop:advisor advice-ref="txAdvice" pointcut="execution(*
com.zh.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属性:指定方法名,可以使用星号代表多个字符 -->
<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" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="update*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="delete*" read-only="false" rollbackfor="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>