Spring

一、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 bean
failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed
to instantiate [com.zh.spring.bean.HelloWorld]: No default constructor found; nested
exception 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 bean
of type 'com.atguigu.spring.bean.HelloWorld' available: expected single matching bean but
found 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 &lt; b"/>

<&lt;

>&gt

④ 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' 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.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:listutil: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 = root
jdbc.password =
jdbc.url = jdbc : mysql : //localhost : 3306/ssm?serverTimezone = UTC
jdbc.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 > {
        @Override
        public User getObject () throws Exception {
                return new User ();
        }
        @Override
        public 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   属性设置自动装配效果
自动装配方式: byType
byType :根据类型匹配 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>
自动装配方式: byName
byName :将自动装配的属性的属性名,作为 
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注解的基础

上起了三个新的名字

它们本质上一样,但为了代码的可读性,为

了程序结构严谨我们肯定不能随便胡乱标记

⑥ 创建组件

创建控制层组件

@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 {
}

⑦ 扫描组件

情况一:最基本的扫描方式

<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 class
UserServiceImpl implements UserService {}

(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 方法

@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 工作流程

 首先根据所需要的组件类型到 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 ,则表示能
装就装,装不上就不装,此时自动装配的属性
为默认值
但是实际开发时,基本上所有需要装配组件的
地方都是必须装配的,用不上这个属性。

三、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  为  的用户,购买  id  为  的图书
用户余额为  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 is
read-only . Queries leading to data modification
are 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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值